diff --git a/.gitattributes b/.gitattributes index 32ea167bb..48c4dbdb0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,8 @@ mobile/openapi/**/*.dart linguist-generated=true mobile/openapi/.openapi-generator/FILES -diff -merge mobile/openapi/.openapi-generator/FILES linguist-generated=true +mobile/lib/**/*.g.dart -diff -merge +mobile/lib/**/*.g.dart linguist-generated=true cli/src/api/open-api/**/*.md -diff -merge cli/src/api/open-api/**/*.md linguist-generated=true diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 18343c06f..661287252 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -32,3 +32,8 @@ jobs: - name: Run dart analyze run: dart analyze --fatal-infos working-directory: ./mobile + + # Enable after riverpod generator migration is completed + # - name: Run dart custom lint + # run: dart run custom_lint + # working-directory: ./mobile diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 2d5071a4f..d026b42fe 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -36,6 +36,9 @@ analyzer: - openapi/ - openapi/test/ - lib/generated_plugin_registrant.dart + +plugins: + - custom_lint dart_code_metrics: metrics: diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 8233fcd7f..e46b51644 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -438,5 +438,6 @@ "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "scaffold_body_error_occured": "Error occured" } diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index ac0b14ef4..e3b28e1f3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:integration_test/integration_test.dart'; import 'package:isar/isar.dart'; // ignore: depend_on_referenced_packages @@ -40,7 +42,12 @@ class ImmichTestHelper { await Store.clear(); await db.writeTxn(() => db.clear()); // Load main Widget - await tester.pumpWidget(app.getMainWidget(db)); + await tester.pumpWidget( + ProviderScope( + overrides: [dbProvider.overrideWithValue(db)], + child: app.getMainWidget(), + ), + ); // Post run tasks await EasyLocalization.ensureInitialized(); } diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart new file mode 100644 index 000000000..2c4725de8 --- /dev/null +++ b/mobile/lib/extensions/asyncvalue_extensions.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; +import 'package:logging/logging.dart'; + +extension ScaffoldBody on AsyncValue { + static final Logger _scaffoldBodyLog = Logger("ScaffoldBody"); + + Widget scaffoldBodyWhen({ + required Widget Function(T data) onData, + Widget? onError, + }) { + if (isLoading) { + return const Center( + child: ImmichLoadingIndicator(), + ); + } + + if (hasError && !hasValue) { + _scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace); + return onError ?? const ScaffoldErrorBody(); + } + + return onData(requireValue); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 224fe3ef4..6d4a812b6 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -43,7 +43,12 @@ void main() async { await initApp(); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); - runApp(getMainWidget(db)); + runApp( + ProviderScope( + overrides: [dbProvider.overrideWithValue(db)], + child: getMainWidget(), + ), + ); } Future initApp() async { @@ -103,16 +108,13 @@ Future loadDb() async { return db; } -Widget getMainWidget(Isar db) { +Widget getMainWidget() { return EasyLocalization( supportedLocales: locales, path: translationsPath, useFallbackTranslations: true, fallbackLocale: locales.first, - child: ProviderScope( - overrides: [dbProvider.overrideWithValue(db)], - child: const ImmichApp(), - ), + child: const ImmichApp(), ); } diff --git a/mobile/lib/modules/search/providers/people.provider.dart b/mobile/lib/modules/search/providers/people.provider.dart index e40ff3fc8..6009ee53a 100644 --- a/mobile/lib/modules/search/providers/people.provider.dart +++ b/mobile/lib/modules/search/providers/people.provider.dart @@ -1,44 +1,51 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/services/person.service.dart'; -import 'package:openapi/api.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final personAssetsProvider = FutureProvider.family - .autoDispose((ref, personId) async { - final PersonService personService = ref.watch(personServiceProvider); +part 'people.provider.g.dart'; +@riverpod +Future> getCuratedPeople( + GetCuratedPeopleRef ref, +) async { + final PersonService personService = ref.read(personServiceProvider); + + final curatedPeople = await personService.getCuratedPeople(); + + return curatedPeople + .map((p) => CuratedContent(id: p.id, label: p.name)) + .toList(); +} + +@riverpod +Future personAssets(PersonAssetsRef ref, String personId) async { + final PersonService personService = ref.read(personServiceProvider); final assets = await personService.getPersonAssets(personId); - if (assets == null) { return RenderList.empty(); } - return RenderList.fromAssets(assets, GroupAssetsBy.auto); -}); - -final getCuratedPeopleProvider = - FutureProvider.autoDispose>((ref) async { - final PersonService personService = ref.watch(personServiceProvider); - - final curatedPeople = await personService.getCuratedPeople(); - - return curatedPeople ?? []; -}); - -class UpdatePersonName { - final String id; - final String name; - - UpdatePersonName(this.id, this.name); + final settings = ref.read(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return await RenderList.fromAssets(assets, groupBy); } -final updatePersonNameProvider = - StateProvider.family((ref, dto) async { - final PersonService personService = ref.watch(personServiceProvider); +@riverpod +Future updatePersonName( + UpdatePersonNameRef ref, + String personId, + String updatedName, +) async { + final PersonService personService = ref.read(personServiceProvider); + final person = await personService.updateName(personId, updatedName); - final person = await personService.updateName(dto.id, dto.name); - - if (person != null && person.name == dto.name) { + if (person != null && person.name == updatedName) { ref.invalidate(getCuratedPeopleProvider); + return true; } -}); + return false; +} diff --git a/mobile/lib/modules/search/providers/people.provider.g.dart b/mobile/lib/modules/search/providers/people.provider.g.dart new file mode 100644 index 000000000..c13c2c160 --- /dev/null +++ b/mobile/lib/modules/search/providers/people.provider.g.dart @@ -0,0 +1,320 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'people.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0'; + +/// See also [getCuratedPeople]. +@ProviderFor(getCuratedPeople) +final getCuratedPeopleProvider = + AutoDisposeFutureProvider>.internal( + getCuratedPeople, + name: r'getCuratedPeopleProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$getCuratedPeopleHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef GetCuratedPeopleRef + = AutoDisposeFutureProviderRef>; +String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; + +/// 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 [personAssets]. +@ProviderFor(personAssets) +const personAssetsProvider = PersonAssetsFamily(); + +/// See also [personAssets]. +class PersonAssetsFamily extends Family> { + /// See also [personAssets]. + const PersonAssetsFamily(); + + /// See also [personAssets]. + PersonAssetsProvider call( + String personId, + ) { + return PersonAssetsProvider( + personId, + ); + } + + @override + PersonAssetsProvider getProviderOverride( + covariant PersonAssetsProvider provider, + ) { + return call( + provider.personId, + ); + } + + 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'personAssetsProvider'; +} + +/// See also [personAssets]. +class PersonAssetsProvider extends AutoDisposeFutureProvider { + /// See also [personAssets]. + PersonAssetsProvider( + String personId, + ) : this._internal( + (ref) => personAssets( + ref as PersonAssetsRef, + personId, + ), + from: personAssetsProvider, + name: r'personAssetsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$personAssetsHash, + dependencies: PersonAssetsFamily._dependencies, + allTransitiveDependencies: + PersonAssetsFamily._allTransitiveDependencies, + personId: personId, + ); + + PersonAssetsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.personId, + }) : super.internal(); + + final String personId; + + @override + Override overrideWith( + FutureOr Function(PersonAssetsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: PersonAssetsProvider._internal( + (ref) => create(ref as PersonAssetsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + personId: personId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _PersonAssetsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PersonAssetsProvider && other.personId == personId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, personId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin PersonAssetsRef on AutoDisposeFutureProviderRef { + /// The parameter `personId` of this provider. + String get personId; +} + +class _PersonAssetsProviderElement + extends AutoDisposeFutureProviderElement with PersonAssetsRef { + _PersonAssetsProviderElement(super.provider); + + @override + String get personId => (origin as PersonAssetsProvider).personId; +} + +String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd'; + +/// See also [updatePersonName]. +@ProviderFor(updatePersonName) +const updatePersonNameProvider = UpdatePersonNameFamily(); + +/// See also [updatePersonName]. +class UpdatePersonNameFamily extends Family> { + /// See also [updatePersonName]. + const UpdatePersonNameFamily(); + + /// See also [updatePersonName]. + UpdatePersonNameProvider call( + String personId, + String updatedName, + ) { + return UpdatePersonNameProvider( + personId, + updatedName, + ); + } + + @override + UpdatePersonNameProvider getProviderOverride( + covariant UpdatePersonNameProvider provider, + ) { + return call( + provider.personId, + provider.updatedName, + ); + } + + 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'updatePersonNameProvider'; +} + +/// See also [updatePersonName]. +class UpdatePersonNameProvider extends AutoDisposeFutureProvider { + /// See also [updatePersonName]. + UpdatePersonNameProvider( + String personId, + String updatedName, + ) : this._internal( + (ref) => updatePersonName( + ref as UpdatePersonNameRef, + personId, + updatedName, + ), + from: updatePersonNameProvider, + name: r'updatePersonNameProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$updatePersonNameHash, + dependencies: UpdatePersonNameFamily._dependencies, + allTransitiveDependencies: + UpdatePersonNameFamily._allTransitiveDependencies, + personId: personId, + updatedName: updatedName, + ); + + UpdatePersonNameProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.personId, + required this.updatedName, + }) : super.internal(); + + final String personId; + final String updatedName; + + @override + Override overrideWith( + FutureOr Function(UpdatePersonNameRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: UpdatePersonNameProvider._internal( + (ref) => create(ref as UpdatePersonNameRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + personId: personId, + updatedName: updatedName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _UpdatePersonNameProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is UpdatePersonNameProvider && + other.personId == personId && + other.updatedName == updatedName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, personId.hashCode); + hash = _SystemHash.combine(hash, updatedName.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef { + /// The parameter `personId` of this provider. + String get personId; + + /// The parameter `updatedName` of this provider. + String get updatedName; +} + +class _UpdatePersonNameProviderElement + extends AutoDisposeFutureProviderElement with UpdatePersonNameRef { + _UpdatePersonNameProviderElement(super.provider); + + @override + String get personId => (origin as UpdatePersonNameProvider).personId; + @override + String get updatedName => (origin as UpdatePersonNameProvider).updatedName; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart index 8314ed109..d4cbe0de5 100644 --- a/mobile/lib/modules/search/services/person.service.dart +++ b/mobile/lib/modules/search/services/person.service.dart @@ -1,44 +1,40 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final personServiceProvider = Provider( - (ref) => PersonService( - ref.watch(apiServiceProvider), - ), -); +part 'person.service.g.dart'; + +@riverpod +PersonService personService(PersonServiceRef ref) => + PersonService(ref.read(apiServiceProvider)); class PersonService { + final Logger _log = Logger("PersonService"); final ApiService _apiService; PersonService(this._apiService); - Future?> getCuratedPeople() async { + Future> getCuratedPeople() async { try { final peopleResponseDto = await _apiService.personApi.getAllPeople(); - return peopleResponseDto?.people; - } catch (e) { - debugPrint("Error [getCuratedPeople] ${e.toString()}"); - return null; + return peopleResponseDto?.people ?? []; + } catch (error, stack) { + _log.severe("Error while fetching curated people", error, stack); + return []; } } Future?> getPersonAssets(String id) async { try { final assets = await _apiService.personApi.getPersonAssets(id); - - if (assets == null) { - return null; - } - - return assets.map((e) => Asset.remote(e)).toList(); - } catch (e) { - debugPrint("Error [getPersonAssets] ${e.toString()}"); - return null; + return assets?.map((e) => Asset.remote(e)).toList(); + } catch (error, stack) { + _log.severe("Error while fetching person assets", error, stack); } + return null; } Future updateName(String id, String name) async { @@ -49,9 +45,9 @@ class PersonService { name: name, ), ); - } catch (e) { - debugPrint("Error [updateName] ${e.toString()}"); - return null; + } catch (error, stack) { + _log.severe("Error while updating person name", error, stack); } + return null; } } diff --git a/mobile/lib/modules/search/services/person.service.g.dart b/mobile/lib/modules/search/services/person.service.g.dart new file mode 100644 index 000000000..e66c6c2aa --- /dev/null +++ b/mobile/lib/modules/search/services/person.service.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'person.service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b'; + +/// See also [personService]. +@ProviderFor(personService) +final personServiceProvider = AutoDisposeProvider.internal( + personService, + name: r'personServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$personServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PersonServiceRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/ui/person_name_edit_form.dart b/mobile/lib/modules/search/ui/person_name_edit_form.dart index 6e50131f9..e32d4a9e0 100644 --- a/mobile/lib/modules/search/ui/person_name_edit_form.dart +++ b/mobile/lib/modules/search/ui/person_name_edit_form.dart @@ -25,6 +25,7 @@ class PersonNameEditForm extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final controller = useTextEditingController(text: personName); + final isError = useState(false); return AlertDialog( title: const Text( @@ -37,18 +38,16 @@ class PersonNameEditForm extends HookConsumerWidget { autofocus: true, decoration: InputDecoration( hintText: 'search_page_person_add_name_dialog_hint'.tr(), + border: const OutlineInputBorder(), + errorText: isError.value ? 'Error occured' : null, ), ), ), actions: [ TextButton( - style: TextButton.styleFrom(), - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop( - PersonNameEditFormResult(false, ''), - ); - }, + onPressed: () => context.pop( + PersonNameEditFormResult(false, ''), + ), child: Text( "search_page_person_add_name_dialog_cancel", style: TextStyle( @@ -58,17 +57,15 @@ class PersonNameEditForm extends HookConsumerWidget { ).tr(), ), TextButton( - onPressed: () { - ref.read( - updatePersonNameProvider( - UpdatePersonName(personId, controller.text), - ), - ); - - Navigator.of(context, rootNavigator: true) - .pop( - PersonNameEditFormResult(true, controller.text), + onPressed: () async { + isError.value = false; + final result = await ref.read( + updatePersonNameProvider(personId, controller.text).future, ); + isError.value = !result; + if (result) { + context.pop(PersonNameEditFormResult(true, controller.text)); + } }, child: Text( "search_page_person_add_name_dialog_save", diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart index 3cbedc949..892006293 100644 --- a/mobile/lib/modules/search/views/all_people_page.dart +++ b/mobile/lib/modules/search/views/all_people_page.dart @@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; @@ -36,14 +35,7 @@ class AllPeoplePage extends HookConsumerWidget { ), data: (people) => ExploreGrid( isPeople: true, - curatedContent: people - .map( - (person) => CuratedContent( - label: person.name, - id: person.id, - ), - ) - .toList(), + curatedContent: people, ), ), ); diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart index 2e8637bc7..60d199942 100644 --- a/mobile/lib/modules/search/views/person_result_page.dart +++ b/mobile/lib/modules/search/views/person_result_page.dart @@ -2,11 +2,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; import 'package:immich_mobile/shared/models/store.dart' as isar_store; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; class PersonResultPage extends HookConsumerWidget { @@ -24,12 +26,12 @@ class PersonResultPage extends HookConsumerWidget { final name = useState(personName); showEditNameDialog() { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return PersonNameEditForm( personId: personId, - personName: personName, + personName: name.value, ); }, ).then((result) { @@ -66,35 +68,33 @@ class PersonResultPage extends HookConsumerWidget { } buildTitleBlock() { - if (name.value == "") { - return GestureDetector( - onTap: showEditNameDialog, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'search_page_person_add_name_title', - style: context.textTheme.titleSmall?.copyWith( - color: context.themeData.colorScheme.secondary, - ), - ).tr(), - Text( - 'search_page_person_add_name_subtitle', - style: context.textTheme.labelSmall, - ).tr(), - ], - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name.value, - style: context.textTheme.titleLarge, - ), - ], + return GestureDetector( + onTap: showEditNameDialog, + child: name.value.isEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'search_page_person_add_name_title', + style: context.textTheme.titleSmall?.copyWith( + color: context.themeData.colorScheme.secondary, + ), + ).tr(), + Text( + 'search_page_person_add_name_subtitle', + style: context.textTheme.labelSmall, + ).tr(), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name.value, + style: context.textTheme.titleLarge, + ), + ], + ), ); } @@ -112,41 +112,32 @@ class PersonResultPage extends HookConsumerWidget { ), ], ), - body: ref.watch(personAssetsProvider(personId)).when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center( - child: Text( - error.toString(), - ), - ), - data: (data) => data.isEmpty - ? const Center( - child: Text('Opps'), - ) - : ImmichAssetGrid( - renderList: data, - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar( - radius: 36, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(personId), - headers: { - "Authorization": - "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", - }, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: buildTitleBlock(), - ), - ], + body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen( + onData: (renderList) => ImmichAssetGrid( + renderList: renderList, + topWidget: Padding( + padding: const EdgeInsets.only(left: 8.0, top: 24), + child: Row( + children: [ + CircleAvatar( + radius: 36, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(personId), + headers: { + "Authorization": + "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", + }, ), ), - ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: buildTitleBlock(), + ), + ], + ), + ), + ), + onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined), ), ); } diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index 562e42c32..cead59e3c 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -77,15 +77,7 @@ class SearchPage extends HookConsumerWidget { loading: () => const Center(child: ImmichLoadingIndicator()), error: (err, stack) => Center(child: Text('Error: $err')), data: (people) => CuratedPeopleRow( - content: people - .map( - (person) => CuratedContent( - id: person.id, - label: person.name, - ), - ) - .take(12) - .toList(), + content: people.take(12).toList(), onTap: (content, index) { context.autoPush( PersonResultRoute( diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.dart b/mobile/lib/modules/settings/providers/app_settings.provider.dart index f5d172e4c..96991451f 100644 --- a/mobile/lib/modules/settings/providers/app_settings.provider.dart +++ b/mobile/lib/modules/settings/providers/app_settings.provider.dart @@ -1,4 +1,8 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final appSettingsServiceProvider = Provider((ref) => AppSettingsService()); +part 'app_settings.provider.g.dart'; + +@Riverpod(keepAlive: true) +AppSettingsService appSettingsService(AppSettingsServiceRef ref) => + AppSettingsService(); diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.g.dart b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart new file mode 100644 index 000000000..692dcf7c0 --- /dev/null +++ b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_settings.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$appSettingsServiceHash() => + r'957a65af6967701112f3076b507f9738fec4b7be'; + +/// See also [appSettingsService]. +@ProviderFor(appSettingsService) +final appSettingsServiceProvider = Provider.internal( + appSettingsService, + name: r'appSettingsServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$appSettingsServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AppSettingsServiceRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/shared/providers/api.provider.dart b/mobile/lib/shared/providers/api.provider.dart index 24cf864e0..cc73f02b3 100644 --- a/mobile/lib/shared/providers/api.provider.dart +++ b/mobile/lib/shared/providers/api.provider.dart @@ -1,4 +1,7 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final apiServiceProvider = Provider((ref) => ApiService()); +part 'api.provider.g.dart'; + +@Riverpod(keepAlive: true) +ApiService apiService(ApiServiceRef ref) => ApiService(); diff --git a/mobile/lib/shared/providers/api.provider.g.dart b/mobile/lib/shared/providers/api.provider.g.dart new file mode 100644 index 000000000..4bc7e93d1 --- /dev/null +++ b/mobile/lib/shared/providers/api.provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d'; + +/// See also [apiService]. +@ProviderFor(apiService) +final apiServiceProvider = Provider.internal( + apiService, + name: r'apiServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$apiServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef ApiServiceRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/shared/ui/scaffold_error_body.dart b/mobile/lib/shared/ui/scaffold_error_body.dart new file mode 100644 index 000000000..5c29f7c2a --- /dev/null +++ b/mobile/lib/shared/ui/scaffold_error_body.dart @@ -0,0 +1,33 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +// Error widget to be used in Scaffold when an AsyncError is received +class ScaffoldErrorBody extends StatelessWidget { + final IconData icon; + + const ScaffoldErrorBody({this.icon = Icons.error_outline, super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "scaffold_body_error_occured", + style: + TextStyle(fontSize: 14, fontWeight: FontWeight.bold, height: 3), + textAlign: TextAlign.center, + ).tr(), + Center( + child: Icon( + icon, + size: 100, + color: context.themeData.iconTheme.color?.withOpacity(0.5), + ), + ), + ], + ); + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index a573f6bf7..7cb4188d9 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" archive: dependency: transitive description: @@ -201,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.7.0" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -281,6 +305,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 + url: "https://pub.dev" + source: hosted + version: "0.5.6" dart_style: dependency: transitive description: @@ -455,10 +503,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec" url: "https://pub.dev" source: hosted - version: "0.18.6" + version: "0.20.3" flutter_launcher_icons: dependency: "direct dev" description: @@ -540,10 +588,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 + sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.4.5" flutter_test: dependency: "direct dev" description: flutter @@ -578,6 +626,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -659,10 +715,18 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" + sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.4.5" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + url: "https://pub.dev" + source: hosted + version: "4.1.0" html: dependency: transitive description: @@ -1175,10 +1239,42 @@ packages: dependency: transitive description: name: riverpod - sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 + sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.4.5" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6 + url: "https://pub.dev" + source: hosted + version: "0.3.4" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "9330309e4400f40e39a2a1d1c340e775d0fd23451cf2dd2286e03c7896fd2bd5" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "5b36ad2f2b562cffb37212e8d59390b25499bf045b732276e30a207b16a25f61" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "70198738c3047ae4f6517ef1a2011a8514a980a52576c7f629a3a08810319a02" + url: "https://pub.dev" + source: hosted + version: "2.1.1" rxdart: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index d5c2a8571..88b5cd1cf 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -14,8 +14,9 @@ dependencies: path_provider_ios: photo_manager: ^2.7.2 - flutter_hooks: ^0.18.6 - hooks_riverpod: ^2.2.0 + flutter_hooks: ^0.20.3 + hooks_riverpod: ^2.4.0 + riverpod_annotation: ^2.3.0 cached_network_image: ^3.2.2 flutter_cache_manager: ^3.3.0 intl: ^0.18.0 @@ -86,6 +87,9 @@ dev_dependencies: mockito: ^5.3.2 integration_test: sdk: flutter + custom_lint: ^0.5.6 + riverpod_lint: ^2.1.0 + riverpod_generator: ^2.3.3 flutter: uses-material-design: true