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>
This commit is contained in:
shenlong 2023-11-19 16:04:44 +00:00 committed by GitHub
parent bfab86b70d
commit 983473261b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 732 additions and 175 deletions

2
.gitattributes vendored
View file

@ -5,6 +5,8 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true 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 -diff -merge
cli/src/api/open-api/**/*.md linguist-generated=true cli/src/api/open-api/**/*.md linguist-generated=true

View file

@ -32,3 +32,8 @@ jobs:
- name: Run dart analyze - name: Run dart analyze
run: dart analyze --fatal-infos run: dart analyze --fatal-infos
working-directory: ./mobile working-directory: ./mobile
# Enable after riverpod generator migration is completed
# - name: Run dart custom lint
# run: dart run custom_lint
# working-directory: ./mobile

View file

@ -37,6 +37,9 @@ analyzer:
- openapi/test/ - openapi/test/
- lib/generated_plugin_registrant.dart - lib/generated_plugin_registrant.dart
plugins:
- custom_lint
dart_code_metrics: dart_code_metrics:
metrics: metrics:
cyclomatic-complexity: 20 cyclomatic-complexity: 20

View file

@ -438,5 +438,6 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack", "viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack" "viewer_unstack": "Un-Stack",
"scaffold_body_error_occured": "Error occured"
} }

View file

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.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/models/store.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
@ -40,7 +42,12 @@ class ImmichTestHelper {
await Store.clear(); await Store.clear();
await db.writeTxn(() => db.clear()); await db.writeTxn(() => db.clear());
// Load main Widget // Load main Widget
await tester.pumpWidget(app.getMainWidget(db)); await tester.pumpWidget(
ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: app.getMainWidget(),
),
);
// Post run tasks // Post run tasks
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
} }

View file

@ -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<T> on AsyncValue<T> {
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);
}
}

View file

@ -43,7 +43,12 @@ void main() async {
await initApp(); await initApp();
await migrateDatabaseIfNeeded(db); await migrateDatabaseIfNeeded(db);
HttpOverrides.global = HttpSSLCertOverride(); HttpOverrides.global = HttpSSLCertOverride();
runApp(getMainWidget(db)); runApp(
ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: getMainWidget(),
),
);
} }
Future<void> initApp() async { Future<void> initApp() async {
@ -103,16 +108,13 @@ Future<Isar> loadDb() async {
return db; return db;
} }
Widget getMainWidget(Isar db) { Widget getMainWidget() {
return EasyLocalization( return EasyLocalization(
supportedLocales: locales, supportedLocales: locales,
path: translationsPath, path: translationsPath,
useFallbackTranslations: true, useFallbackTranslations: true,
fallbackLocale: locales.first, fallbackLocale: locales.first,
child: ProviderScope( child: const ImmichApp(),
overrides: [dbProvider.overrideWithValue(db)],
child: const ImmichApp(),
),
); );
} }

View file

@ -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/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: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 part 'people.provider.g.dart';
.autoDispose<RenderList, String>((ref, personId) async {
final PersonService personService = ref.watch(personServiceProvider);
@riverpod
Future<List<CuratedContent>> 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<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
final PersonService personService = ref.read(personServiceProvider);
final assets = await personService.getPersonAssets(personId); final assets = await personService.getPersonAssets(personId);
if (assets == null) { if (assets == null) {
return RenderList.empty(); return RenderList.empty();
} }
return RenderList.fromAssets(assets, GroupAssetsBy.auto); final settings = ref.read(appSettingsServiceProvider);
}); final groupBy =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
final getCuratedPeopleProvider = return await RenderList.fromAssets(assets, groupBy);
FutureProvider.autoDispose<List<PersonResponseDto>>((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 updatePersonNameProvider = @riverpod
StateProvider.family<void, UpdatePersonName>((ref, dto) async { Future<bool> updatePersonName(
final PersonService personService = ref.watch(personServiceProvider); 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 == updatedName) {
if (person != null && person.name == dto.name) {
ref.invalidate(getCuratedPeopleProvider); ref.invalidate(getCuratedPeopleProvider);
return true;
} }
}); return false;
}

View file

@ -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<List<CuratedContent>>.internal(
getCuratedPeople,
name: r'getCuratedPeopleProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$getCuratedPeopleHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef GetCuratedPeopleRef
= AutoDisposeFutureProviderRef<List<CuratedContent>>;
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<AsyncValue<RenderList>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'personAssetsProvider';
}
/// See also [personAssets].
class PersonAssetsProvider extends AutoDisposeFutureProvider<RenderList> {
/// 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<RenderList> 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<RenderList> 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<RenderList> {
/// The parameter `personId` of this provider.
String get personId;
}
class _PersonAssetsProviderElement
extends AutoDisposeFutureProviderElement<RenderList> 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<AsyncValue<bool>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'updatePersonNameProvider';
}
/// See also [updatePersonName].
class UpdatePersonNameProvider extends AutoDisposeFutureProvider<bool> {
/// 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<bool> 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<bool> 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<bool> {
/// The parameter `personId` of this provider.
String get personId;
/// The parameter `updatedName` of this provider.
String get updatedName;
}
class _UpdatePersonNameProviderElement
extends AutoDisposeFutureProviderElement<bool> 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

View file

@ -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/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final personServiceProvider = Provider( part 'person.service.g.dart';
(ref) => PersonService(
ref.watch(apiServiceProvider), @riverpod
), PersonService personService(PersonServiceRef ref) =>
); PersonService(ref.read(apiServiceProvider));
class PersonService { class PersonService {
final Logger _log = Logger("PersonService");
final ApiService _apiService; final ApiService _apiService;
PersonService(this._apiService); PersonService(this._apiService);
Future<List<PersonResponseDto>?> getCuratedPeople() async { Future<List<PersonResponseDto>> getCuratedPeople() async {
try { try {
final peopleResponseDto = await _apiService.personApi.getAllPeople(); final peopleResponseDto = await _apiService.personApi.getAllPeople();
return peopleResponseDto?.people; return peopleResponseDto?.people ?? [];
} catch (e) { } catch (error, stack) {
debugPrint("Error [getCuratedPeople] ${e.toString()}"); _log.severe("Error while fetching curated people", error, stack);
return null; return [];
} }
} }
Future<List<Asset>?> getPersonAssets(String id) async { Future<List<Asset>?> getPersonAssets(String id) async {
try { try {
final assets = await _apiService.personApi.getPersonAssets(id); final assets = await _apiService.personApi.getPersonAssets(id);
return assets?.map((e) => Asset.remote(e)).toList();
if (assets == null) { } catch (error, stack) {
return null; _log.severe("Error while fetching person assets", error, stack);
}
return assets.map((e) => Asset.remote(e)).toList();
} catch (e) {
debugPrint("Error [getPersonAssets] ${e.toString()}");
return null;
} }
return null;
} }
Future<PersonResponseDto?> updateName(String id, String name) async { Future<PersonResponseDto?> updateName(String id, String name) async {
@ -49,9 +45,9 @@ class PersonService {
name: name, name: name,
), ),
); );
} catch (e) { } catch (error, stack) {
debugPrint("Error [updateName] ${e.toString()}"); _log.severe("Error while updating person name", error, stack);
return null;
} }
return null;
} }
} }

View file

@ -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<PersonService>.internal(
personService,
name: r'personServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$personServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef PersonServiceRef = AutoDisposeProviderRef<PersonService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -25,6 +25,7 @@ class PersonNameEditForm extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final controller = useTextEditingController(text: personName); final controller = useTextEditingController(text: personName);
final isError = useState(false);
return AlertDialog( return AlertDialog(
title: const Text( title: const Text(
@ -37,18 +38,16 @@ class PersonNameEditForm extends HookConsumerWidget {
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'search_page_person_add_name_dialog_hint'.tr(), hintText: 'search_page_person_add_name_dialog_hint'.tr(),
border: const OutlineInputBorder(),
errorText: isError.value ? 'Error occured' : null,
), ),
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom(), onPressed: () => context.pop(
onPressed: () { PersonNameEditFormResult(false, ''),
Navigator.of(context, rootNavigator: true) ),
.pop<PersonNameEditFormResult>(
PersonNameEditFormResult(false, ''),
);
},
child: Text( child: Text(
"search_page_person_add_name_dialog_cancel", "search_page_person_add_name_dialog_cancel",
style: TextStyle( style: TextStyle(
@ -58,17 +57,15 @@ class PersonNameEditForm extends HookConsumerWidget {
).tr(), ).tr(),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
ref.read( isError.value = false;
updatePersonNameProvider( final result = await ref.read(
UpdatePersonName(personId, controller.text), updatePersonNameProvider(personId, controller.text).future,
),
);
Navigator.of(context, rootNavigator: true)
.pop<PersonNameEditFormResult>(
PersonNameEditFormResult(true, controller.text),
); );
isError.value = !result;
if (result) {
context.pop(PersonNameEditFormResult(true, controller.text));
}
}, },
child: Text( child: Text(
"search_page_person_add_name_dialog_save", "search_page_person_add_name_dialog_save",

View file

@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@ -36,14 +35,7 @@ class AllPeoplePage extends HookConsumerWidget {
), ),
data: (people) => ExploreGrid( data: (people) => ExploreGrid(
isPeople: true, isPeople: true,
curatedContent: people curatedContent: people,
.map(
(person) => CuratedContent(
label: person.name,
id: person.id,
),
)
.toList(),
), ),
), ),
); );

View file

@ -2,11 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.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/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.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/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'; import 'package:immich_mobile/utils/image_url_builder.dart';
class PersonResultPage extends HookConsumerWidget { class PersonResultPage extends HookConsumerWidget {
@ -24,12 +26,12 @@ class PersonResultPage extends HookConsumerWidget {
final name = useState(personName); final name = useState(personName);
showEditNameDialog() { showEditNameDialog() {
showDialog<PersonNameEditFormResult>( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return PersonNameEditForm( return PersonNameEditForm(
personId: personId, personId: personId,
personName: personName, personName: name.value,
); );
}, },
).then((result) { ).then((result) {
@ -66,35 +68,33 @@ class PersonResultPage extends HookConsumerWidget {
} }
buildTitleBlock() { buildTitleBlock() {
if (name.value == "") { return GestureDetector(
return GestureDetector( onTap: showEditNameDialog,
onTap: showEditNameDialog, child: name.value.isEmpty
child: Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'search_page_person_add_name_title', 'search_page_person_add_name_title',
style: context.textTheme.titleSmall?.copyWith( style: context.textTheme.titleSmall?.copyWith(
color: context.themeData.colorScheme.secondary, color: context.themeData.colorScheme.secondary,
), ),
).tr(), ).tr(),
Text( Text(
'search_page_person_add_name_subtitle', 'search_page_person_add_name_subtitle',
style: context.textTheme.labelSmall, style: context.textTheme.labelSmall,
).tr(), ).tr(),
], ],
), )
); : Column(
} crossAxisAlignment: CrossAxisAlignment.start,
children: [
return Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, name.value,
children: [ style: context.textTheme.titleLarge,
Text( ),
name.value, ],
style: context.textTheme.titleLarge, ),
),
],
); );
} }
@ -112,41 +112,32 @@ class PersonResultPage extends HookConsumerWidget {
), ),
], ],
), ),
body: ref.watch(personAssetsProvider(personId)).when( body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen(
loading: () => const Center(child: CircularProgressIndicator()), onData: (renderList) => ImmichAssetGrid(
error: (error, stackTrace) => Center( renderList: renderList,
child: Text( topWidget: Padding(
error.toString(), padding: const EdgeInsets.only(left: 8.0, top: 24),
), child: Row(
), children: [
data: (data) => data.isEmpty CircleAvatar(
? const Center( radius: 36,
child: Text('Opps'), backgroundImage: NetworkImage(
) getFaceThumbnailUrl(personId),
: ImmichAssetGrid( headers: {
renderList: data, "Authorization":
topWidget: Padding( "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}",
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(),
),
],
), ),
), ),
), Padding(
padding: const EdgeInsets.only(left: 16.0),
child: buildTitleBlock(),
),
],
),
),
),
onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined),
), ),
); );
} }

View file

@ -77,15 +77,7 @@ class SearchPage extends HookConsumerWidget {
loading: () => const Center(child: ImmichLoadingIndicator()), loading: () => const Center(child: ImmichLoadingIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')), error: (err, stack) => Center(child: Text('Error: $err')),
data: (people) => CuratedPeopleRow( data: (people) => CuratedPeopleRow(
content: people content: people.take(12).toList(),
.map(
(person) => CuratedContent(
id: person.id,
label: person.name,
),
)
.take(12)
.toList(),
onTap: (content, index) { onTap: (content, index) {
context.autoPush( context.autoPush(
PersonResultRoute( PersonResultRoute(

View file

@ -1,4 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.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();

View file

@ -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<AppSettingsService>.internal(
appSettingsService,
name: r'appSettingsServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appSettingsServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AppSettingsServiceRef = ProviderRef<AppSettingsService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -1,4 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/services/api.service.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();

View file

@ -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<ApiService>.internal(
apiService,
name: r'apiServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$apiServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ApiServiceRef = ProviderRef<ApiService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -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),
),
),
],
);
}
}

View file

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" 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: archive:
dependency: transitive dependency: transitive
description: description:
@ -201,6 +209,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.7.0" 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: clock:
dependency: transitive dependency: transitive
description: description:
@ -281,6 +305,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" 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: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -455,10 +503,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_hooks name: flutter_hooks
sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.6" version: "0.20.3"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -540,10 +588,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_riverpod name: flutter_riverpod
sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.7" version: "2.4.5"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -578,6 +626,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.2" 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: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -659,10 +715,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: hooks_riverpod name: hooks_riverpod
sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: html:
dependency: transitive dependency: transitive
description: description:
@ -1175,10 +1239,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: riverpod name: riverpod
sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: rxdart:
dependency: transitive dependency: transitive
description: description:

View file

@ -14,8 +14,9 @@ dependencies:
path_provider_ios: path_provider_ios:
photo_manager: ^2.7.2 photo_manager: ^2.7.2
flutter_hooks: ^0.18.6 flutter_hooks: ^0.20.3
hooks_riverpod: ^2.2.0 hooks_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
cached_network_image: ^3.2.2 cached_network_image: ^3.2.2
flutter_cache_manager: ^3.3.0 flutter_cache_manager: ^3.3.0
intl: ^0.18.0 intl: ^0.18.0
@ -86,6 +87,9 @@ dev_dependencies:
mockito: ^5.3.2 mockito: ^5.3.2
integration_test: integration_test:
sdk: flutter sdk: flutter
custom_lint: ^0.5.6
riverpod_lint: ^2.1.0
riverpod_generator: ^2.3.3
flutter: flutter:
uses-material-design: true uses-material-design: true