From 356b7bf01ace18d82122fb39fc3a35148552105c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 29 Jun 2025 23:00:51 +0800 Subject: [PATCH] :sparkles: Developer app basis --- assets/i18n/en-US.json | 15 +- lib/models/custom_app.dart | 71 +++ lib/models/custom_app.freezed.dart | 771 +++++++++++++++++++++++++ lib/models/custom_app.g.dart | 137 +++++ lib/route.dart | 22 + lib/screens/chat/chat.dart | 41 +- lib/screens/creators/hub.dart | 43 +- lib/screens/developers/apps.dart | 65 +++ lib/screens/developers/apps.g.dart | 151 +++++ lib/screens/developers/edit_app.dart | 253 ++++++++ lib/screens/developers/edit_app.g.dart | 161 ++++++ lib/screens/developers/hub.dart | 15 + lib/screens/developers/new_app.dart | 12 + lib/screens/realm/realms.dart | 129 +++-- lib/screens/settings.dart | 19 + 15 files changed, 1813 insertions(+), 92 deletions(-) create mode 100644 lib/models/custom_app.dart create mode 100644 lib/models/custom_app.freezed.dart create mode 100644 lib/models/custom_app.g.dart create mode 100644 lib/screens/developers/apps.dart create mode 100644 lib/screens/developers/apps.g.dart create mode 100644 lib/screens/developers/edit_app.dart create mode 100644 lib/screens/developers/edit_app.g.dart create mode 100644 lib/screens/developers/new_app.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 8c522f1..a3cf418 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -632,10 +632,23 @@ "discoverRealms": "Discover Realms", "discoverPublishers": "Discover Publishers", "search": "Search", + "publisherMembers": "Collaborators", "developerHub": "Developer Hub", "developerHubUnselectedHint": "Select a developer to see stats or enroll a new one.", "enrollDeveloper": "Enroll as a Developer", "enrollDeveloperHint": "Enroll one of your publishers to become a developer.", "noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.", - "totalCustomApps": "Total Custom Apps" + "totalCustomApps": "Total Custom Apps", + "customApps": "Custom Apps", + "noCustomApps": "No custom apps yet.", + "createCustomApp": "Create Custom App", + "editCustomApp": "Edit Custom App", + "publicRealm": "Public Realm", + "publicRealmDescription": "Anyone can preview the content of this realm.", + "communityRealm": "Community Realm", + "communityRealmDescription": "Anyone can join this realm and participate in discussions. And will show in the discover page & feed.", + "publicChat": "Public Chat", + "publicChatDescription": "Anyone can preview the content of this chat. Including unjoined bots.", + "communityChat": "Community Chat", + "communityChatDescription": "Anyone can join this chat and participate in discussions." } diff --git a/lib/models/custom_app.dart b/lib/models/custom_app.dart new file mode 100644 index 0000000..df90fbb --- /dev/null +++ b/lib/models/custom_app.dart @@ -0,0 +1,71 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:island/models/file.dart'; +import 'package:island/models/user.dart'; + +part 'custom_app.freezed.dart'; +part 'custom_app.g.dart'; + +@freezed +sealed class CustomApp with _$CustomApp { + const factory CustomApp({ + @Default('') String id, + @Default('') String slug, + @Default('') String name, + String? description, + @Default(0) int status, + SnCloudFile? picture, + SnCloudFile? background, + SnVerificationMark? verification, + CustomAppOauthConfig? oauthConfig, + CustomAppLinks? links, + @Default([]) List secrets, + @Default('') String publisherId, + }) = _CustomApp; + + factory CustomApp.fromJson(Map json) => + _$CustomAppFromJson(json); +} + +@freezed +sealed class CustomAppLinks with _$CustomAppLinks { + const factory CustomAppLinks({ + String? homePage, + String? privacyPolicy, + String? termsOfService, + }) = _CustomAppLinks; + + factory CustomAppLinks.fromJson(Map json) => + _$CustomAppLinksFromJson(json); +} + +@freezed +sealed class CustomAppOauthConfig with _$CustomAppOauthConfig { + const factory CustomAppOauthConfig({ + String? clientUri, + @Default([]) List redirectUris, + List? postLogoutRedirectUris, + @Default(['openid', 'profile', 'email']) List allowedScopes, + @Default(['authorization_code', 'refresh_token']) + List allowedGrantTypes, + @Default(true) bool requirePkce, + @Default(false) bool allowOfflineAccess, + }) = _CustomAppOauthConfig; + + factory CustomAppOauthConfig.fromJson(Map json) => + _$CustomAppOauthConfigFromJson(json); +} + +@freezed +sealed class CustomAppSecret with _$CustomAppSecret { + const factory CustomAppSecret({ + @Default('') String id, + @Default('') String secret, + String? description, + DateTime? expiredAt, + @Default(false) bool isOidc, + @Default('') String appId, + }) = _CustomAppSecret; + + factory CustomAppSecret.fromJson(Map json) => + _$CustomAppSecretFromJson(json); +} diff --git a/lib/models/custom_app.freezed.dart b/lib/models/custom_app.freezed.dart new file mode 100644 index 0000000..b1e7ee7 --- /dev/null +++ b/lib/models/custom_app.freezed.dart @@ -0,0 +1,771 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'custom_app.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$CustomApp { + + String get id; String get slug; String get name; String? get description; int get status; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; CustomAppOauthConfig? get oauthConfig; CustomAppLinks? get links; List get secrets; String get publisherId; +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CustomAppCopyWith get copyWith => _$CustomAppCopyWithImpl(this as CustomApp, _$identity); + + /// Serializes this CustomApp to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other.secrets, secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(secrets),publisherId); + +@override +String toString() { + return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomAppCopyWith<$Res> { + factory $CustomAppCopyWith(CustomApp value, $Res Function(CustomApp) _then) = _$CustomAppCopyWithImpl; +@useResult +$Res call({ + String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List secrets, String publisherId +}); + + +$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;$CustomAppLinksCopyWith<$Res>? get links; + +} +/// @nodoc +class _$CustomAppCopyWithImpl<$Res> + implements $CustomAppCopyWith<$Res> { + _$CustomAppCopyWithImpl(this._self, this._then); + + final CustomApp _self; + final $Res Function(CustomApp) _then; + +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable +as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable +as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable +as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable +as CustomAppLinks?,secrets: null == secrets ? _self.secrets : secrets // ignore: cast_nullable_to_non_nullable +as List,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable +as String, + )); +} +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnCloudFileCopyWith<$Res>? get picture { + if (_self.picture == null) { + return null; + } + + return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { + return _then(_self.copyWith(picture: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnCloudFileCopyWith<$Res>? get background { + if (_self.background == null) { + return null; + } + + return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { + return _then(_self.copyWith(background: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnVerificationMarkCopyWith<$Res>? get verification { + if (_self.verification == null) { + return null; + } + + return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { + return _then(_self.copyWith(verification: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig { + if (_self.oauthConfig == null) { + return null; + } + + return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) { + return _then(_self.copyWith(oauthConfig: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CustomAppLinksCopyWith<$Res>? get links { + if (_self.links == null) { + return null; + } + + return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) { + return _then(_self.copyWith(links: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _CustomApp implements CustomApp { + const _CustomApp({this.id = '', this.slug = '', this.name = '', this.description, this.status = 0, this.picture, this.background, this.verification, this.oauthConfig, this.links, final List secrets = const [], this.publisherId = ''}): _secrets = secrets; + factory _CustomApp.fromJson(Map json) => _$CustomAppFromJson(json); + +@override@JsonKey() final String id; +@override@JsonKey() final String slug; +@override@JsonKey() final String name; +@override final String? description; +@override@JsonKey() final int status; +@override final SnCloudFile? picture; +@override final SnCloudFile? background; +@override final SnVerificationMark? verification; +@override final CustomAppOauthConfig? oauthConfig; +@override final CustomAppLinks? links; + final List _secrets; +@override@JsonKey() List get secrets { + if (_secrets is EqualUnmodifiableListView) return _secrets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_secrets); +} + +@override@JsonKey() final String publisherId; + +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CustomAppCopyWith<_CustomApp> get copyWith => __$CustomAppCopyWithImpl<_CustomApp>(this, _$identity); + +@override +Map toJson() { + return _$CustomAppToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other._secrets, _secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(_secrets),publisherId); + +@override +String toString() { + return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)'; +} + + +} + +/// @nodoc +abstract mixin class _$CustomAppCopyWith<$Res> implements $CustomAppCopyWith<$Res> { + factory _$CustomAppCopyWith(_CustomApp value, $Res Function(_CustomApp) _then) = __$CustomAppCopyWithImpl; +@override @useResult +$Res call({ + String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List secrets, String publisherId +}); + + +@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;@override $CustomAppLinksCopyWith<$Res>? get links; + +} +/// @nodoc +class __$CustomAppCopyWithImpl<$Res> + implements _$CustomAppCopyWith<$Res> { + __$CustomAppCopyWithImpl(this._self, this._then); + + final _CustomApp _self; + final $Res Function(_CustomApp) _then; + +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = null,}) { + return _then(_CustomApp( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable +as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable +as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable +as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable +as CustomAppLinks?,secrets: null == secrets ? _self._secrets : secrets // ignore: cast_nullable_to_non_nullable +as List,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnCloudFileCopyWith<$Res>? get picture { + if (_self.picture == null) { + return null; + } + + return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { + return _then(_self.copyWith(picture: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnCloudFileCopyWith<$Res>? get background { + if (_self.background == null) { + return null; + } + + return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { + return _then(_self.copyWith(background: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnVerificationMarkCopyWith<$Res>? get verification { + if (_self.verification == null) { + return null; + } + + return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { + return _then(_self.copyWith(verification: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig { + if (_self.oauthConfig == null) { + return null; + } + + return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) { + return _then(_self.copyWith(oauthConfig: value)); + }); +}/// Create a copy of CustomApp +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CustomAppLinksCopyWith<$Res>? get links { + if (_self.links == null) { + return null; + } + + return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) { + return _then(_self.copyWith(links: value)); + }); +} +} + + +/// @nodoc +mixin _$CustomAppLinks { + + String? get homePage; String? get privacyPolicy; String? get termsOfService; +/// Create a copy of CustomAppLinks +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CustomAppLinksCopyWith get copyWith => _$CustomAppLinksCopyWithImpl(this as CustomAppLinks, _$identity); + + /// Serializes this CustomAppLinks to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService); + +@override +String toString() { + return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomAppLinksCopyWith<$Res> { + factory $CustomAppLinksCopyWith(CustomAppLinks value, $Res Function(CustomAppLinks) _then) = _$CustomAppLinksCopyWithImpl; +@useResult +$Res call({ + String? homePage, String? privacyPolicy, String? termsOfService +}); + + + + +} +/// @nodoc +class _$CustomAppLinksCopyWithImpl<$Res> + implements $CustomAppLinksCopyWith<$Res> { + _$CustomAppLinksCopyWithImpl(this._self, this._then); + + final CustomAppLinks _self; + final $Res Function(CustomAppLinks) _then; + +/// Create a copy of CustomAppLinks +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) { + return _then(_self.copyWith( +homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable +as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable +as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// @nodoc +@JsonSerializable() + +class _CustomAppLinks implements CustomAppLinks { + const _CustomAppLinks({this.homePage, this.privacyPolicy, this.termsOfService}); + factory _CustomAppLinks.fromJson(Map json) => _$CustomAppLinksFromJson(json); + +@override final String? homePage; +@override final String? privacyPolicy; +@override final String? termsOfService; + +/// Create a copy of CustomAppLinks +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CustomAppLinksCopyWith<_CustomAppLinks> get copyWith => __$CustomAppLinksCopyWithImpl<_CustomAppLinks>(this, _$identity); + +@override +Map toJson() { + return _$CustomAppLinksToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService); + +@override +String toString() { + return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)'; +} + + +} + +/// @nodoc +abstract mixin class _$CustomAppLinksCopyWith<$Res> implements $CustomAppLinksCopyWith<$Res> { + factory _$CustomAppLinksCopyWith(_CustomAppLinks value, $Res Function(_CustomAppLinks) _then) = __$CustomAppLinksCopyWithImpl; +@override @useResult +$Res call({ + String? homePage, String? privacyPolicy, String? termsOfService +}); + + + + +} +/// @nodoc +class __$CustomAppLinksCopyWithImpl<$Res> + implements _$CustomAppLinksCopyWith<$Res> { + __$CustomAppLinksCopyWithImpl(this._self, this._then); + + final _CustomAppLinks _self; + final $Res Function(_CustomAppLinks) _then; + +/// Create a copy of CustomAppLinks +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) { + return _then(_CustomAppLinks( +homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable +as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable +as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$CustomAppOauthConfig { + + String? get clientUri; List get redirectUris; List? get postLogoutRedirectUris; List get allowedScopes; List get allowedGrantTypes; bool get requirePkce; bool get allowOfflineAccess; +/// Create a copy of CustomAppOauthConfig +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CustomAppOauthConfigCopyWith get copyWith => _$CustomAppOauthConfigCopyWithImpl(this as CustomAppOauthConfig, _$identity); + + /// Serializes this CustomAppOauthConfig to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other.redirectUris, redirectUris)&&const DeepCollectionEquality().equals(other.postLogoutRedirectUris, postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other.allowedScopes, allowedScopes)&&const DeepCollectionEquality().equals(other.allowedGrantTypes, allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(redirectUris),const DeepCollectionEquality().hash(postLogoutRedirectUris),const DeepCollectionEquality().hash(allowedScopes),const DeepCollectionEquality().hash(allowedGrantTypes),requirePkce,allowOfflineAccess); + +@override +String toString() { + return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomAppOauthConfigCopyWith<$Res> { + factory $CustomAppOauthConfigCopyWith(CustomAppOauthConfig value, $Res Function(CustomAppOauthConfig) _then) = _$CustomAppOauthConfigCopyWithImpl; +@useResult +$Res call({ + String? clientUri, List redirectUris, List? postLogoutRedirectUris, List allowedScopes, List allowedGrantTypes, bool requirePkce, bool allowOfflineAccess +}); + + + + +} +/// @nodoc +class _$CustomAppOauthConfigCopyWithImpl<$Res> + implements $CustomAppOauthConfigCopyWith<$Res> { + _$CustomAppOauthConfigCopyWithImpl(this._self, this._then); + + final CustomAppOauthConfig _self; + final $Res Function(CustomAppOauthConfig) _then; + +/// Create a copy of CustomAppOauthConfig +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) { + return _then(_self.copyWith( +clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable +as String?,redirectUris: null == redirectUris ? _self.redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable +as List,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self.postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable +as List?,allowedScopes: null == allowedScopes ? _self.allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable +as List,allowedGrantTypes: null == allowedGrantTypes ? _self.allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable +as List,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable +as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// @nodoc +@JsonSerializable() + +class _CustomAppOauthConfig implements CustomAppOauthConfig { + const _CustomAppOauthConfig({this.clientUri, final List redirectUris = const [], final List? postLogoutRedirectUris, final List allowedScopes = const ['openid', 'profile', 'email'], final List allowedGrantTypes = const ['authorization_code', 'refresh_token'], this.requirePkce = true, this.allowOfflineAccess = false}): _redirectUris = redirectUris,_postLogoutRedirectUris = postLogoutRedirectUris,_allowedScopes = allowedScopes,_allowedGrantTypes = allowedGrantTypes; + factory _CustomAppOauthConfig.fromJson(Map json) => _$CustomAppOauthConfigFromJson(json); + +@override final String? clientUri; + final List _redirectUris; +@override@JsonKey() List get redirectUris { + if (_redirectUris is EqualUnmodifiableListView) return _redirectUris; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_redirectUris); +} + + final List? _postLogoutRedirectUris; +@override List? get postLogoutRedirectUris { + final value = _postLogoutRedirectUris; + if (value == null) return null; + if (_postLogoutRedirectUris is EqualUnmodifiableListView) return _postLogoutRedirectUris; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + final List _allowedScopes; +@override@JsonKey() List get allowedScopes { + if (_allowedScopes is EqualUnmodifiableListView) return _allowedScopes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_allowedScopes); +} + + final List _allowedGrantTypes; +@override@JsonKey() List get allowedGrantTypes { + if (_allowedGrantTypes is EqualUnmodifiableListView) return _allowedGrantTypes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_allowedGrantTypes); +} + +@override@JsonKey() final bool requirePkce; +@override@JsonKey() final bool allowOfflineAccess; + +/// Create a copy of CustomAppOauthConfig +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CustomAppOauthConfigCopyWith<_CustomAppOauthConfig> get copyWith => __$CustomAppOauthConfigCopyWithImpl<_CustomAppOauthConfig>(this, _$identity); + +@override +Map toJson() { + return _$CustomAppOauthConfigToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other._redirectUris, _redirectUris)&&const DeepCollectionEquality().equals(other._postLogoutRedirectUris, _postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other._allowedScopes, _allowedScopes)&&const DeepCollectionEquality().equals(other._allowedGrantTypes, _allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(_redirectUris),const DeepCollectionEquality().hash(_postLogoutRedirectUris),const DeepCollectionEquality().hash(_allowedScopes),const DeepCollectionEquality().hash(_allowedGrantTypes),requirePkce,allowOfflineAccess); + +@override +String toString() { + return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)'; +} + + +} + +/// @nodoc +abstract mixin class _$CustomAppOauthConfigCopyWith<$Res> implements $CustomAppOauthConfigCopyWith<$Res> { + factory _$CustomAppOauthConfigCopyWith(_CustomAppOauthConfig value, $Res Function(_CustomAppOauthConfig) _then) = __$CustomAppOauthConfigCopyWithImpl; +@override @useResult +$Res call({ + String? clientUri, List redirectUris, List? postLogoutRedirectUris, List allowedScopes, List allowedGrantTypes, bool requirePkce, bool allowOfflineAccess +}); + + + + +} +/// @nodoc +class __$CustomAppOauthConfigCopyWithImpl<$Res> + implements _$CustomAppOauthConfigCopyWith<$Res> { + __$CustomAppOauthConfigCopyWithImpl(this._self, this._then); + + final _CustomAppOauthConfig _self; + final $Res Function(_CustomAppOauthConfig) _then; + +/// Create a copy of CustomAppOauthConfig +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) { + return _then(_CustomAppOauthConfig( +clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable +as String?,redirectUris: null == redirectUris ? _self._redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable +as List,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self._postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable +as List?,allowedScopes: null == allowedScopes ? _self._allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable +as List,allowedGrantTypes: null == allowedGrantTypes ? _self._allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable +as List,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable +as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + +/// @nodoc +mixin _$CustomAppSecret { + + String get id; String get secret; String? get description; DateTime? get expiredAt; bool get isOidc; String get appId; +/// Create a copy of CustomAppSecret +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CustomAppSecretCopyWith get copyWith => _$CustomAppSecretCopyWithImpl(this as CustomAppSecret, _$identity); + + /// Serializes this CustomAppSecret to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId); + +@override +String toString() { + return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomAppSecretCopyWith<$Res> { + factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl; +@useResult +$Res call({ + String id, String secret, String? description, DateTime? expiredAt, bool isOidc, String appId +}); + + + + +} +/// @nodoc +class _$CustomAppSecretCopyWithImpl<$Res> + implements $CustomAppSecretCopyWith<$Res> { + _$CustomAppSecretCopyWithImpl(this._self, this._then); + + final CustomAppSecret _self; + final $Res Function(CustomAppSecret) _then; + +/// Create a copy of CustomAppSecret +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable +as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// @nodoc +@JsonSerializable() + +class _CustomAppSecret implements CustomAppSecret { + const _CustomAppSecret({this.id = '', this.secret = '', this.description, this.expiredAt, this.isOidc = false, this.appId = ''}); + factory _CustomAppSecret.fromJson(Map json) => _$CustomAppSecretFromJson(json); + +@override@JsonKey() final String id; +@override@JsonKey() final String secret; +@override final String? description; +@override final DateTime? expiredAt; +@override@JsonKey() final bool isOidc; +@override@JsonKey() final String appId; + +/// Create a copy of CustomAppSecret +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CustomAppSecretCopyWith<_CustomAppSecret> get copyWith => __$CustomAppSecretCopyWithImpl<_CustomAppSecret>(this, _$identity); + +@override +Map toJson() { + return _$CustomAppSecretToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId); + +@override +String toString() { + return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)'; +} + + +} + +/// @nodoc +abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecretCopyWith<$Res> { + factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl; +@override @useResult +$Res call({ + String id, String secret, String? description, DateTime? expiredAt, bool isOidc, String appId +}); + + + + +} +/// @nodoc +class __$CustomAppSecretCopyWithImpl<$Res> + implements _$CustomAppSecretCopyWith<$Res> { + __$CustomAppSecretCopyWithImpl(this._self, this._then); + + final _CustomAppSecret _self; + final $Res Function(_CustomAppSecret) _then; + +/// Create a copy of CustomAppSecret +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) { + return _then(_CustomAppSecret( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable +as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/models/custom_app.g.dart b/lib/models/custom_app.g.dart new file mode 100644 index 0000000..cbde9a7 --- /dev/null +++ b/lib/models/custom_app.g.dart @@ -0,0 +1,137 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_app.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_CustomApp _$CustomAppFromJson(Map json) => _CustomApp( + id: json['id'] as String? ?? '', + slug: json['slug'] as String? ?? '', + name: json['name'] as String? ?? '', + description: json['description'] as String?, + status: (json['status'] as num?)?.toInt() ?? 0, + picture: + json['picture'] == null + ? null + : SnCloudFile.fromJson(json['picture'] as Map), + background: + json['background'] == null + ? null + : SnCloudFile.fromJson(json['background'] as Map), + verification: + json['verification'] == null + ? null + : SnVerificationMark.fromJson( + json['verification'] as Map, + ), + oauthConfig: + json['oauth_config'] == null + ? null + : CustomAppOauthConfig.fromJson( + json['oauth_config'] as Map, + ), + links: + json['links'] == null + ? null + : CustomAppLinks.fromJson(json['links'] as Map), + secrets: + (json['secrets'] as List?) + ?.map((e) => CustomAppSecret.fromJson(e as Map)) + .toList() ?? + const [], + publisherId: json['publisher_id'] as String? ?? '', +); + +Map _$CustomAppToJson(_CustomApp instance) => + { + 'id': instance.id, + 'slug': instance.slug, + 'name': instance.name, + 'description': instance.description, + 'status': instance.status, + 'picture': instance.picture?.toJson(), + 'background': instance.background?.toJson(), + 'verification': instance.verification?.toJson(), + 'oauth_config': instance.oauthConfig?.toJson(), + 'links': instance.links?.toJson(), + 'secrets': instance.secrets.map((e) => e.toJson()).toList(), + 'publisher_id': instance.publisherId, + }; + +_CustomAppLinks _$CustomAppLinksFromJson(Map json) => + _CustomAppLinks( + homePage: json['home_page'] as String?, + privacyPolicy: json['privacy_policy'] as String?, + termsOfService: json['terms_of_service'] as String?, + ); + +Map _$CustomAppLinksToJson(_CustomAppLinks instance) => + { + 'home_page': instance.homePage, + 'privacy_policy': instance.privacyPolicy, + 'terms_of_service': instance.termsOfService, + }; + +_CustomAppOauthConfig _$CustomAppOauthConfigFromJson( + Map json, +) => _CustomAppOauthConfig( + clientUri: json['client_uri'] as String?, + redirectUris: + (json['redirect_uris'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + postLogoutRedirectUris: + (json['post_logout_redirect_uris'] as List?) + ?.map((e) => e as String) + .toList(), + allowedScopes: + (json['allowed_scopes'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ['openid', 'profile', 'email'], + allowedGrantTypes: + (json['allowed_grant_types'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ['authorization_code', 'refresh_token'], + requirePkce: json['require_pkce'] as bool? ?? true, + allowOfflineAccess: json['allow_offline_access'] as bool? ?? false, +); + +Map _$CustomAppOauthConfigToJson( + _CustomAppOauthConfig instance, +) => { + 'client_uri': instance.clientUri, + 'redirect_uris': instance.redirectUris, + 'post_logout_redirect_uris': instance.postLogoutRedirectUris, + 'allowed_scopes': instance.allowedScopes, + 'allowed_grant_types': instance.allowedGrantTypes, + 'require_pkce': instance.requirePkce, + 'allow_offline_access': instance.allowOfflineAccess, +}; + +_CustomAppSecret _$CustomAppSecretFromJson(Map json) => + _CustomAppSecret( + id: json['id'] as String? ?? '', + secret: json['secret'] as String? ?? '', + description: json['description'] as String?, + expiredAt: + json['expired_at'] == null + ? null + : DateTime.parse(json['expired_at'] as String), + isOidc: json['is_oidc'] as bool? ?? false, + appId: json['app_id'] as String? ?? '', + ); + +Map _$CustomAppSecretToJson(_CustomAppSecret instance) => + { + 'id': instance.id, + 'secret': instance.secret, + 'description': instance.description, + 'expired_at': instance.expiredAt?.toIso8601String(), + 'is_oidc': instance.isOidc, + 'app_id': instance.appId, + }; diff --git a/lib/route.dart b/lib/route.dart index c5dfeb2..b3f2e71 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/screens/developers/apps.dart'; +import 'package:island/screens/developers/edit_app.dart'; +import 'package:island/screens/developers/new_app.dart'; import 'package:island/screens/developers/hub.dart'; import 'package:island/widgets/app_wrapper.dart'; import 'package:island/screens/tabs.dart'; @@ -162,6 +165,25 @@ final routerProvider = Provider((ref) { path: '/developers', builder: (context, state) => const DeveloperHubScreen(), ), + GoRoute( + path: '/developers/:name/apps', + builder: (context, state) => CustomAppsScreen( + publisherName: state.pathParameters['name']!, + ), + ), + GoRoute( + path: '/developers/:name/apps/new', + builder: (context, state) => NewCustomAppScreen( + publisherName: state.pathParameters['name']!, + ), + ), + GoRoute( + path: '/developers/:name/apps/:id', + builder: (context, state) => EditAppScreen( + publisherName: state.pathParameters['name']!, + id: state.pathParameters['id']!, + ), + ), ], ), diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index cdb5bf9..2c8e7df 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -676,17 +676,36 @@ class EditChatScreen extends HookConsumerWidget { (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 16), - CheckboxListTile( - title: const Text('isPublic').tr(), - subtitle: const Text('isPublicHint').tr(), - value: isPublic.value, - onChanged: (value) => isPublic.value = value ?? false, - ), - CheckboxListTile( - title: const Text('isCommunity').tr(), - subtitle: const Text('isCommunityHint').tr(), - value: isCommunity.value, - onChanged: (value) => isCommunity.value = value ?? false, + Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + CheckboxListTile( + secondary: const Icon(Symbols.public), + title: Text('publicChat').tr(), + subtitle: Text('publicChatDescription').tr(), + value: isPublic.value, + onChanged: (value) { + isPublic.value = value ?? true; + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + CheckboxListTile( + secondary: const Icon(Symbols.travel_explore), + title: Text('communityChat').tr(), + subtitle: Text('communityChatDescription').tr(), + value: isCommunity.value, + onChanged: (value) { + isCommunity.value = value ?? false; + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), ), const SizedBox(height: 16), Align( diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index 3752169..cff227a 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -356,13 +356,7 @@ class CreatorHubScreen extends HookConsumerWidget { ), ListTile( minTileHeight: 48, - title: Text('members').plural( - ref - .watch(publisherMemberStateProvider( - currentPublisher.value!.name, - )) - .total, - ), + title: Text('publisherMembers').tr(), trailing: Icon(Symbols.chevron_right), leading: const Icon(Symbols.group), contentPadding: EdgeInsets.symmetric( @@ -537,23 +531,22 @@ class PublisherMemberState { } final publisherMemberStateProvider = StateNotifierProvider.family< - PublisherMemberNotifier, PublisherMemberState, String>( - (ref, publisherUname) { - final apiClient = ref.watch(apiClientProvider); - return PublisherMemberNotifier(apiClient, publisherUname); - }, -); + PublisherMemberNotifier, + PublisherMemberState, + String +>((ref, publisherUname) { + final apiClient = ref.watch(apiClientProvider); + return PublisherMemberNotifier(apiClient, publisherUname); +}); class PublisherMemberNotifier extends StateNotifier { final String publisherUname; final Dio _apiClient; PublisherMemberNotifier(this._apiClient, this.publisherUname) - : super(const PublisherMemberState( - members: [], - isLoading: false, - total: 0, - )); + : super( + const PublisherMemberState(members: [], isLoading: false, total: 0), + ); Future loadMore({int offset = 0, int take = 20}) async { if (state.isLoading) return; @@ -569,8 +562,7 @@ class PublisherMemberNotifier extends StateNotifier { final total = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; - final members = - data.map((e) => SnPublisherMember.fromJson(e)).toList(); + final members = data.map((e) => SnPublisherMember.fromJson(e)).toList(); state = state.copyWith( members: [...state.members, ...members], @@ -596,8 +588,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget { final publisherIdentity = ref.watch( publisherIdentityProvider(publisherUname), ); - final memberListProvider = - publisherMemberListNotifierProvider(publisherUname); + final memberListProvider = publisherMemberListNotifierProvider( + publisherUname, + ); final memberState = ref.watch(publisherMemberStateProvider(publisherUname)); final memberNotifier = ref.read( publisherMemberStateProvider(publisherUname).notifier, @@ -642,9 +635,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget { Text( 'members'.plural(memberState.total), style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), ), const Spacer(), IconButton( diff --git a/lib/screens/developers/apps.dart b/lib/screens/developers/apps.dart new file mode 100644 index 0000000..ae141b3 --- /dev/null +++ b/lib/screens/developers/apps.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/custom_app.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'apps.g.dart'; + +@riverpod +Future> customApps(Ref ref, String publisherName) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/developers/$publisherName/apps'); + return resp.data.map((e) => CustomApp.fromJson(e)).cast().toList(); +} + +class CustomAppsScreen extends HookConsumerWidget { + final String publisherName; + const CustomAppsScreen({super.key, required this.publisherName}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apps = ref.watch(customAppsProvider(publisherName)); + + return AppScaffold( + appBar: AppBar(title: Text('customApps').tr()), + floatingActionButton: FloatingActionButton( + child: const Icon(Symbols.add), + onPressed: () { + context.push('/developers/$publisherName/apps/new'); + }, + ), + body: apps.when( + data: (data) { + if (data.isEmpty) { + return Center(child: Text('noCustomApps').tr()); + } + return ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + final app = data[index]; + return ListTile( + title: Text(app.name), + subtitle: Text(app.slug), + onTap: () { + context.push('/developers/$publisherName/apps/${app.id}'); + }, + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => ResponseErrorWidget( + error: err, + onRetry: () => ref.invalidate(customAppsProvider(publisherName)), + ), + ), + ); + } +} diff --git a/lib/screens/developers/apps.g.dart b/lib/screens/developers/apps.g.dart new file mode 100644 index 0000000..74a23e0 --- /dev/null +++ b/lib/screens/developers/apps.g.dart @@ -0,0 +1,151 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'apps.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$customAppsHash() => r'1dec11573b9d987c3adbdf4732b3781a6f40172a'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [customApps]. +@ProviderFor(customApps) +const customAppsProvider = CustomAppsFamily(); + +/// See also [customApps]. +class CustomAppsFamily extends Family>> { + /// See also [customApps]. + const CustomAppsFamily(); + + /// See also [customApps]. + CustomAppsProvider call(String publisherName) { + return CustomAppsProvider(publisherName); + } + + @override + CustomAppsProvider getProviderOverride( + covariant CustomAppsProvider provider, + ) { + return call(provider.publisherName); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'customAppsProvider'; +} + +/// See also [customApps]. +class CustomAppsProvider extends AutoDisposeFutureProvider> { + /// See also [customApps]. + CustomAppsProvider(String publisherName) + : this._internal( + (ref) => customApps(ref as CustomAppsRef, publisherName), + from: customAppsProvider, + name: r'customAppsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$customAppsHash, + dependencies: CustomAppsFamily._dependencies, + allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies, + publisherName: publisherName, + ); + + CustomAppsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.publisherName, + }) : super.internal(); + + final String publisherName; + + @override + Override overrideWith( + FutureOr> Function(CustomAppsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: CustomAppsProvider._internal( + (ref) => create(ref as CustomAppsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + publisherName: publisherName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _CustomAppsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CustomAppsProvider && other.publisherName == publisherName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, publisherName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin CustomAppsRef on AutoDisposeFutureProviderRef> { + /// The parameter `publisherName` of this provider. + String get publisherName; +} + +class _CustomAppsProviderElement + extends AutoDisposeFutureProviderElement> + with CustomAppsRef { + _CustomAppsProviderElement(super.provider); + + @override + String get publisherName => (origin as CustomAppsProvider).publisherName; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/developers/edit_app.dart b/lib/screens/developers/edit_app.dart new file mode 100644 index 0000000..852129f --- /dev/null +++ b/lib/screens/developers/edit_app.dart @@ -0,0 +1,253 @@ +import 'package:croppy/croppy.dart' hide cropImage; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:island/models/custom_app.dart'; +import 'package:island/models/file.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/screens/developers/apps.dart'; +import 'package:island/services/file.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:styled_widget/styled_widget.dart'; + +part 'edit_app.g.dart'; + +@riverpod +Future customApp(Ref ref, String publisherName, String id) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/developers/$publisherName/apps/$id'); + return CustomApp.fromJson(resp.data); +} + +class EditAppScreen extends HookConsumerWidget { + final String publisherName; + final String? id; + const EditAppScreen({super.key, required this.publisherName, this.id}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isNew = id == null; + final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!)); + + final formKey = useMemoized(() => GlobalKey()); + + final nameController = useTextEditingController(); + final slugController = useTextEditingController(); + final descriptionController = useTextEditingController(); + final picture = useState(null); + final background = useState(null); + + final submitting = useState(false); + + useEffect(() { + if (app?.value != null) { + nameController.text = app!.value!.name; + slugController.text = app.value!.slug; + descriptionController.text = app.value!.description ?? ''; + picture.value = app.value!.picture; + background.value = app.value!.background; + } + return null; + }, [app]); + + void setPicture(String position) async { + showLoadingModal(context); + var result = await ref + .read(imagePickerProvider) + .pickImage(source: ImageSource.gallery); + if (result == null) { + if (context.mounted) hideLoadingModal(context); + return; + } + if (!context.mounted) return; + hideLoadingModal(context); + result = await cropImage( + context, + image: result, + allowedAspectRatios: [ + if (position == 'background') + const CropAspectRatio(height: 7, width: 16) + else + const CropAspectRatio(height: 1, width: 1), + ], + ); + if (result == null) { + if (context.mounted) hideLoadingModal(context); + return; + } + if (!context.mounted) return; + showLoadingModal(context); + + submitting.value = true; + try { + final baseUrl = ref.watch(serverUrlProvider); + final token = await getToken(ref.watch(tokenProvider)); + if (token == null) throw ArgumentError('Token is null'); + final cloudFile = + await putMediaToCloud( + fileData: UniversalFile( + data: result, + type: UniversalFileType.image, + ), + atk: token, + baseUrl: baseUrl, + filename: result.name, + mimetype: result.mimeType ?? 'image/jpeg', + ).future; + if (cloudFile == null) { + throw ArgumentError('Failed to upload the file...'); + } + switch (position) { + case 'picture': + picture.value = cloudFile; + case 'background': + background.value = cloudFile; + } + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + submitting.value = false; + } + } + + void performAction() async { + final client = ref.read(apiClientProvider); + final data = { + 'name': nameController.text, + 'slug': slugController.text, + 'description': descriptionController.text, + 'picture_id': picture.value?.id, + 'background_id': background.value?.id, + }; + if (isNew) { + await client.post('/developers/$publisherName/apps', data: data); + } else { + await client.patch('/developers/$publisherName/apps/$id', data: data); + } + ref.invalidate(customAppsProvider(publisherName)); + if (context.mounted) { + Navigator.pop(context); + } + } + + return AppScaffold( + appBar: AppBar( + title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()), + ), + body: + app == null && !isNew + ? const Center(child: CircularProgressIndicator()) + : app?.hasError == true && !isNew + ? ResponseErrorWidget( + error: app!.error, + onRetry: + () => ref.invalidate(customAppProvider(publisherName, id!)), + ) + : SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + GestureDetector( + child: Container( + color: + Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + child: + background.value != null + ? CloudFileWidget( + item: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + onTap: () { + setPicture('background'); + }, + ), + Positioned( + left: 20, + bottom: -32, + child: GestureDetector( + child: ProfilePictureWidget( + fileId: picture.value?.id, + radius: 40, + fallbackIcon: Symbols.apps, + ), + onTap: () { + setPicture('picture'); + }, + ), + ), + ], + ), + ).padding(bottom: 32), + Form( + key: formKey, + child: Column( + children: [ + TextFormField( + controller: nameController, + decoration: InputDecoration(labelText: 'name'.tr()), + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: slugController, + decoration: InputDecoration( + labelText: 'slug'.tr(), + helperText: 'slugHint'.tr(), + ), + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: descriptionController, + decoration: InputDecoration( + labelText: 'description'.tr(), + ), + maxLines: 3, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: + submitting.value ? null : performAction, + label: Text('saveChanges'.tr()), + icon: const Icon(Symbols.save), + ), + ), + ], + ).padding(all: 24), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/developers/edit_app.g.dart b/lib/screens/developers/edit_app.g.dart new file mode 100644 index 0000000..f60d07b --- /dev/null +++ b/lib/screens/developers/edit_app.g.dart @@ -0,0 +1,161 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'edit_app.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$customAppHash() => r'aa4d1fb803c47a99cbacf6d91481f4fce3fda457'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [customApp]. +@ProviderFor(customApp) +const customAppProvider = CustomAppFamily(); + +/// See also [customApp]. +class CustomAppFamily extends Family> { + /// See also [customApp]. + const CustomAppFamily(); + + /// See also [customApp]. + CustomAppProvider call(String publisherName, String id) { + return CustomAppProvider(publisherName, id); + } + + @override + CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) { + return call(provider.publisherName, provider.id); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'customAppProvider'; +} + +/// See also [customApp]. +class CustomAppProvider extends AutoDisposeFutureProvider { + /// See also [customApp]. + CustomAppProvider(String publisherName, String id) + : this._internal( + (ref) => customApp(ref as CustomAppRef, publisherName, id), + from: customAppProvider, + name: r'customAppProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$customAppHash, + dependencies: CustomAppFamily._dependencies, + allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies, + publisherName: publisherName, + id: id, + ); + + CustomAppProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.publisherName, + required this.id, + }) : super.internal(); + + final String publisherName; + final String id; + + @override + Override overrideWith( + FutureOr Function(CustomAppRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: CustomAppProvider._internal( + (ref) => create(ref as CustomAppRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + publisherName: publisherName, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _CustomAppProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CustomAppProvider && + other.publisherName == publisherName && + other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin CustomAppRef on AutoDisposeFutureProviderRef { + /// The parameter `publisherName` of this provider. + String get publisherName; + + /// The parameter `id` of this provider. + String get id; +} + +class _CustomAppProviderElement + extends AutoDisposeFutureProviderElement + with CustomAppRef { + _CustomAppProviderElement(super.provider); + + @override + String get publisherName => (origin as CustomAppProvider).publisherName; + @override + String get id => (origin as CustomAppProvider).id; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/developers/hub.dart b/lib/screens/developers/hub.dart index c261add..85321ab 100644 --- a/lib/screens/developers/hub.dart +++ b/lib/screens/developers/hub.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/developer.dart'; import 'package:island/models/publisher.dart'; @@ -227,6 +228,20 @@ class DeveloperHubScreen extends HookConsumerWidget { _DeveloperStatsWidget( stats: stats, ).padding(vertical: 12, horizontal: 12), + ListTile( + minTileHeight: 48, + title: Text('customApps').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.apps), + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + onTap: () { + context.push( + '/developers/${currentDeveloper.value!.name}/apps', + ); + }, + ), ], ), ), diff --git a/lib/screens/developers/new_app.dart b/lib/screens/developers/new_app.dart new file mode 100644 index 0000000..d55b98d --- /dev/null +++ b/lib/screens/developers/new_app.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:island/screens/developers/edit_app.dart'; + +class NewCustomAppScreen extends StatelessWidget { + final String publisherName; + const NewCustomAppScreen({super.key, required this.publisherName}); + + @override + Widget build(BuildContext context) { + return EditAppScreen(publisherName: publisherName); + } +} diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 412b692..2556586 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -294,9 +294,9 @@ class EditRealmScreen extends HookConsumerWidget { child: background.value != null ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) + item: background.value!, + fit: BoxFit.cover, + ) : const SizedBox.shrink(), ), onTap: () { @@ -351,17 +351,36 @@ class EditRealmScreen extends HookConsumerWidget { (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 16), - CheckboxListTile( - title: const Text('isPublic').tr(), - subtitle: const Text('isPublicHint').tr(), - value: isPublic.value, - onChanged: (value) => isPublic.value = value ?? false, - ), - CheckboxListTile( - title: const Text('isCommunity').tr(), - subtitle: const Text('isCommunityHint').tr(), - value: isCommunity.value, - onChanged: (value) => isCommunity.value = value ?? false, + Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + CheckboxListTile( + secondary: const Icon(Symbols.public), + title: Text('publicRealm').tr(), + subtitle: Text('publicRealmDescription').tr(), + value: isPublic.value, + onChanged: (value) { + isPublic.value = value ?? true; + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + CheckboxListTile( + secondary: const Icon(Symbols.travel_explore), + title: Text('communityRealm').tr(), + subtitle: Text('communityRealmDescription').tr(), + value: isCommunity.value, + onChanged: (value) { + isCommunity.value = value ?? false; + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), ), const SizedBox(height: 16), Align( @@ -435,47 +454,47 @@ class _RealmInviteSheet extends HookConsumerWidget { (items) => items.isEmpty ? Center( - child: - Text( - 'invitesEmpty', - textAlign: TextAlign.center, - ).tr(), - ) + child: + Text( + 'invitesEmpty', + textAlign: TextAlign.center, + ).tr(), + ) : ListView.builder( - shrinkWrap: true, - itemCount: items.length, - itemBuilder: (context, index) { - final invite = items[index]; - return ListTile( - leading: ProfilePictureWidget( - fileId: invite.realm!.picture?.id, - fallbackIcon: Symbols.group, - ), - title: Text(invite.realm!.name), - subtitle: - Text( - invite.role >= 100 - ? 'permissionOwner' - : invite.role >= 50 - ? 'permissionModerator' - : 'permissionMember', - ).tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Symbols.check), - onPressed: () => acceptInvite(invite), - ), - IconButton( - icon: const Icon(Symbols.close), - onPressed: () => declineInvite(invite), - ), - ], - ), - ); - }, - ), + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final invite = items[index]; + return ListTile( + leading: ProfilePictureWidget( + fileId: invite.realm!.picture?.id, + fallbackIcon: Symbols.group, + ), + title: Text(invite.realm!.name), + subtitle: + Text( + invite.role >= 100 + ? 'permissionOwner' + : invite.role >= 50 + ? 'permissionModerator' + : 'permissionMember', + ).tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Symbols.check), + onPressed: () => acceptInvite(invite), + ), + IconButton( + icon: const Icon(Symbols.close), + onPressed: () => declineInvite(invite), + ), + ], + ), + ); + }, + ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => ResponseErrorWidget( @@ -485,4 +504,4 @@ class _RealmInviteSheet extends HookConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index bbcf2b7..7f9949d 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -341,6 +341,25 @@ class SettingsScreen extends HookConsumerWidget { ]; final behaviorSettings = [ + ListTile( + minLeadingWidth: 48, + title: Text('creatorHub').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.rocket_launch), + trailing: const Icon(Symbols.chevron_right), + onTap: () => context.push('/creators'), + ), + + // Developer Hub + ListTile( + minLeadingWidth: 48, + title: Text('developerHub').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.hub), + trailing: const Icon(Symbols.chevron_right), + onTap: () => context.push('/developers'), + ), + // Auto translate settings ListTile( minLeadingWidth: 48,