From 983473261ba66d25a0c29f524c91de315286c90b Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:04:44 +0000 Subject: [PATCH] refactor(mobile): riverpod codegen + riverpod lint (#4836) * build(mobile): add riverpod_lint * refactor(mobile): riverpod_generator for providers * test(mobile): fix integration test helper * refactor: ApiService to riverpod codegen * refactor(mobile): return curatedcontent instead of people dto * refactor: person provider to asyncnotifier * mobile: update service providers to use lambda * mobile: update scaffoldbody default error icon * remove logger mixin --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .gitattributes | 2 + .github/workflows/static_analysis.yml | 5 + mobile/analysis_options.yaml | 3 + mobile/assets/i18n/en-US.json | 3 +- .../test_utils/general_helper.dart | 9 +- .../lib/extensions/asyncvalue_extensions.dart | 27 ++++ mobile/lib/main.dart | 14 ++- .../search/providers/people.provider.dart | 67 +++++----- .../search/providers/people.provider.g.dart | Bin 0 -> 8998 bytes .../search/services/person.service.dart | 44 +++---- .../search/services/person.service.g.dart | Bin 0 -> 907 bytes .../search/ui/person_name_edit_form.dart | 31 +++-- .../modules/search/views/all_people_page.dart | 10 +- .../search/views/person_result_page.dart | 119 ++++++++---------- .../lib/modules/search/views/search_page.dart | 10 +- .../providers/app_settings.provider.dart | 8 +- .../providers/app_settings.provider.g.dart | Bin 0 -> 946 bytes mobile/lib/shared/providers/api.provider.dart | 7 +- .../lib/shared/providers/api.provider.g.dart | Bin 0 -> 847 bytes mobile/lib/shared/ui/scaffold_error_body.dart | 33 +++++ mobile/pubspec.lock | 112 +++++++++++++++-- mobile/pubspec.yaml | 8 +- 22 files changed, 337 insertions(+), 175 deletions(-) create mode 100644 mobile/lib/extensions/asyncvalue_extensions.dart create mode 100644 mobile/lib/modules/search/providers/people.provider.g.dart create mode 100644 mobile/lib/modules/search/services/person.service.g.dart create mode 100644 mobile/lib/modules/settings/providers/app_settings.provider.g.dart create mode 100644 mobile/lib/shared/providers/api.provider.g.dart create mode 100644 mobile/lib/shared/ui/scaffold_error_body.dart diff --git a/.gitattributes b/.gitattributes index 32ea167bbe..48c4dbdb02 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 18343c06fc..661287252e 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 2d5071a4f1..d026b42fe2 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 8233fcd7f2..e46b51644b 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 ac0b14ef4d..e3b28e1f3d 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 0000000000..2c4725de8b --- /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 224fe3ef44..6d4a812b61 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 e40ff3fc83..6009ee53a1 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 0000000000000000000000000000000000000000..c13c2c160c1f5b3e517f26dd4f2d73098dc61748 GIT binary patch literal 8998 zcmeHNTT|Oc6n^)wI1ia%x77H84R&l2iXkv9Oeg=n=j>|b z)mk!8=uA4TUxKxJ?$^)C))u@!JUzVFzdD@2!P(>xzJbXZoSt354`-8;qmS_IM>yU; zoz!XzS0oV4U}M3eMZlU15#4g1iKdSS8*gg0tu6TK#XiLCg5NT+i2U~~WWr4%@xmf% zmx8{V(eKzo0$^~0+$r!jn9Kp{v*I$~ocjuYcjv`UF&l|NF58VLl=b8q_HNek)nnV*G zFQS+orAaE7J|BJ0<7AW>au9_H3zNOQCdWSn@~tDa&3Y4`h3=dU(NOzLv)!OeKAWc3 z@6kKr6f|cdnjeO@Ttwjq$}E_Oqi{ctSrVJ+()RmoHtTtA7t^ELn)Y_RZQu0=y>`Dn=uSKB-9dk_ zyY1_AIfxdV`9Q9N36!WhmpW|V>x5oF*6(#g%2rUv2+a4!Wde2?V0+(YpBbI zUxVWIM&5fivwFRC*J`zPND9gz5e*oPpxtVqag^$UVSb151b)qw*xQ4?JpOFikHy2W zY_<6>X0ueYo4X=cv#YgyiX)^S4O80fqN$}`&m7evB?^w}DHT4~&=*^%#1p>J z^Q=)*^=m%Ug^x*aZe;n9JLkc@%)7O+RPwG;0=vVG3V^KVtcu8r;sI)tF1OXgra~POAe#ycY5Ivz4M*ovmXBo&6fJ;BhJ=*IGEv_} zILrhNOngvAPpw%ibCz9IJiX}F72L(DE~UfXD(7&Co=clhFFmfS6wEq_N{16{4DK{w zqg*ASqPF)I9N*};m?PB6WiK*KT*m+{#d)&q+B9biOwG>r2z@HY!QWMG4Z}T|BG`*7 z@2t2&<0NJ-O{@ ziIrPVK`Y&lp0)#N8T4;qA2JVUH!`(6rnzniH_K>P@SQ5xW}&W3%F6s58th4xbHkaM zX(h2KLFs+s>v*VEs;tAlh+<6EI!<=}#3}M=I-cA+Posp-I1`)MJ|o!gDHpgtsaq6R zrGa%|x~cTS0|u?ID_@37SyM_{2eMAId+6+Q1BRImdG#n4DMVn6q?Hz<2L|XN^Ak^Q zRCy_ga3;(d3rA__B|Hl2W(2U>@+r{(<#1BY9)8&%(K2-vM}lATP&EN%FXZYJQ?n}6 zlp1-XQ1a<(NmnpxURC_k$;3p}H_auj2mzSoz*!;*y8Q?|p{}h%K}BIYwyUY1XW$OnEc?!-UV- z72>_7CYI*GIzqa+nQLSc7W}w`V9g9)5Mky$9M*Gw$HPJ+qcV!Z_29B>5MSM}B`AYW z+HareHy4UnUYku+Flz|pFMUz&4jFJfH+r@XNfv!+o!-QslyHAwPg${)vTkj%sm{E} zng%)!3Cu;Y#1dMN>Z+aCwXcn7~`Gk&a5--&vInG1XDIw8&KKj8dTZOwf4A|ey|i(leGj~ z**s9T3UWDzd<#?zO^`*!T?(&CMCg^KTu3R*D&=C+=N?!>6Kl4qj-5SJJbj8}$t0>` zt|yYN3_S!aZdK^4itS?%S|zP6fD$v7uBb$5#=#mR{zq`ZJ>pYpik15u_7>RzUjV_W zNGyAe&h0W3XWy-?V$CG4ONRa z5J#u3Rdvs*0YY0-8giyZ#Z~u%ZUlmN<O0G_8Z?;bKgm0d8mI_b>?h7rJs 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 0000000000000000000000000000000000000000..e66c6c2aa654ee61efffd2d4574ccaa8ef3dce35 GIT binary patch literal 907 zcmb_aL2lbH5WMRZdnh2gNafUyUBs!=x{>1aP&=@F35r6X$fZkwB84R-AipnV*#|W~ zhBwX(XSuUG8o?@CXWQjIOW|RYX7CBp4XihN__axYKEA+@7kFB((p3Z=Si6Z5k zPb8lRE^lVn6aJ-`PWiP!hO?=h+{zgre;y;Uo7-{G^*@3gA~5YN{O(!)Nuux1WE&-r z9xV-e%TFbzHMtcC?FYj0EKD(mo!etkZU;Pm19L>Z41JO&Z;ZLR`fSc&t{%SHdV zRR}EV!wUTaA@jJiHF0cHm;<-Qc_^&ai6mQQrcq=~iN+7&cJ4`2CKXvxa~}_bQ+vLF zskJ^`=9kj#YF20j8m`dIk1tx3X}u?AobqAzDZZ5q!=m}u3I$5&g|H34#jFDHUtD+X z9;t(|gt@eoOQl0Vt?NREBrmeAE?PluW$ut^6gk(-xhqhnVOoj2c1Q9&iM+(JKzhSB R%Bezkbv-&C_U|N({sGI39wPt% literal 0 HcmV?d00001 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 6e50131f9c..e32d4a9e0a 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 3cbedc949f..892006293a 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 2e8637bc7e..60d1999420 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 562e42c326..cead59e3cd 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 f5d172e4c4..96991451f5 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 0000000000000000000000000000000000000000..692dcf7c055cb8adb9275c86115f3d664000fd7a GIT binary patch literal 946 zcmb_bO^?$s5WV|X%mFDaT9THf4eio}vdMNi(4wZ76+)4djEg15j>b+T{yk1V#39RV zeTwHjzwx|0M-gn&ZMt6{(gYrMNeb^E*}-;qfbYBH+v78QeTJv?HesycHmH%fGxW+WSmwx&0{I(1Saz8!L+WIS zPu%_g-Bo>SK_M^b^LlGBvHX?!y5xBAj zeh#&Nh3w0Zi+hiTMnlG!38}e)A#yzjE4Y4sS`Ti2A4=_zyu`@gf2eZftD4ui^pg$I zt8mcMDlFRL2HgW8cbJ)$&gLwkG}<~Sj8UN=Q>VI9ZVp@5+=dtd$MoZr1< z-d&$FdDMn57lv{nm3OKaT`BMDi>Pgjo{$^iUkF!N<)z}*-hk8{SF*}mdjhZjC9knA UkOp`stt`~JuS45EwF{N~1tS70H~;_u literal 0 HcmV?d00001 diff --git a/mobile/lib/shared/providers/api.provider.dart b/mobile/lib/shared/providers/api.provider.dart index 24cf864e09..cc73f02b3f 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 0000000000000000000000000000000000000000..4bc7e93d1c1c8f1387cee3e07aeac2561a75e84d GIT binary patch literal 847 zcmb_aO>f&U487}D@G!v1(AiBBH$l2|OKUgXVabr>QVfG3TXZHsmJCWZ(0^aKY32gi zu{?=Z;?B?qmtZ?bQXe4uAW=LjrJT# zLxUa}vQlcUppTq*uz=I^>+)Y`x1rPy$$uFzc=zkVdzx1`_Z^?-3OZO|-W*ow?g_cW zooVPe=-gwS6-HYJxiKm%$yBLsB^h0zc7veXZrE1cx2Uj*yu$v&a>StWezA3y`|yN{Xzpui#1PtimcsI>TFOWv-5O9a`tF=#9!= E078od@c;k- literal 0 HcmV?d00001 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 0000000000..5c29f7c2a9 --- /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 a573f6bf71..7cb4188d90 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 d5c2a8571b..88b5cd1cf3 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