chore(mobile): migrate OCR, Tag, and UserMetadata to Store (#31704)
* chore(mobile): migrate OCR, Tag, and UserMetadata to Store * Add comments to Tag HTTP repository * Add mounted check
This commit is contained in:
+1
-1
@@ -1,8 +1,8 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/data/db/main/dao/ocr.drift.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/data/db/main/table/asset/ocr.drift.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.drift.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class OcrRepository extends DatabaseAccessor<Drift> with $OcrRepositoryMixin {
|
||||
+2
-2
@@ -9,12 +9,12 @@ class UserMetadataRepository extends DatabaseAccessor<Drift> {
|
||||
|
||||
Drift get _db => attachedDatabase;
|
||||
|
||||
Future<List<UserMetadata>> getUserMetadata(String userId) {
|
||||
Stream<List<UserMetadata>> watchUserMetadata(String userId) {
|
||||
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(userId));
|
||||
|
||||
return query.map((userMetadata) {
|
||||
return userMetadata.toDto();
|
||||
}).get();
|
||||
}).watch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/runtime/executor/stream_queries.dart' show StreamQueryStore;
|
||||
import 'package:drift_sqlite_async/drift_sqlite_async.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/data/db/main/dao/ocr.dart';
|
||||
import 'package:immich_mobile/data/db/main/dao/person.dart';
|
||||
import 'package:immich_mobile/data/db/main/dao/user_metadata.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.drift.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.steps.dart';
|
||||
import 'package:immich_mobile/data/db/main/table/app/settings.dart';
|
||||
@@ -40,7 +42,6 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
@@ -51,7 +52,6 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/server/api_repository.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final tagApiRepositoryProvider = Provider((ref) => TagApiRepository(ref.watch(apiServiceProvider).tagsApi));
|
||||
|
||||
class TagApiRepository extends ApiRepository {
|
||||
final TagsApi _api;
|
||||
|
||||
const TagApiRepository(this._api);
|
||||
|
||||
/// Apply every tag in [tagIds] to every asset in [assetIds], returning the number of assets successfully tagged
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
final response = await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
|
||||
return response?.count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves all known tags
|
||||
Future<List<Tag>> getAll() async {
|
||||
final response = await checkNull(_api.getAllTags());
|
||||
return response.map(_toTag).toList();
|
||||
}
|
||||
|
||||
/// Create the tags named [values], returning the list of successfully created (or pre-existing) tags
|
||||
Future<List<Tag>> upsert(List<String> tags) async {
|
||||
final response = await checkNull(_api.upsertTags(TagUpsertDto(tags: tags)));
|
||||
return response.map(_toTag).toList();
|
||||
}
|
||||
|
||||
static Tag _toTag(TagResponseDto dto) => .new(id: dto.id, value: dto.value);
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/data_controller.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/data/store/activity.dart';
|
||||
import 'package:immich_mobile/data/store/ocr.dart';
|
||||
import 'package:immich_mobile/data/store/person.dart';
|
||||
import 'package:immich_mobile/data/store/tag.dart';
|
||||
import 'package:immich_mobile/data/store/user_metadata.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
|
||||
/// The [DataController] backing this container's store
|
||||
@@ -36,4 +39,10 @@ abstract final class Store {
|
||||
static final people = PersonStore.instance;
|
||||
|
||||
static final activity = ActivityStore.instance;
|
||||
|
||||
static final ocr = OcrStore.instance;
|
||||
|
||||
static final tags = TagStore.instance;
|
||||
|
||||
static final userMetadata = UserMetadataStore.instance;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store/util/cache.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
/// Text recognized within an asset
|
||||
///
|
||||
/// State is stored in the local DB, and is only written by sync
|
||||
extension type const OcrStore._(Provider<OcrMutations> _provider) implements Provider<OcrMutations> {
|
||||
/// Internal: access through `Store.ocr`
|
||||
static final OcrStore instance = OcrStore._(Provider((ref) => OcrMutations._(ref)));
|
||||
|
||||
/// The visible text recognized within the asset [assetId]
|
||||
///
|
||||
/// **NOTE:** This is not reactive to changes, and only hits the local DB
|
||||
AutoDisposeFutureProvider<List<Ocr>> forAsset(String assetId) => _forAssetProvider(assetId);
|
||||
}
|
||||
|
||||
final _ocrDb = driftProvider.select((db) => db.ocrRepository);
|
||||
|
||||
final _forAssetProvider = FutureProvider.autoDispose.family<List<Ocr>, String>(
|
||||
(ref, assetId) => ref.watch(_ocrDb).get(assetId),
|
||||
);
|
||||
|
||||
class OcrMutations extends StoreMutations {
|
||||
const OcrMutations._(super.ref);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/server/person.dart';
|
||||
import 'package:immich_mobile/data/store/user_metadata.dart';
|
||||
import 'package:immich_mobile/data/store/util/cache.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
// TODO(rewrite): Remove once user metadata is a store entry of its own
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
|
||||
/// People representing collections of faces and assets
|
||||
///
|
||||
@@ -41,7 +40,7 @@ final _forAssetProvider = StreamProvider.autoDispose.family<List<Person>, String
|
||||
);
|
||||
|
||||
final _allProvider = StreamProvider.autoDispose<List<Person>>((ref) async* {
|
||||
final prefs = await ref.watch(userMetadataPreferencesProvider.future);
|
||||
final prefs = await ref.watch(UserMetadataStore.instance.preferences().future);
|
||||
|
||||
yield* ref
|
||||
.watch(_peopleDb)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/server/tag.dart';
|
||||
import 'package:immich_mobile/data/store/util/cache.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final _log = Logger("TagStore");
|
||||
|
||||
/// User defined tags that can be applied to assets
|
||||
///
|
||||
/// All operations directly access the HTTP API only
|
||||
extension type const TagStore._(Provider<TagMutations> _provider) implements Provider<TagMutations> {
|
||||
static final TagStore instance = TagStore._(Provider((ref) => TagMutations._(ref)));
|
||||
|
||||
/// Every tag known to the server
|
||||
///
|
||||
/// **NOTE:** This is not reactive to changes, and only hits the HTTP API
|
||||
AutoDisposeFutureProvider<List<Tag>> all() => _allProvider;
|
||||
}
|
||||
|
||||
final _allProvider = FutureProvider.autoDispose<List<Tag>>((ref) async {
|
||||
try {
|
||||
return await ref.watch(tagApiRepositoryProvider).getAll();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Failed to get all tags", error, stack);
|
||||
return const [];
|
||||
}
|
||||
});
|
||||
|
||||
class TagMutations extends StoreMutations {
|
||||
const TagMutations._(super.ref);
|
||||
|
||||
/// Create the tags named [values], returning the list of successfully created (or pre-existing) tags
|
||||
Future<List<Tag>> upsert(List<String> values) async {
|
||||
try {
|
||||
return await read(tagApiRepositoryProvider).upsert(values);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Failed to upsert tags", error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply every tag in [tagIds] to every asset in [assetIds], returning the number of assets successfully tagged
|
||||
Future<int> applyToAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
try {
|
||||
return await read(tagApiRepositoryProvider).bulkTagAssets(assetIds, tagIds);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Failed to tag assets", error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store/util/cache.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
// TODO(rewrite): Remove once the current user is a store entry of its own
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
/// Server held metadata about the current user; their preferences, onboarding state and license
|
||||
///
|
||||
/// State is stored in the local DB, and is only written by server sync
|
||||
extension type const UserMetadataStore._(Provider<UserMetadataMutations> _provider)
|
||||
implements Provider<UserMetadataMutations> {
|
||||
static final UserMetadataStore instance = UserMetadataStore._(Provider((ref) => UserMetadataMutations._(ref)));
|
||||
|
||||
/// The current user's server side preferences, or `null` if they have not synced yet
|
||||
///
|
||||
/// **NOTE:** This is only reactive to the local DB
|
||||
StreamProvider<Preferences?> preferences() => _preferencesProvider;
|
||||
}
|
||||
|
||||
final _preferencesProvider = StreamProvider<Preferences?>((ref) {
|
||||
final userId = ref.watch(currentUserProvider.select((user) => user?.id));
|
||||
if (userId == null) {
|
||||
return Stream.value(null);
|
||||
}
|
||||
|
||||
final repository = driftProvider.select((db) => db.userMetadataRepository);
|
||||
return ref
|
||||
.watch(repository)
|
||||
.watchUserMetadata(userId)
|
||||
.map((metadataList) => metadataList.firstWhereOrNull((metadata) => metadata.preferences != null)?.preferences);
|
||||
});
|
||||
|
||||
class UserMetadataMutations extends StoreMutations {
|
||||
const UserMetadataMutations._(super.ref);
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
part 'tag.model.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class Tag with _$Tag {
|
||||
const factory Tag({required String id, required String value}) = _Tag;
|
||||
|
||||
static Tag fromDto(TagResponseDto dto) {
|
||||
return Tag(id: dto.id, value: dto.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/ocr.repository.dart';
|
||||
|
||||
class OcrService {
|
||||
final OcrRepository _repository;
|
||||
|
||||
const OcrService(this._repository);
|
||||
|
||||
Future<List<Ocr>?> get(String assetId) {
|
||||
return _repository.get(assetId);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart';
|
||||
|
||||
final tagServiceProvider = Provider<TagService>((ref) => TagService(ref.watch(tagsApiRepositoryProvider)));
|
||||
|
||||
class TagService {
|
||||
final TagsApiRepository _repository;
|
||||
|
||||
const TagService(this._repository);
|
||||
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
return _repository.bulkTagAssets(assetIds, tagIds);
|
||||
}
|
||||
|
||||
Future<Set<Tag>> getAllTags() async {
|
||||
final dtos = await _repository.getAllTags();
|
||||
if (dtos == null) {
|
||||
return {};
|
||||
}
|
||||
return dtos.map((dto) => Tag.fromDto(dto)).toSet();
|
||||
}
|
||||
|
||||
Future<List<Tag>> upsertTags(List<String> tags) async {
|
||||
final dtos = await _repository.upsertTags(tags);
|
||||
if (dtos == null) {
|
||||
return [];
|
||||
}
|
||||
return dtos.map((dto) => Tag.fromDto(dto)).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/server/api_repository.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final tagsApiRepositoryProvider = Provider<TagsApiRepository>(
|
||||
(ref) => TagsApiRepository(ref.read(apiServiceProvider).tagsApi),
|
||||
);
|
||||
|
||||
class TagsApiRepository extends ApiRepository {
|
||||
final TagsApi _api;
|
||||
const TagsApiRepository(this._api);
|
||||
|
||||
Future<List<TagResponseDto>?> getAllTags() async {
|
||||
return await _api.getAllTags();
|
||||
}
|
||||
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
final response = await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
|
||||
return response?.count ?? 0;
|
||||
}
|
||||
|
||||
Future<List<TagResponseDto>?> upsertTags(List<String> tags) async {
|
||||
return _api.upsertTags(TagUpsertDto(tags: tags));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/data/db/main/dao/user_metadata.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/data/db/main/table/user/auth_user.drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class UserRepository extends DatabaseAccessor<Drift> with $UserRepositoryMixin {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/utils/error_handler.dart';
|
||||
import 'package:immich_mobile/widgets/common/tag_picker.dart';
|
||||
|
||||
final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>((ref, source) {
|
||||
final tagsEnabled = ref.watch(
|
||||
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
|
||||
Store.userMetadata.preferences().select((value) => value.valueOrNull?.tagsEnabled ?? false),
|
||||
);
|
||||
if (!tagsEnabled) {
|
||||
return null;
|
||||
@@ -66,20 +64,19 @@ Future<void> tagAssets(
|
||||
required Set<String> selected,
|
||||
required Set<String> created,
|
||||
}) async {
|
||||
final tagService = ref.read(tagServiceProvider);
|
||||
final toastService = ref.read(toastServiceProvider);
|
||||
final tagIds = {...selected};
|
||||
|
||||
if (created.isNotEmpty) {
|
||||
final tags = await tagService.upsertTags(created.toList());
|
||||
final tags = await ref.read(Store.tags).upsert(created.toList());
|
||||
tagIds.addAll(tags.map((tag) => tag.id));
|
||||
}
|
||||
if (tagIds.isEmpty) {
|
||||
|
||||
if (tagIds.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final count = await tagService.bulkTagAssets(assetIds, tagIds.toList());
|
||||
ref.invalidate(tagProvider);
|
||||
final count = await ref.read(Store.tags).applyToAssets(assetIds, tagIds.toList());
|
||||
if (context.mounted) {
|
||||
toastService.success(context.t.tagged_assets(count: count));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
@@ -19,7 +19,6 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_s
|
||||
import 'package:immich_mobile/presentation/widgets/search/quick_date_picker.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -75,7 +74,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
final ratingCurrentFilterWidget = useState<Widget?>(null);
|
||||
final displayOptionCurrentFilterWidget = useState<Widget?>(null);
|
||||
|
||||
final userPreferences = ref.watch(userMetadataPreferencesProvider);
|
||||
final userPreferences = ref.watch(Store.userMetadata.preferences()).valueOrNull;
|
||||
|
||||
void search(SearchFilter f) {
|
||||
if (f == filter.value) {
|
||||
@@ -669,7 +668,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
label: context.t.search_filter_location,
|
||||
currentFilter: locationCurrentFilterWidget.value,
|
||||
),
|
||||
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
|
||||
if (userPreferences?.tagsEnabled ?? false)
|
||||
SearchFilterChip(
|
||||
icon: Icons.sell_outlined,
|
||||
onTap: showTagPicker,
|
||||
@@ -695,7 +694,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
label: context.t.search_filter_media_type,
|
||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||
),
|
||||
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
|
||||
if (userPreferences?.ratingsEnabled ?? false)
|
||||
SearchFilterChip(
|
||||
icon: Icons.star_outline_rounded,
|
||||
onTap: showStarRatingPicker,
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
|
||||
class RatingDetails extends ConsumerWidget {
|
||||
final ExifInfo? exifInfo;
|
||||
@@ -17,7 +17,7 @@ class RatingDetails extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isRatingEnabled = ref
|
||||
.watch(userMetadataPreferencesProvider)
|
||||
.watch(Store.userMetadata.preferences())
|
||||
.maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false);
|
||||
|
||||
if (!isRatingEnabled) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||
|
||||
class OcrOverlay extends ConsumerStatefulWidget {
|
||||
@@ -92,11 +91,11 @@ class _OcrOverlayState extends ConsumerState<OcrOverlay> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final ocrData = ref.watch(ocrAssetProvider((widget.asset as RemoteAsset).id));
|
||||
final ocrData = ref.watch(Store.ocr.forAsset((widget.asset as RemoteAsset).id));
|
||||
|
||||
return ocrData.when(
|
||||
data: (data) {
|
||||
if (data == null || data.isEmpty) {
|
||||
if (data.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _OcrBoxes(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
|
||||
class OcrToggleButton extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
@@ -12,7 +12,7 @@ class OcrToggleButton extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = this.asset;
|
||||
final hasOcr = asset is RemoteAsset && ref.watch(ocrAssetProvider(asset.id)).valueOrNull?.isNotEmpty == true;
|
||||
final hasOcr = asset is RemoteAsset && ref.watch(Store.ocr.forAsset(asset.id)).valueOrNull?.isNotEmpty == true;
|
||||
final showingOcr = ref.watch(assetViewerProvider.select((s) => s.showingOcr));
|
||||
|
||||
return AnimatedSwitcher(
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/ocr.model.dart';
|
||||
import 'package:immich_mobile/domain/services/ocr.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final ocrAssetProvider = FutureProvider.autoDispose.family<List<Ocr>?, String>((ref, assetId) async {
|
||||
final service = OcrService(ref.watch(driftProvider).ocrRepository);
|
||||
return service.get(assetId);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
// ignore_for_file: use-ref-and-state-synchronously
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
|
||||
class TagNotifier extends AsyncNotifier<Set<Tag>> {
|
||||
@override
|
||||
Future<Set<Tag>> build() async {
|
||||
return ref.watch(tagServiceProvider).getAllTags();
|
||||
}
|
||||
}
|
||||
|
||||
final tagProvider = AsyncNotifierProvider<TagNotifier, Set<Tag>>(TagNotifier.new);
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
final userMetadataProvider = FutureProvider<List<UserMetadata>>((ref) async {
|
||||
final repository = ref.watch(driftProvider).userMetadataRepository;
|
||||
final user = ref.watch(currentUserProvider);
|
||||
if (user == null) {
|
||||
return [];
|
||||
}
|
||||
return repository.getUserMetadata(user.id);
|
||||
});
|
||||
|
||||
final userMetadataPreferencesProvider = FutureProvider<Preferences?>((ref) async {
|
||||
final metadataList = await ref.watch(userMetadataProvider.future);
|
||||
return metadataList.firstWhereOrNull((meta) => meta.preferences != null)?.preferences;
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
|
||||
String _trimSlashes(String s) => s.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
@@ -84,7 +84,7 @@ class TagPicker extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formFocus = useFocusNode();
|
||||
final searchQuery = useState('');
|
||||
final tags = ref.watch(tagProvider);
|
||||
final tags = ref.watch(Store.tags.all());
|
||||
final selectedTagIds = useState<Set<String>>(initialSelection);
|
||||
const borderRadius = BorderRadius.all(Radius.circular(10));
|
||||
final selectedNewTagValues = useState<Set<String>>({});
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/data/server/person.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../medium/repository_context.dart';
|
||||
@@ -26,7 +25,7 @@ void main() {
|
||||
driftProvider.overrideWithValue(ctx.db),
|
||||
personApiRepositoryProvider.overrideWithValue(api),
|
||||
// No stored preferences: the default minimum face count applies
|
||||
userMetadataPreferencesProvider.overrideWith((ref) => Future.value(null)),
|
||||
Store.userMetadata.preferences().overrideWith((ref) => Stream.value(null)),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/data/server/tag.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
@@ -18,7 +18,7 @@ class MockAuthApiRepository extends Mock implements AuthApiRepository {}
|
||||
|
||||
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
class MockTagService extends Mock implements TagService {}
|
||||
class MockTagApiRepository extends Mock implements TagApiRepository {}
|
||||
|
||||
class MockDownloadRepository extends Mock implements DownloadRepository {}
|
||||
|
||||
|
||||
+11
-11
@@ -125,7 +125,7 @@ class ServiceMocks {
|
||||
final asset = AssetServiceStub(MockAssetService());
|
||||
final album = RemoteAlbumServiceStub(MockRemoteAlbumService());
|
||||
final cleanup = CleanupServiceStub(MockCleanupService());
|
||||
final tag = TagServiceStub(MockTagService());
|
||||
final tag = TagApiRepositoryStub(MockTagApiRepository());
|
||||
final backgroundSync = MockBackgroundSyncManager();
|
||||
final upload = MockForegroundUploadService();
|
||||
final cast = MockGCastService();
|
||||
@@ -154,7 +154,7 @@ class ServiceMocks {
|
||||
_stubAssetService();
|
||||
_stubRemoteAlbumService();
|
||||
_stubCleanupService();
|
||||
_stubTagService();
|
||||
_stubTagApi();
|
||||
_stubBackgroundSync();
|
||||
_stubForegroundUpload();
|
||||
}
|
||||
@@ -198,10 +198,10 @@ class ServiceMocks {
|
||||
when(cleanup.deleteLocalAssets).thenAnswer((_) async => 0);
|
||||
}
|
||||
|
||||
void _stubTagService() {
|
||||
void _stubTagApi() {
|
||||
when(tag.bulkTagAssets).thenAnswer((_) async => 0);
|
||||
when(tag.upsertTags).thenAnswer((_) async => const []);
|
||||
when(tag.getAllTags).thenAnswer((_) async => const {});
|
||||
when(tag.upsert).thenAnswer((_) async => const []);
|
||||
when(tag.getAll).thenAnswer((_) async => const []);
|
||||
}
|
||||
|
||||
void _stubBackgroundSync() {
|
||||
@@ -442,13 +442,13 @@ extension type const PermissionRepositoryStub(MockPermissionRepository repo) imp
|
||||
() => repo.getAndroidSdkVersion();
|
||||
}
|
||||
|
||||
extension type const TagServiceStub(MockTagService service) implements Stub<MockTagService> {
|
||||
extension type const TagApiRepositoryStub(MockTagApiRepository repo) implements Stub<MockTagApiRepository> {
|
||||
Future<int> Function() get bulkTagAssets =>
|
||||
() => service.bulkTagAssets(any(), any());
|
||||
() => repo.bulkTagAssets(any(), any());
|
||||
|
||||
Future<List<Tag>> Function() get upsertTags =>
|
||||
() => service.upsertTags(any());
|
||||
Future<List<Tag>> Function() get upsert =>
|
||||
() => repo.upsert(any());
|
||||
|
||||
Future<Set<Tag>> Function() get getAllTags =>
|
||||
() => service.getAllTags();
|
||||
Future<List<Tag>> Function() get getAll =>
|
||||
() => repo.getAll();
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/download.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/tag.action.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/repositories/download.repository.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../repository.mocks.dart';
|
||||
import '../../factories/local_asset_factory.dart';
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
@@ -22,11 +20,11 @@ import '../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late MockTagService tagService;
|
||||
late MockTagApiRepository tagApi;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
tagService = context.service.tag.service;
|
||||
tagApi = context.service.tag.repo;
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
@@ -97,8 +95,7 @@ void main() {
|
||||
overrides: [
|
||||
...context.selected(selection),
|
||||
toastServiceProvider.overrideWithValue(context.service.toast),
|
||||
tagServiceProvider.overrideWithValue(tagService),
|
||||
userMetadataPreferencesProvider.overrideWith((ref) async => const .new(tagsEnabled: true)),
|
||||
Store.userMetadata.preferences().overrideWith((ref) => Stream.value(const .new(tagsEnabled: true))),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -119,13 +116,13 @@ void main() {
|
||||
|
||||
testWidgets('applies the picked tags and reports the count', (tester) async {
|
||||
final asset = owned();
|
||||
when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1);
|
||||
when(() => tagApi.bulkTagAssets(any(), any())).thenAnswer((_) async => 1);
|
||||
|
||||
await pumpTag(tester, {asset});
|
||||
await applyTags([asset.id], selected: {'tag-1'});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => tagService.bulkTagAssets([asset.id], ['tag-1'])).called(1);
|
||||
verify(() => tagApi.bulkTagAssets([asset.id], ['tag-1'])).called(1);
|
||||
|
||||
final message = verify(() => context.service.toast.success(captureAny())).captured.single as String;
|
||||
expect(message, StaticTranslations.instance.tagged_assets(count: 1));
|
||||
@@ -133,15 +130,15 @@ void main() {
|
||||
|
||||
testWidgets('creates new tags first and applies them alongside the picked ones', (tester) async {
|
||||
final asset = owned();
|
||||
when(() => tagService.upsertTags(any())).thenAnswer((_) async => [const Tag(id: 'made-1', value: 'brand new')]);
|
||||
when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1);
|
||||
when(() => tagApi.upsert(any())).thenAnswer((_) async => [const Tag(id: 'made-1', value: 'brand new')]);
|
||||
when(() => tagApi.bulkTagAssets(any(), any())).thenAnswer((_) async => 1);
|
||||
|
||||
await pumpTag(tester, {asset});
|
||||
await applyTags([asset.id], selected: {'tag-1'}, created: {'brand new'});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => tagService.upsertTags(['brand new'])).called(1);
|
||||
final tagIds = verify(() => tagService.bulkTagAssets([asset.id], captureAny())).captured.single as List<String>;
|
||||
verify(() => tagApi.upsert(['brand new'])).called(1);
|
||||
final tagIds = verify(() => tagApi.bulkTagAssets([asset.id], captureAny())).captured.single as List<String>;
|
||||
expect(tagIds, containsAll(['tag-1', 'made-1']));
|
||||
});
|
||||
|
||||
@@ -152,7 +149,7 @@ void main() {
|
||||
await applyTags([asset.id]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(() => tagService.bulkTagAssets(any(), any()));
|
||||
verifyNever(() => tagApi.bulkTagAssets(any(), any()));
|
||||
verifyNever(() => context.service.toast.success(any()));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/data/server/tag.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
@@ -63,6 +64,7 @@ class PresentationContext {
|
||||
serverInfoServiceProvider.overrideWithValue(service.serverInfo),
|
||||
inLockedViewProvider.overrideWithValue(false),
|
||||
assetMediaRepositoryProvider.overrideWithValue(repository.assetMedia.api),
|
||||
tagApiRepositoryProvider.overrideWithValue(service.tag.repo),
|
||||
];
|
||||
|
||||
Drift _mockDrift() {
|
||||
|
||||
Reference in New Issue
Block a user