diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 622696ca..83759cc9 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -914,5 +914,13 @@ "other": "{} articles" }, "webFeedSubscribed": "The feed has been subscribed", - "webFeedUnsubscribed": "The feed has been unsubscribed" + "webFeedUnsubscribed": "The feed has been unsubscribed", + "appDetails": "App Details", + "secrets": "Secrets", + "appNotFound": "App not found.", + "secretCopied": "Secret copied to clipboard.", + "deleteSecret": "Delete Secret", + "deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.", + "generateSecret": "Generate New Secret", + "created_at": "Created at {}" } \ No newline at end of file diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 72819fe8..091c1cac 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -843,5 +843,13 @@ "socialCreditsLevelPoor": "糟糕", "socialCreditsLevelNormal": "正常", "socialCreditsLevelGood": "良好", - "socialCreditsLevelExcellent": "优秀" + "socialCreditsLevelExcellent": "优秀", + "appDetails": "应用详情", + "secrets": "密钥", + "appNotFound": "应用未找到。", + "secretCopied": "密钥已复制到剪贴板。", + "deleteSecret": "删除密钥", + "deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。", + "generateSecret": "生成新密钥", + "created_at": "创建于 {}" } diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 7ba45f44..49963ba3 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -811,5 +811,13 @@ "filesListAdditional": { "one": "+{} 個文件被摺疊", "other": "+{} 個文件被摺疊" - } + }, + "appDetails": "應用程式詳情", + "secrets": "密鑰", + "appNotFound": "找不到應用程式。", + "secretCopied": "密鑰已複製到剪貼簿。", + "deleteSecret": "刪除密鑰", + "deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。", + "generateSecret": "產生新密鑰", + "created_at": "建立於 {}" } \ No newline at end of file diff --git a/lib/models/custom_app_secret.dart b/lib/models/custom_app_secret.dart new file mode 100644 index 00000000..6a8f052e --- /dev/null +++ b/lib/models/custom_app_secret.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'custom_app_secret.freezed.dart'; +part 'custom_app_secret.g.dart'; + +@freezed +sealed class CustomAppSecret with _$CustomAppSecret { + const factory CustomAppSecret({ + required String id, + required String secret, + required DateTime createdAt, + String? description, + }) = _CustomAppSecret; + + factory CustomAppSecret.fromJson(Map json) => + _$CustomAppSecretFromJson(json); +} diff --git a/lib/models/custom_app_secret.freezed.dart b/lib/models/custom_app_secret.freezed.dart new file mode 100644 index 00000000..9e7f6771 --- /dev/null +++ b/lib/models/custom_app_secret.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// 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_secret.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$CustomAppSecret { + + String get id; String get secret; DateTime get createdAt; String? get description; +/// 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.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); + +@override +String toString() { + return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomAppSecretCopyWith<$Res> { + factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl; +@useResult +$Res call({ + String id, String secret, DateTime createdAt, String? description +}); + + + + +} +/// @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? createdAt = null,Object? description = freezed,}) { + 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,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [CustomAppSecret]. +extension CustomAppSecretPatterns on CustomAppSecret { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _CustomAppSecret value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _CustomAppSecret() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _CustomAppSecret value) $default,){ +final _that = this; +switch (_that) { +case _CustomAppSecret(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _CustomAppSecret value)? $default,){ +final _that = this; +switch (_that) { +case _CustomAppSecret() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String secret, DateTime createdAt, String? description)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _CustomAppSecret() when $default != null: +return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String secret, DateTime createdAt, String? description) $default,) {final _that = this; +switch (_that) { +case _CustomAppSecret(): +return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String secret, DateTime createdAt, String? description)? $default,) {final _that = this; +switch (_that) { +case _CustomAppSecret() when $default != null: +return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _CustomAppSecret implements CustomAppSecret { + const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description}); + factory _CustomAppSecret.fromJson(Map json) => _$CustomAppSecretFromJson(json); + +@override final String id; +@override final String secret; +@override final DateTime createdAt; +@override final String? description; + +/// 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.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); + +@override +String toString() { + return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; +} + + +} + +/// @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, DateTime createdAt, String? description +}); + + + + +} +/// @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? createdAt = null,Object? description = freezed,}) { + 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,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/models/custom_app_secret.g.dart b/lib/models/custom_app_secret.g.dart new file mode 100644 index 00000000..daf15170 --- /dev/null +++ b/lib/models/custom_app_secret.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_app_secret.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_CustomAppSecret _$CustomAppSecretFromJson(Map json) => + _CustomAppSecret( + id: json['id'] as String, + secret: json['secret'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + description: json['description'] as String?, + ); + +Map _$CustomAppSecretToJson(_CustomAppSecret instance) => + { + 'id': instance.id, + 'secret': instance.secret, + 'created_at': instance.createdAt.toIso8601String(), + 'description': instance.description, + }; diff --git a/lib/route.dart b/lib/route.dart index d6135f52..e1ffb862 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/screens/about.dart'; import 'package:island/screens/account/credits.dart'; +import 'package:island/screens/developers/app_detail.dart'; import 'package:island/screens/developers/bot_detail.dart'; import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/edit_bot.dart'; @@ -349,6 +350,16 @@ final routerProvider = Provider((ref) { id: state.pathParameters['id']!, ), ), + GoRoute( + name: 'developerAppDetail', + path: 'apps/:appId', + builder: + (context, state) => AppDetailScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + appId: state.pathParameters['appId']!, + ), + ), GoRoute( name: 'developerBotDetail', path: 'bots/:botId', diff --git a/lib/screens/developers/app_detail.dart b/lib/screens/developers/app_detail.dart new file mode 100644 index 00000000..2d782b2e --- /dev/null +++ b/lib/screens/developers/app_detail.dart @@ -0,0 +1,131 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.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/screens/developers/app_secrets.dart'; +import 'package:island/screens/developers/apps.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:styled_widget/styled_widget.dart'; + +class AppDetailScreen extends HookConsumerWidget { + final String publisherName; + final String projectId; + final String appId; + + const AppDetailScreen({ + super.key, + required this.publisherName, + required this.projectId, + required this.appId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabController = useTabController(initialLength: 2); + final appData = ref.watch(customAppProvider(publisherName, projectId, appId)); + + return AppScaffold( + appBar: AppBar( + title: Text(appData.value?.name ?? 'appDetails'.tr()), + actions: [ + IconButton( + icon: const Icon(Symbols.edit), + onPressed: appData.value == null + ? null + : () { + context.pushNamed( + 'developerAppEdit', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + 'id': appId, + }, + ); + }, + ), + ], + bottom: TabBar( + controller: tabController, + tabs: [Tab(text: 'overview'.tr()), Tab(text: 'secrets'.tr())], + ), + ), + body: appData.when( + data: (app) { + return TabBarView( + controller: tabController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _AppOverview(app: app), + AppSecretsScreen( + publisherName: publisherName, + projectId: projectId, + appId: appId, + ), + ], + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => ResponseErrorWidget( + error: err, + onRetry: () => ref.invalidate( + customAppProvider(publisherName, projectId, appId), + ), + ), + ), + ); + } +} + +class _AppOverview extends StatelessWidget { + final CustomApp app; + const _AppOverview({required this.app}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceContainer, + child: app.background != null + ? CloudFileWidget( + item: app.background!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + Positioned( + left: 20, + bottom: -32, + child: ProfilePictureWidget( + fileId: app.picture?.id, + radius: 40, + fallbackIcon: Symbols.apps, + ), + ), + ], + ), + ).padding(bottom: 32), + ListTile(title: Text('name'.tr()), subtitle: Text(app.name)), + ListTile(title: Text('slug'.tr()), subtitle: Text(app.slug)), + if (app.description?.isNotEmpty ?? false) + ListTile( + title: Text('description'.tr()), + subtitle: Text(app.description!), + ), + ], + ).padding(bottom: 24), + ); + } +} diff --git a/lib/screens/developers/app_secrets.dart b/lib/screens/developers/app_secrets.dart new file mode 100644 index 00000000..17d9ae28 --- /dev/null +++ b/lib/screens/developers/app_secrets.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/custom_app_secret.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_secrets.g.dart'; + +@riverpod +Future> customAppSecrets( + Ref ref, + String publisherName, + String projectId, + String appId, +) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', + ); + return (resp.data as List) + .map((e) => CustomAppSecret.fromJson(e)) + .cast() + .toList(); +} + +class AppSecretsScreen extends HookConsumerWidget { + final String publisherName; + final String projectId; + final String appId; + + const AppSecretsScreen({ + super.key, + required this.publisherName, + required this.projectId, + required this.appId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final secrets = ref.watch( + customAppSecretsProvider(publisherName, projectId, appId), + ); + + Future generateSecret() async { + final client = ref.read(apiClientProvider); + try { + showLoadingModal(context); + await client + .post( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', + ) + .then((_) { + ref.invalidate( + customAppSecretsProvider(publisherName, projectId, appId), + ); + }); + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } + + return secrets.when( + data: (data) { + return RefreshIndicator( + onRefresh: + () => ref.refresh( + customAppSecretsProvider( + publisherName, + projectId, + appId, + ).future, + ), + child: Column( + children: [ + ListTile( + leading: const Icon(Symbols.add), + trailing: const Icon(Symbols.chevron_right), + title: Text('appSecretsGenerate').tr(), + onTap: generateSecret, + ), + Expanded( + child: ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + final secret = data[index]; + return ListTile( + title: Text(secret.id), + subtitle: Text( + 'created_at'.tr( + args: [secret.createdAt.toIso8601String()], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Symbols.copy_all), + onPressed: () { + Clipboard.setData( + ClipboardData(text: secret.secret), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('secretCopied'.tr())), + ); + }, + ), + IconButton( + icon: const Icon(Symbols.delete, color: Colors.red), + onPressed: () { + showConfirmAlert( + 'deleteSecretHint'.tr(), + 'deleteSecret'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.read(apiClientProvider); + client + .delete( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}', + ) + .then((_) { + ref.invalidate( + customAppSecretsProvider( + publisherName, + projectId, + appId, + ), + ); + }); + } + }); + }, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => ResponseErrorWidget( + error: err, + onRetry: + () => ref.invalidate( + customAppSecretsProvider(publisherName, projectId, appId), + ), + ), + ); + } +} diff --git a/lib/screens/developers/app_secrets.g.dart b/lib/screens/developers/app_secrets.g.dart new file mode 100644 index 00000000..0d0702f6 --- /dev/null +++ b/lib/screens/developers/app_secrets.g.dart @@ -0,0 +1,188 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_secrets.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$customAppSecretsHash() => r'1bc62ad812487883ce739793b22a76168d656752'; + +/// 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 [customAppSecrets]. +@ProviderFor(customAppSecrets) +const customAppSecretsProvider = CustomAppSecretsFamily(); + +/// See also [customAppSecrets]. +class CustomAppSecretsFamily extends Family>> { + /// See also [customAppSecrets]. + const CustomAppSecretsFamily(); + + /// See also [customAppSecrets]. + CustomAppSecretsProvider call( + String publisherName, + String projectId, + String appId, + ) { + return CustomAppSecretsProvider(publisherName, projectId, appId); + } + + @override + CustomAppSecretsProvider getProviderOverride( + covariant CustomAppSecretsProvider provider, + ) { + return call(provider.publisherName, provider.projectId, provider.appId); + } + + 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'customAppSecretsProvider'; +} + +/// See also [customAppSecrets]. +class CustomAppSecretsProvider + extends AutoDisposeFutureProvider> { + /// See also [customAppSecrets]. + CustomAppSecretsProvider(String publisherName, String projectId, String appId) + : this._internal( + (ref) => customAppSecrets( + ref as CustomAppSecretsRef, + publisherName, + projectId, + appId, + ), + from: customAppSecretsProvider, + name: r'customAppSecretsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$customAppSecretsHash, + dependencies: CustomAppSecretsFamily._dependencies, + allTransitiveDependencies: + CustomAppSecretsFamily._allTransitiveDependencies, + publisherName: publisherName, + projectId: projectId, + appId: appId, + ); + + CustomAppSecretsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.publisherName, + required this.projectId, + required this.appId, + }) : super.internal(); + + final String publisherName; + final String projectId; + final String appId; + + @override + Override overrideWith( + FutureOr> Function(CustomAppSecretsRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: CustomAppSecretsProvider._internal( + (ref) => create(ref as CustomAppSecretsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + publisherName: publisherName, + projectId: projectId, + appId: appId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _CustomAppSecretsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CustomAppSecretsProvider && + other.publisherName == publisherName && + other.projectId == projectId && + other.appId == appId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); + hash = _SystemHash.combine(hash, appId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin CustomAppSecretsRef + on AutoDisposeFutureProviderRef> { + /// The parameter `publisherName` of this provider. + String get publisherName; + + /// The parameter `projectId` of this provider. + String get projectId; + + /// The parameter `appId` of this provider. + String get appId; +} + +class _CustomAppSecretsProviderElement + extends AutoDisposeFutureProviderElement> + with CustomAppSecretsRef { + _CustomAppSecretsProviderElement(super.provider); + + @override + String get publisherName => + (origin as CustomAppSecretsProvider).publisherName; + @override + String get projectId => (origin as CustomAppSecretsProvider).projectId; + @override + String get appId => (origin as CustomAppSecretsProvider).appId; +} + +// 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/apps.dart b/lib/screens/developers/apps.dart index 0d18c8db..eb7cd6f0 100644 --- a/lib/screens/developers/apps.dart +++ b/lib/screens/developers/apps.dart @@ -14,6 +14,20 @@ import 'package:styled_widget/styled_widget.dart'; part 'apps.g.dart'; +@riverpod +Future customApp( + Ref ref, + String publisherName, + String projectId, + String appId, +) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId', + ); + return CustomApp.fromJson(resp.data); +} + @riverpod Future> customApps( Ref ref, @@ -81,98 +95,114 @@ class CustomAppsScreen extends HookConsumerWidget { final app = data[index]; return Card( margin: const EdgeInsets.all(8.0), - child: Column( - children: [ - SizedBox( - height: 150, - child: Stack( - fit: StackFit.expand, - children: [ - if (app.background != null) - CloudFileWidget( - item: app.background!, - fit: BoxFit.cover, - ).clipRRect(topLeft: 8, topRight: 8), - if (app.picture != null) - Positioned( - left: 16, - bottom: 16, - child: ProfilePictureWidget( - fileId: app.picture!.id, - radius: 40, - fallbackIcon: Symbols.apps, - ), - ), - ], - ), - ), - ListTile( - title: Text(app.name), - subtitle: Text( - app.slug, - style: GoogleFonts.robotoMono(fontSize: 12), - ), - contentPadding: EdgeInsets.only(left: 20, right: 12), - trailing: PopupMenuButton( - itemBuilder: - (context) => [ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Symbols.edit), - const SizedBox(width: 12), - Text('edit').tr(), - ], + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () { + context.pushNamed( + 'developerAppDetail', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + 'appId': app.id, + }, + ); + }, + child: Column( + children: [ + SizedBox( + height: 150, + child: Stack( + fit: StackFit.expand, + children: [ + if (app.background != null) + CloudFileWidget( + item: app.background!, + fit: BoxFit.cover, + ).clipRRect(topLeft: 8, topRight: 8), + if (app.picture != null) + Positioned( + left: 16, + bottom: 16, + child: ProfilePictureWidget( + fileId: app.picture!.id, + radius: 40, + fallbackIcon: Symbols.apps, ), ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - const Icon( - Symbols.delete, - color: Colors.red, + ], + ), + ), + ListTile( + title: Text(app.name), + subtitle: Text( + app.slug, + style: GoogleFonts.robotoMono(fontSize: 12), + ), + contentPadding: EdgeInsets.only(left: 20, right: 12), + trailing: PopupMenuButton( + itemBuilder: + (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Symbols.edit), + const SizedBox(width: 12), + Text('edit').tr(), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon( + Symbols.delete, + color: Colors.red, + ), + const SizedBox(width: 12), + Text( + 'delete', + style: TextStyle(color: Colors.red), + ).tr(), + ], + ), + ), + ], + onSelected: (value) { + if (value == 'edit') { + context.pushNamed( + 'developerAppEdit', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + 'id': app.id, + }, + ); + } else if (value == 'delete') { + showConfirmAlert( + 'deleteCustomAppHint'.tr(), + 'deleteCustomApp'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.read(apiClientProvider); + client.delete( + '/develop/developers/$publisherName/projects/$projectId/apps/${app.id}', + ); + ref.invalidate( + customAppsProvider( + publisherName, + projectId, ), - const SizedBox(width: 12), - Text( - 'delete', - style: TextStyle(color: Colors.red), - ).tr(), - ], - ), - ), - ], - onSelected: (value) { - if (value == 'edit') { - context.pushNamed( - 'developerAppEdit', - pathParameters: { - 'name': publisherName, - 'projectId': projectId, - 'id': app.id, - }, - ); - } else if (value == 'delete') { - showConfirmAlert( - 'deleteCustomAppHint'.tr(), - 'deleteCustomApp'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.read(apiClientProvider); - client.delete( - '/develop/developers/$publisherName/projects/$projectId/apps/${app.id}', - ); - ref.invalidate( - customAppsProvider(publisherName, projectId), - ); - } - }); - } - }, + ); + } + }); + } + }, + ), ), - ), - ], + ], + ), ), ); }, diff --git a/lib/screens/developers/apps.g.dart b/lib/screens/developers/apps.g.dart index 0ce9b04d..f8514933 100644 --- a/lib/screens/developers/apps.g.dart +++ b/lib/screens/developers/apps.g.dart @@ -6,7 +6,7 @@ part of 'apps.dart'; // RiverpodGenerator // ************************************************************************** -String _$customAppsHash() => r'450bedaf4220b8963cb44afeb14d4c0e80f01b11'; +String _$customAppHash() => r'be05431ba8bf06fd20ee988a61c3663a68e15fc9'; /// Copied from Dart SDK class _SystemHash { @@ -29,6 +29,148 @@ class _SystemHash { } } +/// 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 projectId, String appId) { + return CustomAppProvider(publisherName, projectId, appId); + } + + @override + CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) { + return call(provider.publisherName, provider.projectId, provider.appId); + } + + 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 projectId, String appId) + : this._internal( + (ref) => + customApp(ref as CustomAppRef, publisherName, projectId, appId), + from: customAppProvider, + name: r'customAppProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$customAppHash, + dependencies: CustomAppFamily._dependencies, + allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies, + publisherName: publisherName, + projectId: projectId, + appId: appId, + ); + + CustomAppProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.publisherName, + required this.projectId, + required this.appId, + }) : super.internal(); + + final String publisherName; + final String projectId; + final String appId; + + @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, + projectId: projectId, + appId: appId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _CustomAppProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CustomAppProvider && + other.publisherName == publisherName && + other.projectId == projectId && + other.appId == appId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); + hash = _SystemHash.combine(hash, appId.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 `projectId` of this provider. + String get projectId; + + /// The parameter `appId` of this provider. + String get appId; +} + +class _CustomAppProviderElement + extends AutoDisposeFutureProviderElement + with CustomAppRef { + _CustomAppProviderElement(super.provider); + + @override + String get publisherName => (origin as CustomAppProvider).publisherName; + @override + String get projectId => (origin as CustomAppProvider).projectId; + @override + String get appId => (origin as CustomAppProvider).appId; +} + +String _$customAppsHash() => r'450bedaf4220b8963cb44afeb14d4c0e80f01b11'; + /// See also [customApps]. @ProviderFor(customApps) const customAppsProvider = CustomAppsFamily(); diff --git a/lib/screens/discovery/articles.dart b/lib/screens/discovery/articles.dart index 65dbb9d2..f1d56229 100644 --- a/lib/screens/discovery/articles.dart +++ b/lib/screens/discovery/articles.dart @@ -183,7 +183,7 @@ class ArticlesScreen extends ConsumerWidget { ), ), ); - }).toList(), + }), ], ), ), diff --git a/lib/screens/discovery/articles.g.dart b/lib/screens/discovery/articles.g.dart index 92f6d1ad..ae473949 100644 --- a/lib/screens/discovery/articles.g.dart +++ b/lib/screens/discovery/articles.g.dart @@ -6,7 +6,7 @@ part of 'articles.dart'; // RiverpodGenerator // ************************************************************************** -String _$subscribedFeedsHash() => r'cd2f5d7d4ea49ad00dc731f8fc2ed65450a3f0e4'; +String _$subscribedFeedsHash() => r'5c0c8c30c5f543f6ea1d39786a6778f77ba5b3df'; /// See also [subscribedFeeds]. @ProviderFor(subscribedFeeds) diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index a77939a1..3e3e17ff 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:island/screens/chat/chat.dart'; import 'package:flutter/material.dart'; import 'package:island/models/chat.dart';