pull/23992/merge
Peter Ombodi 2025-12-18 11:46:02 +02:00 committed by GitHub
commit 9a9c4cd9e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 10322 additions and 236 deletions

View File

@ -440,10 +440,14 @@
"advanced_settings_proxy_headers_title": "Custom proxy headers [EXPERIMENTAL]", "advanced_settings_proxy_headers_title": "Custom proxy headers [EXPERIMENTAL]",
"advanced_settings_readonly_mode_subtitle": "Enables the read-only mode where the photos can be only viewed, things like selecting multiple images, sharing, casting, delete are all disabled. Enable/Disable read-only via user avatar from the main screen", "advanced_settings_readonly_mode_subtitle": "Enables the read-only mode where the photos can be only viewed, things like selecting multiple images, sharing, casting, delete are all disabled. Enable/Disable read-only via user avatar from the main screen",
"advanced_settings_readonly_mode_title": "Read-only mode", "advanced_settings_readonly_mode_title": "Read-only mode",
"advanced_settings_review_remote_deletions_subtitle": "Manually review cloud trash changes. Restorations are applied automatically.",
"advanced_settings_review_remote_deletions_title": "Review remote deletions",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates [EXPERIMENTAL]", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates [EXPERIMENTAL]",
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web", "advanced_settings_sync_remote_deletions_off_subtitle": "Cloud trash changes are ignored",
"advanced_settings_sync_remote_deletions_title": "Sync remote deletions [EXPERIMENTAL]", "advanced_settings_sync_remote_deletions_selector_title": "Sync remote deletions [EXPERIMENTAL]",
"advanced_settings_sync_remote_deletions_subtitle": "Automatically move assets to trash or restore them on this device when that action is taken on the web.",
"advanced_settings_sync_remote_deletions_title": "Auto sync",
"advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting",
@ -542,6 +546,12 @@
"asset_list_settings_title": "Photo Grid", "asset_list_settings_title": "Photo Grid",
"asset_offline": "Asset Offline", "asset_offline": "Asset Offline",
"asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.", "asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.",
"asset_out_of_sync_actions_title": "Pending trash decision",
"asset_out_of_sync_title": "Out-of-sync assets list",
"asset_out_of_sync_trash_confirmation_text": "Move selected assets to your device trash?",
"asset_out_of_sync_trash_confirmation_title": "Sync trash change",
"asset_out_of_sync_trash_subtitle": "Assets moved to the Immich cloud trash: choose to move them to local trash or keep them on this device.",
"asset_out_of_sync_trash_subtitle_result": "Nothing left to review — all decisions applied.",
"asset_restored_successfully": "Asset restored successfully", "asset_restored_successfully": "Asset restored successfully",
"asset_skipped": "Skipped", "asset_skipped": "Skipped",
"asset_skipped_in_trash": "In trash", "asset_skipped_in_trash": "In trash",
@ -555,11 +565,13 @@
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}", "assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album", "assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}", "assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
"assets_allowed_to_moved_to_trash_count": "Allowed to move {count, plural, one {# asset} other {# assets}} to trash",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums", "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
"assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_count": "{count, plural, one {# asset} other {# assets}}",
"assets_deleted_permanently": "{count} asset(s) deleted permanently", "assets_deleted_permanently": "{count} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server", "assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
"assets_denied_to_moved_to_trash_count": "Allowed to keep {count, plural, one {# asset} other {# assets}} on device",
"assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}", "assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}",
"assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}", "assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}",
"assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash", "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
@ -1249,6 +1261,8 @@
"jobs": "Jobs", "jobs": "Jobs",
"keep": "Keep", "keep": "Keep",
"keep_all": "Keep All", "keep_all": "Keep All",
"keep_in_trash": "Keep in trash",
"keep_on_device": "Keep on device",
"keep_this_delete_others": "Keep this, delete others", "keep_this_delete_others": "Keep this, delete others",
"kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}", "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
@ -1499,6 +1513,7 @@
"obtainium_configurator": "Obtainium Configurator", "obtainium_configurator": "Obtainium Configurator",
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link", "obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link",
"ocr": "OCR", "ocr": "OCR",
"off": "Off",
"official_immich_resources": "Official Immich Resources", "official_immich_resources": "Official Immich Resources",
"offline": "Offline", "offline": "Offline",
"offset": "Offset", "offset": "Offset",
@ -1758,6 +1773,7 @@
"retry_upload": "Retry upload", "retry_upload": "Retry upload",
"review_duplicates": "Review duplicates", "review_duplicates": "Review duplicates",
"review_large_files": "Review large files", "review_large_files": "Review large files",
"review_out_of_sync_changes": "Review out-of-sync changes",
"role": "Role", "role": "Role",
"role_editor": "Editor", "role_editor": "Editor",
"role_viewer": "Viewer", "role_viewer": "Viewer",

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@ sealed class BaseAsset {
final AssetType type; final AssetType type;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
final DateTime? deletedAt;
final int? width; final int? width;
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
@ -29,6 +30,7 @@ sealed class BaseAsset {
required this.type, required this.type,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt,
this.width, this.width,
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
@ -67,6 +69,7 @@ sealed class BaseAsset {
type: $type, type: $type,
createdAt: $createdAt, createdAt: $createdAt,
updatedAt: $updatedAt, updatedAt: $updatedAt,
deletedAt: $deletedAt,
width: ${width ?? "<NA>"}, width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"}, height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"}, durationInSeconds: ${durationInSeconds ?? "<NA>"},
@ -82,6 +85,7 @@ sealed class BaseAsset {
type == other.type && type == other.type &&
createdAt == other.createdAt && createdAt == other.createdAt &&
updatedAt == other.updatedAt && updatedAt == other.updatedAt &&
deletedAt == other.deletedAt &&
width == other.width && width == other.width &&
height == other.height && height == other.height &&
durationInSeconds == other.durationInSeconds && durationInSeconds == other.durationInSeconds &&
@ -96,6 +100,7 @@ sealed class BaseAsset {
type.hashCode ^ type.hashCode ^
createdAt.hashCode ^ createdAt.hashCode ^
updatedAt.hashCode ^ updatedAt.hashCode ^
deletedAt.hashCode ^
width.hashCode ^ width.hashCode ^
height.hashCode ^ height.hashCode ^
durationInSeconds.hashCode ^ durationInSeconds.hashCode ^

View File

@ -17,6 +17,7 @@ class LocalAsset extends BaseAsset {
required super.type, required super.type,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
super.deletedAt,
super.width, super.width,
super.height, super.height,
super.durationInSeconds, super.durationInSeconds,
@ -50,6 +51,7 @@ class LocalAsset extends BaseAsset {
type: $type, type: $type,
createdAt: $createdAt, createdAt: $createdAt,
updatedAt: $updatedAt, updatedAt: $updatedAt,
deletedAt: $deletedAt,
width: ${width ?? "<NA>"}, width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"}, height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"}, durationInSeconds: ${durationInSeconds ?? "<NA>"},
@ -98,6 +100,7 @@ class LocalAsset extends BaseAsset {
int? durationInSeconds, int? durationInSeconds,
bool? isFavorite, bool? isFavorite,
int? orientation, int? orientation,
DateTime? deletedAt,
DateTime? adjustmentTime, DateTime? adjustmentTime,
double? latitude, double? latitude,
double? longitude, double? longitude,
@ -118,6 +121,7 @@ class LocalAsset extends BaseAsset {
adjustmentTime: adjustmentTime ?? this.adjustmentTime, adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude, latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude, longitude: longitude ?? this.longitude,
deletedAt: deletedAt ?? this.deletedAt,
); );
} }
} }

View File

@ -20,6 +20,7 @@ class RemoteAsset extends BaseAsset {
required super.type, required super.type,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
super.deletedAt,
super.width, super.width,
super.height, super.height,
super.durationInSeconds, super.durationInSeconds,
@ -51,6 +52,7 @@ class RemoteAsset extends BaseAsset {
type: $type, type: $type,
createdAt: $createdAt, createdAt: $createdAt,
updatedAt: $updatedAt, updatedAt: $updatedAt,
deletedAt: ${deletedAt ?? "<NA>"},
width: ${width ?? "<NA>"}, width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"}, height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"}, durationInSeconds: ${durationInSeconds ?? "<NA>"},
@ -104,6 +106,7 @@ class RemoteAsset extends BaseAsset {
AssetVisibility? visibility, AssetVisibility? visibility,
String? livePhotoVideoId, String? livePhotoVideoId,
String? stackId, String? stackId,
DateTime? deletedAt,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: id ?? this.id, id: id ?? this.id,
@ -122,6 +125,7 @@ class RemoteAsset extends BaseAsset {
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId, stackId: stackId ?? this.stackId,
deletedAt: deletedAt ?? this.deletedAt,
); );
} }
} }

View File

@ -82,7 +82,8 @@ enum StoreKey<T> {
useWifiForUploadPhotos<bool>._(1005), useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006), needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api // TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007); shouldResetSync<bool>._(1007),
reviewOutOfSyncChangesAndroid<bool>._(1008);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;

View File

@ -0,0 +1,33 @@
enum OutSyncType { trash, upload, etc }
class TrashSyncDecision {
final String checksum;
final bool? isSyncApproved;
const TrashSyncDecision({required this.checksum, this.isSyncApproved});
@override
String toString() {
return '''TrashSyncDecision {
checksum: $checksum,
isSyncApproved: $isSyncApproved,
}''';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! TrashSyncDecision) return false;
return checksum == other.checksum && isSyncApproved == other.isSyncApproved;
}
@override
int get hashCode => checksum.hashCode ^ (isSyncApproved?.hashCode ?? 0);
TrashSyncDecision copyWith({String? checksum, bool? isSyncApproved}) {
return TrashSyncDecision(
checksum: checksum ?? this.checksum,
isSyncApproved: isSyncApproved ?? this.isSyncApproved,
);
}
}

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
@ -20,6 +21,7 @@ class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository; final DriftLocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi; final NativeSyncApi _nativeSyncApi;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final DriftTrashSyncRepository _trashSyncRepository;
final LocalFilesManagerRepository _localFilesManager; final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository; final StorageRepository _storageRepository;
final Logger _log = Logger("DeviceSyncService"); final Logger _log = Logger("DeviceSyncService");
@ -27,11 +29,13 @@ class LocalSyncService {
LocalSyncService({ LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository, required DriftLocalAlbumRepository localAlbumRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required DriftTrashSyncRepository trashSyncRepository,
required LocalFilesManagerRepository localFilesManager, required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository, required StorageRepository storageRepository,
required NativeSyncApi nativeSyncApi, required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository, }) : _localAlbumRepository = localAlbumRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository,
_trashSyncRepository = trashSyncRepository,
_localFilesManager = localFilesManager, _localFilesManager = localFilesManager,
_storageRepository = storageRepository, _storageRepository = storageRepository,
_nativeSyncApi = nativeSyncApi; _nativeSyncApi = nativeSyncApi;
@ -39,7 +43,8 @@ class LocalSyncService {
Future<void> sync({bool full = false}) async { Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
try { try {
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false) ||
Store.get(StoreKey.reviewOutOfSyncChangesAndroid, false)) {
final hasPermission = await _localFilesManager.hasManageMediaPermission(); final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (hasPermission) { if (hasPermission) {
await _syncTrashedAssets(); await _syncTrashedAssets();
@ -333,9 +338,16 @@ class LocalSyncService {
} else { } else {
_log.info("syncTrashedAssets, No remote assets found for restoration"); _log.info("syncTrashedAssets, No remote assets found for restoration");
} }
final reviewMode = Store.get(StoreKey.reviewOutOfSyncChangesAndroid, false);
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash(); final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
if (reviewMode) {
final itemsToReview = localAssetsToTrash.values.flattened.where((la) => la.checksum?.isNotEmpty == true);
_log.info(
"Apply remote trash action to review for: ${itemsToReview.map((e) => 'id:${e.id}, name:${e.name}, deletedAt:${e.deletedAt}').join('|')}",
);
await _trashSyncRepository.upsertReviewCandidates(itemsToReview);
} else {
final mediaUrls = await Future.wait( final mediaUrls = await Future.wait(
localAssetsToTrash.values localAssetsToTrash.values
.expand((e) => e) .expand((e) => e)
@ -346,9 +358,14 @@ class LocalSyncService {
if (result) { if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
} }
}
} else { } else {
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash"); _log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
} }
if (reviewMode) {
final result = await _trashSyncRepository.deleteOutdated();
_log.info("syncTrashedAssets, outdated deleted: $result");
}
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@ -8,6 +9,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_asset.repository
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -20,6 +22,7 @@ class SyncStreamService {
final SyncStreamRepository _syncStreamRepository; final SyncStreamRepository _syncStreamRepository;
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final DriftTrashSyncRepository _trashSyncRepository;
final LocalFilesManagerRepository _localFilesManager; final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository; final StorageRepository _storageRepository;
final bool Function()? _cancelChecker; final bool Function()? _cancelChecker;
@ -29,6 +32,7 @@ class SyncStreamService {
required SyncStreamRepository syncStreamRepository, required SyncStreamRepository syncStreamRepository,
required DriftLocalAssetRepository localAssetRepository, required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required DriftTrashSyncRepository trashSyncRepository,
required LocalFilesManagerRepository localFilesManager, required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository, required StorageRepository storageRepository,
bool Function()? cancelChecker, bool Function()? cancelChecker,
@ -36,6 +40,7 @@ class SyncStreamService {
_syncStreamRepository = syncStreamRepository, _syncStreamRepository = syncStreamRepository,
_localAssetRepository = localAssetRepository, _localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository,
_trashSyncRepository = trashSyncRepository,
_localFilesManager = localFilesManager, _localFilesManager = localFilesManager,
_storageRepository = storageRepository, _storageRepository = storageRepository,
_cancelChecker = cancelChecker; _cancelChecker = cancelChecker;
@ -104,11 +109,21 @@ class SyncStreamService {
case SyncEntityType.assetV1: case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false) ||
Store.get(StoreKey.reviewOutOfSyncChangesAndroid, false)) {
final hasPermission = await _localFilesManager.hasManageMediaPermission(); final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (hasPermission) { if (hasPermission) {
await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum)); final reviewMode = Store.get(StoreKey.reviewOutOfSyncChangesAndroid, false);
final trashedAssetsMap = Map<String, DateTime>.fromEntries(
remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => MapEntry(e.checksum, e.deletedAt!)),
);
await _handleRemoteTrashed(trashedAssetsMap, reviewMode);
await _applyRemoteRestoreToLocal(); await _applyRemoteRestoreToLocal();
if (reviewMode) {
await _trashSyncRepository.deleteOutdated();
final result = await _trashSyncRepository.deleteOutdated();
_logger.info("syncTrashedAssets, outdated deleted: $result");
}
} else { } else {
_logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing"); _logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
} }
@ -243,24 +258,34 @@ class SyncStreamService {
} }
} }
Future<void> _handleRemoteTrashed(Iterable<String> checksums) async { Future<void> _handleRemoteTrashed(Map<String, DateTime> trashedAssetsMap, bool reviewMode) async {
if (checksums.isEmpty) { if (trashedAssetsMap.isEmpty) {
return Future.value(); return Future.value();
} else { } else {
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums); final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(trashedAssetsMap);
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
if (reviewMode) {
final itemsToReview = localAssetsToTrash.values.flattened.where((la) => la.checksum?.isNotEmpty == true);
_logger.info(
"Apply remote trash action to review for: ${itemsToReview.map((e) => 'id:${e.id}, name:${e.name}, deletedAt:${e.deletedAt}').join('*')}",
);
await _trashSyncRepository.upsertReviewCandidates(itemsToReview);
} else {
final mediaUrls = await Future.wait( final mediaUrls = await Future.wait(
localAssetsToTrash.values localAssetsToTrash.values
.expand((e) => e) .expand((e) => e)
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())), .map(
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
),
); );
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets"); _logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
if (result) { if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
} }
}
} else { } else {
_logger.info("No assets found in backup-enabled albums for assets: $checksums"); _logger.info("No assets found in backup-enabled albums for assets: $trashedAssetsMap");
} }
} }
} }

View File

@ -35,6 +35,7 @@ enum TimelineOrigin {
search, search,
deepLink, deepLink,
albumActivities, albumActivities,
syncTrash,
} }
class TimelineFactory { class TimelineFactory {
@ -65,6 +66,8 @@ class TimelineFactory {
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy)); TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
TimelineService toTrashSyncReview() => TimelineService(_timelineRepository.toTrashSyncReview(groupBy));
TimelineService archive(String userId) => TimelineService(_timelineRepository.archived(userId, groupBy)); TimelineService archive(String userId) => TimelineService(_timelineRepository.archived(userId, groupBy));
TimelineService lockedFolder(String userId) => TimelineService(_timelineRepository.locked(userId, groupBy)); TimelineService lockedFolder(String userId) => TimelineService(_timelineRepository.locked(userId, groupBy));
@ -93,6 +96,7 @@ class TimelineService {
StreamSubscription? _bucketSubscription; StreamSubscription? _bucketSubscription;
int _totalAssets = 0; int _totalAssets = 0;
int get totalAssets => _totalAssets; int get totalAssets => _totalAssets;
TimelineService(TimelineQuery query) TimelineService(TimelineQuery query)

View File

@ -0,0 +1,12 @@
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
class TrashSyncService {
final DriftTrashSyncRepository _trashSyncRepository;
const TrashSyncService({required DriftTrashSyncRepository trashSyncRepository})
: _trashSyncRepository = trashSyncRepository;
Stream<int> watchPendingApprovalCount() => _trashSyncRepository.watchPendingApprovalCount();
Stream<bool> watchIsApprovalPending(String checksum) => _trashSyncRepository.watchIsApprovalPending(checksum);
}

View File

@ -3,6 +3,7 @@ import 'stack.entity.dart';
import 'local_asset.entity.dart'; import 'local_asset.entity.dart';
import 'local_album.entity.dart'; import 'local_album.entity.dart';
import 'local_album_asset.entity.dart'; import 'local_album_asset.entity.dart';
import 'trash_sync.entity.dart';
mergedAsset: mergedAsset:
SELECT SELECT
@ -12,6 +13,7 @@ SELECT
rae."type", rae."type",
rae.created_at as created_at, rae.created_at as created_at,
rae.updated_at, rae.updated_at,
rae.deleted_at,
rae.width, rae.width,
rae.height, rae.height,
rae.duration_in_seconds, rae.duration_in_seconds,
@ -21,19 +23,31 @@ SELECT
rae.owner_id, rae.owner_id,
rae.live_photo_video_id, rae.live_photo_video_id,
0 as orientation, 0 as orientation,
rae.stack_id rae.stack_id,
COALESCE(
(SELECT ts.is_sync_approved = 0 FROM trash_sync_entity ts WHERE ts.checksum = rae.checksum LIMIT 1),
FALSE
) AS sync_rejected
FROM FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN LEFT JOIN
stack_entity se ON rae.stack_id = se.id stack_entity se ON rae.stack_id = se.id
WHERE WHERE
(
rae.deleted_at IS NULL rae.deleted_at IS NULL
OR EXISTS (
SELECT 1 FROM local_asset_entity lae WHERE lae.checksum = rae.checksum
)
)
AND rae.visibility = 0 -- timeline visibility AND rae.visibility = 0 -- timeline visibility
AND rae.owner_id IN :user_ids AND rae.owner_id IN :user_ids
AND ( AND (
rae.stack_id IS NULL rae.stack_id IS NULL
OR rae.id = se.primary_asset_id OR rae.id = se.primary_asset_id
) )
AND NOT EXISTS (
SELECT 1 FROM trash_sync_entity ts WHERE ts.checksum = rae.checksum AND ts.is_sync_approved = 1
)
UNION ALL UNION ALL
@ -44,6 +58,7 @@ SELECT
lae."type", lae."type",
lae.created_at as created_at, lae.created_at as created_at,
lae.updated_at, lae.updated_at,
NULL as deleted_at,
lae.width, lae.width,
lae.height, lae.height,
lae.duration_in_seconds, lae.duration_in_seconds,
@ -53,7 +68,8 @@ SELECT
NULL as owner_id, NULL as owner_id,
NULL as live_photo_video_id, NULL as live_photo_video_id,
lae.orientation, lae.orientation,
NULL as stack_id NULL as stack_id,
false as sync_rejected
FROM FROM
local_asset_entity lae local_asset_entity lae
WHERE NOT EXISTS ( WHERE NOT EXISTS (
@ -88,13 +104,21 @@ FROM
LEFT JOIN LEFT JOIN
stack_entity se ON rae.stack_id = se.id stack_entity se ON rae.stack_id = se.id
WHERE WHERE
(
rae.deleted_at IS NULL rae.deleted_at IS NULL
OR EXISTS (
SELECT 1 FROM local_asset_entity lae WHERE lae.checksum = rae.checksum
)
)
AND rae.visibility = 0 -- timeline visibility AND rae.visibility = 0 -- timeline visibility
AND rae.owner_id in :user_ids AND rae.owner_id in :user_ids
AND ( AND (
rae.stack_id IS NULL rae.stack_id IS NULL
OR rae.id = se.primary_asset_id OR rae.id = se.primary_asset_id
) )
AND NOT EXISTS (
SELECT 1 FROM trash_sync_entity ts WHERE ts.checksum = rae.checksum AND ts.is_sync_approved = 1
)
UNION ALL UNION ALL
SELECT SELECT
lae.created_at lae.created_at

View File

@ -9,10 +9,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i4; as i4;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i5; as i5;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart'
as i6; as i6;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i7; as i7;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i8;
class MergedAssetDrift extends i1.ModularAccessor { class MergedAssetDrift extends i1.ModularAccessor {
MergedAssetDrift(i0.GeneratedDatabase db) : super(db); MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
@ -29,7 +31,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
); );
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}', 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.deleted_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, COALESCE((SELECT ts.is_sync_approved = 0 FROM trash_sync_entity AS ts WHERE ts.checksum = rae.checksum LIMIT 1), FALSE) AS sync_rejected FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE(rae.deleted_at IS NULL OR EXISTS (SELECT 1 AS _c0 FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum))AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)AND NOT EXISTS (SELECT 1 AS _c1 FROM trash_sync_entity AS ts WHERE ts.checksum = rae.checksum AND ts.is_sync_approved = 1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, NULL AS deleted_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, FALSE AS sync_rejected FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [ variables: [
for (var $ in userIds) i0.Variable<String>($), for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables, ...generatedlimit.introducedVariables,
@ -37,6 +39,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
readsFrom: { readsFrom: {
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
trashSyncEntity,
stackEntity, stackEntity,
localAlbumAssetEntity, localAlbumAssetEntity,
localAlbumEntity, localAlbumEntity,
@ -52,6 +55,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
), ),
createdAt: row.read<DateTime>('created_at'), createdAt: row.read<DateTime>('created_at'),
updatedAt: row.read<DateTime>('updated_at'), updatedAt: row.read<DateTime>('updated_at'),
deletedAt: row.readNullable<DateTime>('deleted_at'),
width: row.readNullable<int>('width'), width: row.readNullable<int>('width'),
height: row.readNullable<int>('height'), height: row.readNullable<int>('height'),
durationInSeconds: row.readNullable<int>('duration_in_seconds'), durationInSeconds: row.readNullable<int>('duration_in_seconds'),
@ -62,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'), livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'), orientation: row.read<int>('orientation'),
stackId: row.readNullable<String>('stack_id'), stackId: row.readNullable<String>('stack_id'),
syncRejected: row.read<bool>('sync_rejected'),
), ),
); );
} }
@ -74,7 +79,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandeduserIds = $expandVar($arrayStartIndex, userIds.length); final expandeduserIds = $expandVar($arrayStartIndex, userIds.length);
$arrayStartIndex += userIds.length; $arrayStartIndex += userIds.length;
return customSelect( return customSelect(
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.created_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT lae.created_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC', 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.created_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE(rae.deleted_at IS NULL OR EXISTS (SELECT 1 AS _c0 FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum))AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)AND NOT EXISTS (SELECT 1 AS _c1 FROM trash_sync_entity AS ts WHERE ts.checksum = rae.checksum AND ts.is_sync_approved = 1) UNION ALL SELECT lae.created_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC',
variables: [ variables: [
i0.Variable<int>(groupBy), i0.Variable<int>(groupBy),
for (var $ in userIds) i0.Variable<String>($), for (var $ in userIds) i0.Variable<String>($),
@ -83,6 +88,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
remoteAssetEntity, remoteAssetEntity,
stackEntity, stackEntity,
localAssetEntity, localAssetEntity,
trashSyncEntity,
localAlbumAssetEntity, localAlbumAssetEntity,
localAlbumEntity, localAlbumEntity,
}, },
@ -103,13 +109,16 @@ class MergedAssetDrift extends i1.ModularAccessor {
i3.$LocalAssetEntityTable get localAssetEntity => i1.ReadDatabaseContainer( i3.$LocalAssetEntityTable get localAssetEntity => i1.ReadDatabaseContainer(
attachedDatabase, attachedDatabase,
).resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'); ).resultSet<i3.$LocalAssetEntityTable>('local_asset_entity');
i6.$LocalAlbumAssetEntityTable get localAlbumAssetEntity => i6.$TrashSyncEntityTable get trashSyncEntity => i1.ReadDatabaseContainer(
attachedDatabase,
).resultSet<i6.$TrashSyncEntityTable>('trash_sync_entity');
i7.$LocalAlbumAssetEntityTable get localAlbumAssetEntity =>
i1.ReadDatabaseContainer( i1.ReadDatabaseContainer(
attachedDatabase, attachedDatabase,
).resultSet<i6.$LocalAlbumAssetEntityTable>('local_album_asset_entity'); ).resultSet<i7.$LocalAlbumAssetEntityTable>('local_album_asset_entity');
i7.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer( i8.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer(
attachedDatabase, attachedDatabase,
).resultSet<i7.$LocalAlbumEntityTable>('local_album_entity'); ).resultSet<i8.$LocalAlbumEntityTable>('local_album_entity');
} }
class MergedAssetResult { class MergedAssetResult {
@ -119,6 +128,7 @@ class MergedAssetResult {
final i2.AssetType type; final i2.AssetType type;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
final DateTime? deletedAt;
final int? width; final int? width;
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
@ -129,6 +139,7 @@ class MergedAssetResult {
final String? livePhotoVideoId; final String? livePhotoVideoId;
final int orientation; final int orientation;
final String? stackId; final String? stackId;
final bool syncRejected;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
this.localId, this.localId,
@ -136,6 +147,7 @@ class MergedAssetResult {
required this.type, required this.type,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt,
this.width, this.width,
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
@ -146,6 +158,7 @@ class MergedAssetResult {
this.livePhotoVideoId, this.livePhotoVideoId,
required this.orientation, required this.orientation,
this.stackId, this.stackId,
required this.syncRejected,
}); });
} }

View File

@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/trash_sync.model.dart';
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex(name: 'idx_trash_sync_checksum', columns: {#checksum})
@TableIndex(name: 'idx_trash_sync_status', columns: {#isSyncApproved})
@TableIndex(name: 'idx_trash_sync_checksum_status', columns: {#checksum, #isSyncApproved})
class TrashSyncEntity extends Table with DriftDefaultsMixin {
const TrashSyncEntity();
TextColumn get checksum => text()();
BoolColumn get isSyncApproved => boolean().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {checksum};
}
extension LocalAssetEntityDataDomainEx on TrashSyncEntityData {
TrashSyncDecision toDto() => TrashSyncDecision(checksum: checksum, isSyncApproved: isSyncApproved);
}

View File

@ -0,0 +1,458 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$TrashSyncEntityTableCreateCompanionBuilder =
i1.TrashSyncEntityCompanion Function({
required String checksum,
i0.Value<bool?> isSyncApproved,
i0.Value<DateTime> updatedAt,
});
typedef $$TrashSyncEntityTableUpdateCompanionBuilder =
i1.TrashSyncEntityCompanion Function({
i0.Value<String> checksum,
i0.Value<bool?> isSyncApproved,
i0.Value<DateTime> updatedAt,
});
class $$TrashSyncEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isSyncApproved => $composableBuilder(
column: $table.isSyncApproved,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$TrashSyncEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isSyncApproved => $composableBuilder(
column: $table.isSyncApproved,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$TrashSyncEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$TrashSyncEntityTable> {
$$TrashSyncEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
i0.GeneratedColumn<bool> get isSyncApproved => $composableBuilder(
column: $table.isSyncApproved,
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$TrashSyncEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData,
i1.$$TrashSyncEntityTableFilterComposer,
i1.$$TrashSyncEntityTableOrderingComposer,
i1.$$TrashSyncEntityTableAnnotationComposer,
$$TrashSyncEntityTableCreateCompanionBuilder,
$$TrashSyncEntityTableUpdateCompanionBuilder,
(
i1.TrashSyncEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData
>,
),
i1.TrashSyncEntityData,
i0.PrefetchHooks Function()
> {
$$TrashSyncEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$TrashSyncEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$TrashSyncEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$TrashSyncEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$TrashSyncEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> checksum = const i0.Value.absent(),
i0.Value<bool?> isSyncApproved = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.TrashSyncEntityCompanion(
checksum: checksum,
isSyncApproved: isSyncApproved,
updatedAt: updatedAt,
),
createCompanionCallback:
({
required String checksum,
i0.Value<bool?> isSyncApproved = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.TrashSyncEntityCompanion.insert(
checksum: checksum,
isSyncApproved: isSyncApproved,
updatedAt: updatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$TrashSyncEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData,
i1.$$TrashSyncEntityTableFilterComposer,
i1.$$TrashSyncEntityTableOrderingComposer,
i1.$$TrashSyncEntityTableAnnotationComposer,
$$TrashSyncEntityTableCreateCompanionBuilder,
$$TrashSyncEntityTableUpdateCompanionBuilder,
(
i1.TrashSyncEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$TrashSyncEntityTable,
i1.TrashSyncEntityData
>,
),
i1.TrashSyncEntityData,
i0.PrefetchHooks Function()
>;
i0.Index get idxTrashSyncChecksum => i0.Index(
'idx_trash_sync_checksum',
'CREATE INDEX idx_trash_sync_checksum ON trash_sync_entity (checksum)',
);
class $TrashSyncEntityTable extends i2.TrashSyncEntity
with i0.TableInfo<$TrashSyncEntityTable, i1.TrashSyncEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$TrashSyncEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
'checksum',
);
@override
late final i0.GeneratedColumn<String> checksum = i0.GeneratedColumn<String>(
'checksum',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _isSyncApprovedMeta =
const i0.VerificationMeta('isSyncApproved');
@override
late final i0.GeneratedColumn<bool> isSyncApproved = i0.GeneratedColumn<bool>(
'is_sync_approved',
aliasedName,
true,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_sync_approved" IN (0, 1))',
),
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [
checksum,
isSyncApproved,
updatedAt,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'trash_sync_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.TrashSyncEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('checksum')) {
context.handle(
_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta),
);
} else if (isInserting) {
context.missing(_checksumMeta);
}
if (data.containsKey('is_sync_approved')) {
context.handle(
_isSyncApprovedMeta,
isSyncApproved.isAcceptableOrUnknown(
data['is_sync_approved']!,
_isSyncApprovedMeta,
),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {checksum};
@override
i1.TrashSyncEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.TrashSyncEntityData(
checksum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}checksum'],
)!,
isSyncApproved: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_sync_approved'],
),
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$TrashSyncEntityTable createAlias(String alias) {
return $TrashSyncEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class TrashSyncEntityData extends i0.DataClass
implements i0.Insertable<i1.TrashSyncEntityData> {
final String checksum;
final bool? isSyncApproved;
final DateTime updatedAt;
const TrashSyncEntityData({
required this.checksum,
this.isSyncApproved,
required this.updatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['checksum'] = i0.Variable<String>(checksum);
if (!nullToAbsent || isSyncApproved != null) {
map['is_sync_approved'] = i0.Variable<bool>(isSyncApproved);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map;
}
factory TrashSyncEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return TrashSyncEntityData(
checksum: serializer.fromJson<String>(json['checksum']),
isSyncApproved: serializer.fromJson<bool?>(json['isSyncApproved']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'checksum': serializer.toJson<String>(checksum),
'isSyncApproved': serializer.toJson<bool?>(isSyncApproved),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
};
}
i1.TrashSyncEntityData copyWith({
String? checksum,
i0.Value<bool?> isSyncApproved = const i0.Value.absent(),
DateTime? updatedAt,
}) => i1.TrashSyncEntityData(
checksum: checksum ?? this.checksum,
isSyncApproved: isSyncApproved.present
? isSyncApproved.value
: this.isSyncApproved,
updatedAt: updatedAt ?? this.updatedAt,
);
TrashSyncEntityData copyWithCompanion(i1.TrashSyncEntityCompanion data) {
return TrashSyncEntityData(
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isSyncApproved: data.isSyncApproved.present
? data.isSyncApproved.value
: this.isSyncApproved,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('TrashSyncEntityData(')
..write('checksum: $checksum, ')
..write('isSyncApproved: $isSyncApproved, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(checksum, isSyncApproved, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.TrashSyncEntityData &&
other.checksum == this.checksum &&
other.isSyncApproved == this.isSyncApproved &&
other.updatedAt == this.updatedAt);
}
class TrashSyncEntityCompanion
extends i0.UpdateCompanion<i1.TrashSyncEntityData> {
final i0.Value<String> checksum;
final i0.Value<bool?> isSyncApproved;
final i0.Value<DateTime> updatedAt;
const TrashSyncEntityCompanion({
this.checksum = const i0.Value.absent(),
this.isSyncApproved = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
TrashSyncEntityCompanion.insert({
required String checksum,
this.isSyncApproved = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
}) : checksum = i0.Value(checksum);
static i0.Insertable<i1.TrashSyncEntityData> custom({
i0.Expression<String>? checksum,
i0.Expression<bool>? isSyncApproved,
i0.Expression<DateTime>? updatedAt,
}) {
return i0.RawValuesInsertable({
if (checksum != null) 'checksum': checksum,
if (isSyncApproved != null) 'is_sync_approved': isSyncApproved,
if (updatedAt != null) 'updated_at': updatedAt,
});
}
i1.TrashSyncEntityCompanion copyWith({
i0.Value<String>? checksum,
i0.Value<bool?>? isSyncApproved,
i0.Value<DateTime>? updatedAt,
}) {
return i1.TrashSyncEntityCompanion(
checksum: checksum ?? this.checksum,
isSyncApproved: isSyncApproved ?? this.isSyncApproved,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
if (isSyncApproved.present) {
map['is_sync_approved'] = i0.Variable<bool>(isSyncApproved.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TrashSyncEntityCompanion(')
..write('checksum: $checksum, ')
..write('isSyncApproved: $isSyncApproved, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
i0.Index get idxTrashSyncStatus => i0.Index(
'idx_trash_sync_status',
'CREATE INDEX idx_trash_sync_status ON trash_sync_entity (is_sync_approved)',
);
i0.Index get idxTrashSyncChecksumStatus => i0.Index(
'idx_trash_sync_checksum_status',
'CREATE INDEX idx_trash_sync_checksum_status ON trash_sync_entity (checksum, is_sync_approved)',
);

View File

@ -95,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository {
} }
@override @override
int get schemaVersion => 14; int get schemaVersion => 15;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@ -190,6 +190,12 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
}, },
from14To15: (m, v15) async {
await m.create(v15.trashSyncEntity);
await m.createIndex(v15.idxTrashSyncChecksum);
await m.createIndex(v15.idxTrashSyncStatus);
await m.createIndex(v15.idxTrashSyncChecksumStatus);
},
), ),
); );

View File

@ -9,39 +9,41 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i3; as i3;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i4; as i4;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart'
as i5; as i5;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i6; as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i7; as i7;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i8; as i8;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
as i9; as i9;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i10; as i10;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i11; as i11;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i12; as i12;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i13; as i13;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i14; as i14;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i15; as i15;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i16; as i16;
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i17; as i17;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i18; as i18;
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i19; as i19;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
as i20; as i20;
import 'package:drift/internal/modular.dart' as i21; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i21;
import 'package:drift/internal/modular.dart' as i22;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@ -52,38 +54,40 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this); late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
late final i4.$LocalAssetEntityTable localAssetEntity = i4 late final i4.$LocalAssetEntityTable localAssetEntity = i4
.$LocalAssetEntityTable(this); .$LocalAssetEntityTable(this);
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5 late final i5.$TrashSyncEntityTable trashSyncEntity = i5
.$TrashSyncEntityTable(this);
late final i6.$RemoteAlbumEntityTable remoteAlbumEntity = i6
.$RemoteAlbumEntityTable(this); .$RemoteAlbumEntityTable(this);
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6 late final i7.$LocalAlbumEntityTable localAlbumEntity = i7
.$LocalAlbumEntityTable(this); .$LocalAlbumEntityTable(this);
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7 late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i8
.$LocalAlbumAssetEntityTable(this); .$LocalAlbumAssetEntityTable(this);
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable( late final i9.$AuthUserEntityTable authUserEntity = i9.$AuthUserEntityTable(
this, this,
); );
late final i9.$UserMetadataEntityTable userMetadataEntity = i9 late final i10.$UserMetadataEntityTable userMetadataEntity = i10
.$UserMetadataEntityTable(this); .$UserMetadataEntityTable(this);
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable( late final i11.$PartnerEntityTable partnerEntity = i11.$PartnerEntityTable(
this, this,
); );
late final i11.$RemoteExifEntityTable remoteExifEntity = i11 late final i12.$RemoteExifEntityTable remoteExifEntity = i12
.$RemoteExifEntityTable(this); .$RemoteExifEntityTable(this);
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12 late final i13.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i13
.$RemoteAlbumAssetEntityTable(this); .$RemoteAlbumAssetEntityTable(this);
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13 late final i14.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i14
.$RemoteAlbumUserEntityTable(this); .$RemoteAlbumUserEntityTable(this);
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this); late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this);
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15 late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16
.$MemoryAssetEntityTable(this); .$MemoryAssetEntityTable(this);
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this); late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this);
late final i17.$AssetFaceEntityTable assetFaceEntity = i17 late final i18.$AssetFaceEntityTable assetFaceEntity = i18
.$AssetFaceEntityTable(this); .$AssetFaceEntityTable(this);
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this); late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
late final i19.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i19 late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
.$TrashedLocalAssetEntityTable(this); .$TrashedLocalAssetEntityTable(this);
i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer( i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
this, this,
).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new); ).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@ -93,9 +97,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAssetEntity, remoteAssetEntity,
stackEntity, stackEntity,
localAssetEntity, localAssetEntity,
trashSyncEntity,
remoteAlbumEntity, remoteAlbumEntity,
localAlbumEntity, localAlbumEntity,
localAlbumAssetEntity, localAlbumAssetEntity,
i5.idxTrashSyncChecksum,
i5.idxTrashSyncStatus,
i5.idxTrashSyncChecksumStatus,
i4.idxLocalAssetChecksum, i4.idxLocalAssetChecksum,
i2.idxRemoteAssetOwnerChecksum, i2.idxRemoteAssetOwnerChecksum,
i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerChecksum,
@ -113,9 +121,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
assetFaceEntity, assetFaceEntity,
storeEntity, storeEntity,
trashedLocalAssetEntity, trashedLocalAssetEntity,
i11.idxLatLng, i12.idxLatLng,
i19.idxTrashedLocalAssetChecksum, i20.idxTrashedLocalAssetChecksum,
i19.idxTrashedLocalAssetAlbum, i20.idxTrashedLocalAssetAlbum,
]; ];
@override @override
i0.StreamQueryUpdateRules i0.StreamQueryUpdateRules
@ -312,39 +320,41 @@ class $DriftManager {
i3.$$StackEntityTableTableManager(_db, _db.stackEntity); i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
i4.$$LocalAssetEntityTableTableManager get localAssetEntity => i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => i5.$$TrashSyncEntityTableTableManager get trashSyncEntity =>
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); i5.$$TrashSyncEntityTableTableManager(_db, _db.trashSyncEntity);
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity => i6.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); i6.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7 i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i8.$$AuthUserEntityTableTableManager get authUserEntity => i9.$$AuthUserEntityTableTableManager get authUserEntity =>
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity); i9.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity => i10.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i10.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i10.$$PartnerEntityTableTableManager get partnerEntity => i11.$$PartnerEntityTableTableManager get partnerEntity =>
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); i11.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity => i12.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); i12.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => i13.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i12.$$RemoteAlbumAssetEntityTableTableManager( i13.$$RemoteAlbumAssetEntityTableTableManager(
_db, _db,
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
); );
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13 i14.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i14
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i14.$$MemoryEntityTableTableManager get memoryEntity => i15.$$MemoryEntityTableTableManager get memoryEntity =>
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i16.$$PersonEntityTableTableManager get personEntity => i17.$$PersonEntityTableTableManager get personEntity =>
i16.$$PersonEntityTableTableManager(_db, _db.personEntity); i17.$$PersonEntityTableTableManager(_db, _db.personEntity);
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity => i18.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i18.$$StoreEntityTableTableManager get storeEntity => i19.$$StoreEntityTableTableManager get storeEntity =>
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity); i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i19.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity => i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
i19.$$TrashedLocalAssetEntityTableTableManager( i20.$$TrashedLocalAssetEntityTableTableManager(
_db, _db,
_db.trashedLocalAssetEntity, _db.trashedLocalAssetEntity,
); );

View File

@ -5941,6 +5941,470 @@ i1.GeneratedColumn<DateTime> _column_96(String aliasedName) =>
true, true,
type: i1.DriftSqlType.dateTime, type: i1.DriftSqlType.dateTime,
); );
final class Schema15 extends i0.VersionedSchema {
Schema15({required super.database}) : super(version: 15);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
trashSyncEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxTrashSyncChecksum,
idxTrashSyncStatus,
idxTrashSyncChecksumStatus,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
idxLatLng,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape24 localAssetEntity = Shape24(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 trashSyncEntity = Shape25(
source: i0.VersionedTable(
entityName: 'trash_sync_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(checksum)'],
columns: [_column_13, _column_97, _column_5],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAlbumAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35, _column_33],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxTrashSyncChecksum = i1.Index(
'idx_trash_sync_checksum',
'CREATE INDEX idx_trash_sync_checksum ON trash_sync_entity (checksum)',
);
final i1.Index idxTrashSyncStatus = i1.Index(
'idx_trash_sync_status',
'CREATE INDEX idx_trash_sync_status ON trash_sync_entity (is_sync_approved)',
);
final i1.Index idxTrashSyncChecksumStatus = i1.Index(
'idx_trash_sync_checksum_status',
'CREATE INDEX idx_trash_sync_checksum_status ON trash_sync_entity (checksum, is_sync_approved)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 trashedLocalAssetEntity = Shape23(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_95,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
}
class Shape25 extends i0.VersionedTable {
Shape25({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isSyncApproved =>
columnsByName['is_sync_approved']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<bool> _column_97(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_sync_approved',
aliasedName,
true,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_sync_approved" IN (0, 1))',
),
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -5955,6 +6419,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14, required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -6023,6 +6488,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from13To14(migrator, schema); await from13To14(migrator, schema);
return 14; return 14;
case 14:
final schema = Schema15(database: database);
final migrator = i1.Migrator(database, schema);
await from14To15(migrator, schema);
return 15;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -6043,6 +6513,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14, required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -6058,5 +6529,6 @@ i1.OnUpgrade stepByStep({
from11To12: from11To12, from11To12: from11To12,
from12To13: from12To13, from12To13: from12To13,
from13To14: from13To14, from13To14: from13To14,
from14To15: from14To15,
), ),
); );

View File

@ -99,14 +99,14 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get(); return query.map((localAlbum) => localAlbum.toDto()).get();
} }
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async { Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Map<String, DateTime> trashedAssetsMap) async {
if (checksums.isEmpty) { if (trashedAssetsMap.isEmpty) {
return {}; return {};
} }
final result = <String, List<LocalAsset>>{}; final result = <String, List<LocalAsset>>{};
for (final slice in checksums.toSet().slices(kDriftMaxChunk)) { for (final slice in trashedAssetsMap.keys.toSet().slices(kDriftMaxChunk)) {
final rows = final rows =
await (_db.select(_db.localAlbumAssetEntity).join([ await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)), innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
@ -120,10 +120,16 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
for (final row in rows) { for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final assetData = row.readTable(_db.localAssetEntity); final assetData = row.readTable(_db.localAssetEntity);
final asset = assetData.toDto(); final asset = assetData.toDto().copyWith(deletedAt: trashedAssetsMap[assetData.checksum]);
(result[albumId] ??= <LocalAsset>[]).add(asset); (result[albumId] ??= <LocalAsset>[]).add(asset);
} }
} }
return result; return result;
} }
Future<List<LocalAsset>> getByChecksums(Iterable<String> checksums) {
if (checksums.isEmpty) return Future.value([]);
final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.isIn(checksums));
return query.map((row) => row.toDto()).get();
}
} }

View File

@ -258,4 +258,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
Future<int> getCount() { Future<int> getCount() {
return _db.managers.remoteAssetEntity.count(); return _db.managers.remoteAssetEntity.count();
} }
Future<List<RemoteAsset>> getByChecksums(Iterable<String> checksums, {bool? isTrashed}) {
if (checksums.isEmpty) return Future.value([]);
final query = _db.remoteAssetEntity.select()..where((rae) => rae.checksum.isIn(checksums));
if (isTrashed != null) {
query.where((rae) => isTrashed ? rae.deletedAt.isNotNull() : rae.deletedAt.isNull());
}
return query.map((row) => row.toDto()).get();
}
} }

View File

@ -53,7 +53,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.mergedAssetDrift return _db.mergedAssetDrift
.mergedAsset(userIds: userIds, limit: (_) => Limit(count, offset)) .mergedAsset(userIds: userIds, limit: (_) => Limit(count, offset))
.map( .map(
(row) => row.remoteId != null && row.ownerId != null (row) => row.remoteId != null && row.ownerId != null && !row.syncRejected
? RemoteAsset( ? RemoteAsset(
id: row.remoteId!, id: row.remoteId!,
localId: row.localId, localId: row.localId,
@ -63,6 +63,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
type: row.type, type: row.type,
createdAt: row.createdAt, createdAt: row.createdAt,
updatedAt: row.updatedAt, updatedAt: row.updatedAt,
deletedAt: row.deletedAt,
thumbHash: row.thumbHash, thumbHash: row.thumbHash,
width: row.width, width: row.width,
height: row.height, height: row.height,
@ -73,12 +74,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
) )
: LocalAsset( : LocalAsset(
id: row.localId!, id: row.localId!,
remoteId: row.remoteId, remoteId: row.syncRejected ? null : row.remoteId,
name: row.name, name: row.name,
checksum: row.checksum, checksum: row.checksum,
type: row.type, type: row.type,
createdAt: row.createdAt, createdAt: row.createdAt,
updatedAt: row.updatedAt, updatedAt: row.updatedAt,
deletedAt: row.deletedAt,
width: row.width, width: row.width,
height: row.height, height: row.height,
isFavorite: row.isFavorite, isFavorite: row.isFavorite,
@ -277,6 +279,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
joinLocal: true, joinLocal: true,
); );
TimelineQuery toTrashSyncReview(GroupAssetsBy groupBy) => (
bucketSource: () => _watchTrashSyncBucket(groupBy: groupBy),
assetSource: (offset, count) => _getToTrashSyncBucketAssets(offset: offset, count: count),
origin: TimelineOrigin.syncTrash,
);
TimelineQuery archived(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( TimelineQuery archived(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) => filter: (row) =>
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
@ -585,6 +593,56 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
} }
} }
Stream<List<Bucket>> _watchTrashSyncBucket({GroupAssetsBy groupBy = GroupAssetsBy.day}) {
if (groupBy == GroupAssetsBy.none) {
// TODO: implement GroupAssetBy for place
throw UnsupportedError("GroupAssetsBy.none is not supported for watchPlaceBucket");
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final pendingTrashChecksums = _db.trashSyncEntity.selectOnly()
..addColumns([_db.trashSyncEntity.checksum])
..where(_db.trashSyncEntity.isSyncApproved.isNull())
..groupBy([_db.trashSyncEntity.checksum]);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.deletedAt.isNotNull() &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.checksum.isInQuery(pendingTrashChecksums),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.truncateDate(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> _getToTrashSyncBucketAssets({required int offset, required int count}) {
final pendingTrashChecksums = _db.trashSyncEntity.selectOnly()
..addColumns([_db.trashSyncEntity.checksum])
..where(_db.trashSyncEntity.isSyncApproved.isNull())
..groupBy([_db.trashSyncEntity.checksum]);
final query = _db.remoteAssetEntity.select()
..where(
(tbl) =>
tbl.deletedAt.isNotNull() &
tbl.visibility.equalsValue(AssetVisibility.timeline) &
tbl.checksum.isInQuery(pendingTrashChecksums),
)
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
} }
List<Bucket> _generateBuckets(int count) { List<Bucket> _generateBuckets(int count) {

View File

@ -0,0 +1,109 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftTrashSyncRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftTrashSyncRepository(this._db) : super(_db);
Future<void> upsertReviewCandidates(Iterable<LocalAsset> itemsToReview) async {
if (itemsToReview.isEmpty) {
return Future.value();
}
final existingEntities = <TrashSyncEntityData>[];
final checksums = itemsToReview.map((e) => e.checksum).nonNulls;
for (final slice in checksums.slices(kDriftMaxChunk)) {
final sliceResult = await (_db.trashSyncEntity.select()..where((tbl) => tbl.checksum.isIn(slice))).get();
existingEntities.addAll(sliceResult);
}
final existingMap = {for (var e in existingEntities) e.checksum: e};
return _db.batch((batch) {
for (var item in itemsToReview) {
final existing = existingMap[item.checksum];
if (existing == null || (existing.isSyncApproved == false && item.deletedAt!.isAfter(existing.updatedAt))) {
batch.insert(
_db.trashSyncEntity,
TrashSyncEntityCompanion.insert(checksum: item.checksum!, updatedAt: Value(item.deletedAt!)),
onConflict: DoUpdate(
(_) => TrashSyncEntityCompanion.custom(
updatedAt: Variable(item.deletedAt),
isSyncApproved: const Variable(null),
),
),
);
}
}
});
}
Future<void> updateApproves(Iterable<String> checksums, bool isSyncApproved) {
if (checksums.isEmpty) {
return Future.value();
}
return _db.batch((batch) {
batch.update(
_db.trashSyncEntity,
TrashSyncEntityCompanion(isSyncApproved: Value(isSyncApproved)),
where: (tbl) => tbl.checksum.isIn(checksums),
);
});
}
Future<int> deleteOutdated() async {
final remoteAliveSelect = _db.selectOnly(_db.remoteAssetEntity)
..addColumns([_db.remoteAssetEntity.checksum])
..where(_db.remoteAssetEntity.deletedAt.isNull());
final localTrashedSelect = _db.selectOnly(_db.trashedLocalAssetEntity)
..addColumns([_db.trashedLocalAssetEntity.checksum]);
final query = _db.delete(_db.trashSyncEntity)
..where((row) => row.isSyncApproved.isNull() | row.isSyncApproved.equals(false))
..where((row) => row.checksum.isInQuery(remoteAliveSelect) | row.checksum.isInQuery(localTrashedSelect));
final deletedMatched = await query.go();
final localTrashedChecksums = _db.selectOnly(_db.trashedLocalAssetEntity)
..addColumns([_db.trashedLocalAssetEntity.checksum])
..where(_db.trashedLocalAssetEntity.checksum.isNotNull());
final localAssetChecksums = _db.selectOnly(_db.localAssetEntity)
..addColumns([_db.localAssetEntity.checksum])
..where(_db.localAssetEntity.checksum.isNotNull());
final orphanQuery = _db.delete(_db.trashSyncEntity)
..where(
(row) =>
(row.isSyncApproved.equals(false) & row.checksum.isNotInQuery(localAssetChecksums)) |
(row.isSyncApproved.equals(true) & row.checksum.isNotInQuery(localTrashedChecksums)),
);
final deletedOrphans = await orphanQuery.go();
return deletedMatched + deletedOrphans;
}
Stream<int> watchPendingApprovalCount() {
final countExpr = _db.trashSyncEntity.checksum.count(distinct: true);
final q = _db.selectOnly(_db.trashSyncEntity)
..addColumns([countExpr])
..where(_db.trashSyncEntity.isSyncApproved.isNull());
return q.watchSingle().map((row) => row.read(countExpr) ?? 0).distinct();
}
Stream<bool> watchIsApprovalPending(String checksum) {
final query = _db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.checksum])
..where((_db.trashSyncEntity.checksum.equals(checksum) & _db.trashSyncEntity.isSyncApproved.isNull()))
..limit(1);
return query.watchSingleOrNull().map((row) => row != null).distinct();
}
}

View File

@ -224,7 +224,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
for (final row in rows) { for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final asset = row.readTable(_db.localAssetEntity).toDto(); final remoteDeletedAt = row.read(_db.remoteAssetEntity.deletedAt);
final asset = row.readTable(_db.localAssetEntity).toDto().copyWith(deletedAt: remoteDeletedAt);
(result[albumId] ??= <LocalAsset>[]).add(asset); (result[albumId] ??= <LocalAsset>[]).add(asset);
} }

View File

@ -0,0 +1,81 @@
import 'package:auto_route/auto_route.dart';
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/presentation/widgets/bottom_sheet/trash_sync_bottom_bar.widget.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/trash_sync.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@RoutePage()
class DriftTrashSyncReviewPage extends ConsumerWidget {
const DriftTrashSyncReviewPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = context.router;
ref.listen(outOfSyncCountProvider, (previous, next) {
final prevCount = previous?.asData?.value ?? 0;
final nextCount = next.asData?.value;
if (prevCount > 0 && nextCount == 0) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 1600));
if (router.current.name == DriftTrashSyncReviewRoute.name) {
await router.maybePop();
}
});
}
});
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access trash');
}
final timelineService = ref.watch(timelineFactoryProvider).toTrashSyncReview();
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: SliverAppBar(
title: Text('asset_out_of_sync_title'.tr()),
floating: true,
snap: true,
pinned: true,
centerTitle: true,
elevation: 0,
),
topSliverWidgetHeight: 24,
topSliverWidget: SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 72.0,
child: Consumer(
builder: (context, ref, _) {
final outOfSyncCount = ref.watch(outOfSyncCountProvider).maybeWhen(data: (v) => v, orElse: () => 0);
return outOfSyncCount > 0
? const Text('asset_out_of_sync_trash_subtitle').tr()
: Center(
child: Text(
'asset_out_of_sync_trash_subtitle_result',
style: context.textTheme.bodyLarge,
).tr(),
);
},
),
),
),
),
bottomSheet: const TrashSyncBottomBar(),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This deny move to trash action has the following behavior:
/// - Deny moving to the local trash those assets that are in the remote trash.
///
/// This action is used when the asset is selected in multi-selection mode in the trash page
class KeepOnDeviceActionButton extends ConsumerWidget {
final ActionSource source;
final bool isPreview;
const KeepOnDeviceActionButton({super.key, required this.source, required this.isPreview});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final actionNotifier = ref.read(actionProvider.notifier);
final multiSelectNotifier = ref.read(multiSelectProvider.notifier);
final result = await actionNotifier.resolveRemoteTrash(source, allow: false);
multiSelectNotifier.reset();
if (source == ActionSource.viewer) {
Future.delayed(Durations.extralong4, () {
EventStream.shared.emit(const ViewerReloadAssetEvent());
EventStream.shared.emit(const TimelineReloadEvent());
});
}
if (context.mounted) {
final successMessage = 'assets_denied_to_moved_to_trash_count'.t(
context: context,
args: {'count': result.count.toString()},
);
ImmichToast.show(
context: context,
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
const iconData = Icons.cloud_off_outlined;
return isPreview
? BaseActionButton(
maxWidth: 110.0,
iconData: iconData,
label: 'keep'.tr(),
onPressed: () => _onTap(context, ref),
)
: TextButton.icon(
icon: const Icon(iconData),
label: Text('keep_on_device'.tr(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@ -0,0 +1,105 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'base_action_button.widget.dart';
/// This move to trash action has the following behavior:
/// - Allows moving to the local trash those assets that are in the remote trash.
///
/// This action is used when the asset is selected in multi-selection mode in the review out-of-sync changes
class MoveToTrashActionButton extends ConsumerWidget {
final ActionSource source;
final bool isPreview;
const MoveToTrashActionButton({super.key, required this.source, required this.isPreview});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('asset_out_of_sync_trash_confirmation_title'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [Text('asset_out_of_sync_trash_confirmation_text'.tr())],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('cancel'.t(context: context)),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
child: Text('control_bottom_app_bar_trash_from_immich'.tr()),
),
],
);
},
);
if (confirmed != true) {
return;
}
final actionNotifier = ref.read(actionProvider.notifier);
final multiSelectNotifier = ref.read(multiSelectProvider.notifier);
final result = await actionNotifier.resolveRemoteTrash(source, allow: true);
multiSelectNotifier.reset();
if (source == ActionSource.viewer) {
Future.delayed(Durations.extralong4, () {
EventStream.shared.emit(const ViewerReloadAssetEvent());
EventStream.shared.emit(const TimelineReloadEvent());
});
}
if (context.mounted) {
final successMessage = 'assets_allowed_to_moved_to_trash_count'.t(
context: context,
args: {'count': result.count.toString()},
);
ImmichToast.show(
context: context,
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
const iconData = Icons.delete_forever_outlined;
return isPreview
? BaseActionButton(
maxWidth: 100.0,
iconData: iconData,
label: 'delete'.tr(),
onPressed: () => _onTap(context, ref),
)
: TextButton.icon(
icon: Icon(iconData, color: Colors.red[400]),
label: Text(
'control_bottom_app_bar_trash_from_immich'.tr(),
style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold),
),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@ -457,7 +457,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
void _onAssetReloadEvent() async { void _onAssetReloadEvent() async {
final index = pageController.page?.round() ?? 0; int index = pageController.page?.round() ?? 0;
if (index == totalAssets && index > 0) {
--index;
await pageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn);
}
final timelineService = ref.read(timelineServiceProvider); final timelineService = ref.read(timelineServiceProvider);
final newAsset = await timelineService.getAssetAsync(index); final newAsset = await timelineService.getAssetAsync(index);

View File

@ -1,17 +1,23 @@
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/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/keep_on_device_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
@ -34,6 +40,10 @@ class ViewerBottomBar extends ConsumerWidget {
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final timelineOrigin = ref.read(timelineServiceProvider).origin;
final isSyncTrashTimeline = timelineOrigin == TimelineOrigin.syncTrash;
final isWaitingForSyncApproval = ref.watch(isWaitingForSyncApprovalProvider(asset.checksum!)).value == true;
if (!showControls) { if (!showControls) {
opacity = 0; opacity = 0;
} }
@ -41,6 +51,11 @@ class ViewerBottomBar extends ConsumerWidget {
final originalTheme = context.themeData; final originalTheme = context.themeData;
final actions = <Widget>[ final actions = <Widget>[
if (isSyncTrashTimeline || isWaitingForSyncApproval) ...[
const Text('asset_out_of_sync_actions_title').tr(),
const KeepOnDeviceActionButton(source: ActionSource.viewer, isPreview: true),
const MoveToTrashActionButton(source: ActionSource.viewer, isPreview: true),
] else ...[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(source: ActionSource.viewer),
if (!isInLockedView) ...[ if (!isInLockedView) ...[
@ -54,6 +69,7 @@ class ViewerBottomBar extends ConsumerWidget {
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), : const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
], ],
], ],
],
]; ];
return IgnorePointer( return IgnorePointer(

View File

@ -15,6 +15,7 @@ import 'package:immich_mobile/providers/activity.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@ -49,6 +50,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final originalTheme = context.themeData; final originalTheme = context.themeData;
final isWaitingForSyncApproval = ref.watch(isWaitingForSyncApprovalProvider(asset.checksum)).value == true;
final actions = <Widget>[ final actions = <Widget>[
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
if (album != null && album.isActivityEnabled && album.isShared) if (album != null && album.isActivityEnabled && album.isShared)
@ -59,9 +62,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
}, },
), ),
if (asset.hasRemote && isOwner && !asset.isFavorite) if (asset.hasRemote && isOwner && !asset.isFavorite && !isWaitingForSyncApproval)
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true), const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.hasRemote && isOwner && asset.isFavorite) if (asset.hasRemote && isOwner && asset.isFavorite && !isWaitingForSyncApproval)
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true), const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
ViewerKebabMenu(originalTheme: originalTheme), ViewerKebabMenu(originalTheme: originalTheme),

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@ -35,6 +36,7 @@ class ViewerKebabMenu extends ConsumerWidget {
final currentAlbum = ref.watch(currentRemoteAlbumProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
final isWaitingForTrashApproval = ref.watch(isWaitingForSyncApprovalProvider(asset.checksum)).value == true;
final actionContext = ActionButtonContext( final actionContext = ActionButtonContext(
asset: asset, asset: asset,
@ -49,6 +51,7 @@ class ViewerKebabMenu extends ConsumerWidget {
isCasting: isCasting, isCasting: isCasting,
timelineOrigin: timelineOrigin, timelineOrigin: timelineOrigin,
originalTheme: originalTheme, originalTheme: originalTheme,
isWaitingForTrashApproval: isWaitingForTrashApproval,
); );
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref); final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/keep_on_device_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_trash_action_button.widget.dart';
class TrashSyncBottomBar extends ConsumerWidget {
const TrashSyncBottomBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Container(
color: context.themeData.canvasColor,
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
KeepOnDeviceActionButton(source: ActionSource.timeline, isPreview: false),
MoveToTrashActionButton(source: ActionSource.timeline, isPreview: false),
],
),
),
),
),
);
}
}

View File

@ -88,11 +88,15 @@ class ThumbnailTile extends ConsumerWidget {
child: _TileOverlayIcon(Icons.cloud_outlined), child: _TileOverlayIcon(Icons.cloud_outlined),
), ),
), ),
AssetState.merged => const Align( AssetState.merged => Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0), padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_done_outlined), child: _TileOverlayIcon(
(asset as RemoteAsset).deletedAt != null
? Icons.sync_problem_rounded
: Icons.cloud_done_outlined,
),
), ),
), ),
}, },
@ -157,6 +161,7 @@ class _SelectionIndicator extends StatelessWidget {
class _VideoIndicator extends StatelessWidget { class _VideoIndicator extends StatelessWidget {
final Duration duration; final Duration duration;
const _VideoIndicator(this.duration); const _VideoIndicator(this.duration);
@override @override

View File

@ -6,3 +6,8 @@ part 'app_settings.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
AppSettingsService appSettingsService(Ref _) => const AppSettingsService(); AppSettingsService appSettingsService(Ref _) => const AppSettingsService();
final appSettingStreamProvider = StreamProvider.family.autoDispose<bool, AppSettingsEnum<bool>>((ref, setting) {
final service = ref.watch(appSettingsServiceProvider);
return service.watchSetting(setting);
});

View File

@ -421,6 +421,22 @@ class ActionNotifier extends Notifier<void> {
return ActionResult(count: assets.length, success: false, error: error.toString()); return ActionResult(count: assets.length, success: false, error: error.toString());
} }
} }
Future<ActionResult> resolveRemoteTrash(ActionSource source, {required bool allow}) async {
final remoteChecksums = _getAssets(source).map((a) => a.checksum).nonNulls;
_logger.info('resolveRemoteTrash, remoteChecksums: $remoteChecksums, allow: $allow');
try {
final result = await _service.resolveRemoteTrash(remoteChecksums, allow: allow);
return ActionResult(
count: remoteChecksums.length,
success: result,
error: result ? null : 'Failed to move assets to trash',
);
} catch (error, stack) {
_logger.severe('Failed to ${allow ? 'allow' : 'deny'} to move assets to trash', error, stack);
return ActionResult(count: remoteChecksums.length, success: false, error: error.toString());
}
}
} }
extension on Iterable<RemoteAsset> { extension on Iterable<RemoteAsset> {

View File

@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
final syncStreamServiceProvider = Provider( final syncStreamServiceProvider = Provider(
@ -19,6 +20,7 @@ final syncStreamServiceProvider = Provider(
syncStreamRepository: ref.watch(syncStreamRepositoryProvider), syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
localAssetRepository: ref.watch(localAssetRepository), localAssetRepository: ref.watch(localAssetRepository),
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
trashSyncRepository: ref.watch(trashSyncRepositoryProvider),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider), localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider), storageRepository: ref.watch(storageRepositoryProvider),
cancelChecker: ref.watch(cancellationProvider), cancelChecker: ref.watch(cancellationProvider),
@ -33,6 +35,7 @@ final localSyncServiceProvider = Provider(
(ref) => LocalSyncService( (ref) => LocalSyncService(
localAlbumRepository: ref.watch(localAlbumRepository), localAlbumRepository: ref.watch(localAlbumRepository),
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
trashSyncRepository: ref.watch(trashSyncRepositoryProvider),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider), localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider), storageRepository: ref.watch(storageRepositoryProvider),
nativeSyncApi: ref.watch(nativeSyncApiProvider), nativeSyncApi: ref.watch(nativeSyncApiProvider),

View File

@ -1,12 +1,45 @@
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'db.provider.dart';
typedef TrashedAssetsCount = ({int total, int hashed}); typedef TrashedAssetsCount = ({int total, int hashed});
final trashSyncRepositoryProvider = Provider<DriftTrashSyncRepository>(
(ref) => DriftTrashSyncRepository(ref.watch(driftProvider)),
);
final trashedAssetsCountProvider = StreamProvider<TrashedAssetsCount>((ref) { final trashedAssetsCountProvider = StreamProvider<TrashedAssetsCount>((ref) {
final repo = ref.watch(trashedLocalAssetRepository); final repo = ref.watch(trashedLocalAssetRepository);
final total$ = repo.watchCount(); final total$ = repo.watchCount();
final hashed$ = repo.watchHashedCount(); final hashed$ = repo.watchHashedCount();
return StreamZip<int>([total$, hashed$]).map((values) => (total: values[0], hashed: values[1])); return StreamZip<int>([total$, hashed$]).map((values) => (total: values[0], hashed: values[1]));
}); });
final trashSyncServiceProvider = Provider(
(ref) => TrashSyncService(trashSyncRepository: ref.watch(trashSyncRepositoryProvider)),
);
final outOfSyncCountProvider = StreamProvider<int>((ref) {
final enabledReviewMode = ref.watch(appSettingStreamProvider(AppSettingsEnum.reviewOutOfSyncChangesAndroid));
final service = ref.watch(trashSyncServiceProvider);
return enabledReviewMode.when(
data: (enabled) => enabled ? service.watchPendingApprovalCount() : Stream<int>.value(0),
loading: () => Stream<int>.value(0),
error: (_, __) => Stream<int>.value(0),
);
});
final isWaitingForSyncApprovalProvider = StreamProvider.family<bool, String?>((ref, checksum) {
final enabledReviewMode = ref.watch(appSettingStreamProvider(AppSettingsEnum.reviewOutOfSyncChangesAndroid));
final service = ref.watch(trashSyncServiceProvider);
return enabledReviewMode.when(
data: (enabled) => enabled && checksum != null ? service.watchIsApprovalPending(checksum) : Stream.value(false),
loading: () => Stream.value(false),
error: (_, __) => Stream.value(false),
);
});

View File

@ -104,6 +104,7 @@ import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash_sync_review.page.dart';
import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart';
import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart';
@ -309,6 +310,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftMemoryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMemoryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftFavoriteRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftFavoriteRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftTrashRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftTrashRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftTrashSyncReviewRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftLockedFolderRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: DriftLockedFolderRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
AutoRoute(page: DriftVideoRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftVideoRoute.page, guards: [_authGuard, _duplicateGuard]),

View File

@ -1488,6 +1488,22 @@ class DriftTrashRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [DriftTrashSyncReviewPage]
class DriftTrashSyncReviewRoute extends PageRouteInfo<void> {
const DriftTrashSyncReviewRoute({List<PageRouteInfo>? children})
: super(DriftTrashSyncReviewRoute.name, initialChildren: children);
static const String name = 'DriftTrashSyncReviewRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftTrashSyncReviewPage();
},
);
}
/// generated route for /// generated route for
/// [DriftUploadDetailPage] /// [DriftUploadDetailPage]
class DriftUploadDetailRoute extends PageRouteInfo<void> { class DriftUploadDetailRoute extends PageRouteInfo<void> {

View File

@ -8,16 +8,22 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/utils/timezone.dart';
import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart';
import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -28,8 +34,12 @@ final actionServiceProvider = Provider<ActionService>(
ref.watch(localAssetRepository), ref.watch(localAssetRepository),
ref.watch(driftAlbumApiRepositoryProvider), ref.watch(driftAlbumApiRepositoryProvider),
ref.watch(remoteAlbumRepository), ref.watch(remoteAlbumRepository),
ref.watch(trashSyncRepositoryProvider),
ref.watch(assetMediaRepositoryProvider), ref.watch(assetMediaRepositoryProvider),
ref.watch(downloadRepositoryProvider), ref.watch(downloadRepositoryProvider),
ref.watch(storageRepositoryProvider),
ref.watch(localFilesManagerRepositoryProvider),
Logger('ActionService'),
), ),
); );
@ -39,8 +49,12 @@ class ActionService {
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final DriftAlbumApiRepository _albumApiRepository; final DriftAlbumApiRepository _albumApiRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftTrashSyncRepository _trashSyncRepository;
final AssetMediaRepository _assetMediaRepository; final AssetMediaRepository _assetMediaRepository;
final DownloadRepository _downloadRepository; final DownloadRepository _downloadRepository;
final StorageRepository _storageRepository;
final LocalFilesManagerRepository _localFilesManager;
final Logger _logger;
const ActionService( const ActionService(
this._assetApiRepository, this._assetApiRepository,
@ -48,8 +62,12 @@ class ActionService {
this._localAssetRepository, this._localAssetRepository,
this._albumApiRepository, this._albumApiRepository,
this._remoteAlbumRepository, this._remoteAlbumRepository,
this._trashSyncRepository,
this._assetMediaRepository, this._assetMediaRepository,
this._downloadRepository, this._downloadRepository,
this._storageRepository,
this._localFilesManager,
this._logger,
); );
Future<void> shareLink(List<String> remoteIds, BuildContext context) async { Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
@ -242,4 +260,28 @@ class ActionService {
Future<List<bool>> downloadAll(List<RemoteAsset> assets) { Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
return _downloadRepository.downloadAllAssets(assets); return _downloadRepository.downloadAllAssets(assets);
} }
Future<bool> resolveRemoteTrash(Iterable<String> trashedChecksums, {required bool allow}) async {
if (trashedChecksums.isEmpty) {
return false;
}
await _trashSyncRepository.updateApproves(trashedChecksums, allow);
if (!allow) {
return true;
}
final localAssets = await _localAssetRepository.getByChecksums(trashedChecksums);
if (localAssets.isEmpty) {
return false;
}
final mediaUrls = await Future.wait(
localAssets.map(
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
),
);
_logger.info("Moving assets to trash: ${mediaUrls.join(", ")}");
if (mediaUrls.isEmpty) {
return false;
}
return await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
}
} }

View File

@ -30,6 +30,7 @@ enum AppSettingsEnum<T> {
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2), selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false), manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
reviewOutOfSyncChangesAndroid<bool>(StoreKey.reviewOutOfSyncChangesAndroid, null, false),
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5 logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true), loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
@ -72,4 +73,11 @@ class AppSettingsService {
Future<void> setSetting<T>(AppSettingsEnum<T> setting, T value) { Future<void> setSetting<T>(AppSettingsEnum<T> setting, T value) {
return Store.put(setting.storeKey, value); return Store.put(setting.storeKey, value);
} }
Stream<T> watchSetting<T>(AppSettingsEnum<T> setting) async* {
yield getSetting<T>(setting);
await for (final dynamic value in Store.watch(setting.storeKey)) {
yield (value as T?) ?? setting.defaultValue;
}
}
} }

View File

@ -38,6 +38,7 @@ class ActionButtonContext {
final bool isStacked; final bool isStacked;
final RemoteAlbum? currentAlbum; final RemoteAlbum? currentAlbum;
final bool advancedTroubleshooting; final bool advancedTroubleshooting;
final bool isWaitingForTrashApproval;
final ActionSource source; final ActionSource source;
final bool isCasting; final bool isCasting;
final TimelineOrigin timelineOrigin; final TimelineOrigin timelineOrigin;
@ -52,6 +53,7 @@ class ActionButtonContext {
required this.isInLockedView, required this.isInLockedView,
required this.currentAlbum, required this.currentAlbum,
required this.advancedTroubleshooting, required this.advancedTroubleshooting,
required this.isWaitingForTrashApproval,
required this.source, required this.source,
this.isCasting = false, this.isCasting = false,
this.timelineOrigin = TimelineOrigin.main, this.timelineOrigin = TimelineOrigin.main,
@ -92,7 +94,8 @@ enum ActionButtonType {
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
context.asset.hasRemote && // context.asset.hasRemote && //
!context.isArchived, !context.isArchived &&
!context.isWaitingForTrashApproval,
ActionButtonType.unarchive => ActionButtonType.unarchive =>
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
@ -106,27 +109,31 @@ enum ActionButtonType {
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
context.asset.hasRemote && // context.asset.hasRemote && //
context.isTrashEnabled, context.isTrashEnabled &&
!context.isWaitingForTrashApproval,
ActionButtonType.deletePermanent => ActionButtonType.deletePermanent =>
context.isOwner && // context.isOwner && //
context.asset.hasRemote && // context.asset.hasRemote && //
!context.isTrashEnabled || !context.isTrashEnabled ||
context.isInLockedView, context.isInLockedView && !context.isWaitingForTrashApproval,
ActionButtonType.delete => ActionButtonType.delete =>
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
context.asset.hasRemote, context.asset.hasRemote &&
!context.isWaitingForTrashApproval,
ActionButtonType.moveToLockFolder => ActionButtonType.moveToLockFolder =>
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
context.asset.hasRemote, context.asset.hasRemote &&
!context.isWaitingForTrashApproval,
ActionButtonType.removeFromLockFolder => ActionButtonType.removeFromLockFolder =>
context.isOwner && // context.isOwner && //
context.isInLockedView && // context.isInLockedView && //
context.asset.hasRemote, context.asset.hasRemote,
ActionButtonType.deleteLocal => ActionButtonType.deleteLocal =>
!context.isInLockedView && // !context.isInLockedView && //
context.asset.hasLocal, context.asset.hasLocal &&
!context.isWaitingForTrashApproval,
ActionButtonType.upload => ActionButtonType.upload =>
!context.isInLockedView && // !context.isInLockedView && //
context.asset.storage == AssetState.local, context.asset.storage == AssetState.local,
@ -154,6 +161,7 @@ enum ActionButtonType {
context.timelineOrigin != TimelineOrigin.lockedFolder && context.timelineOrigin != TimelineOrigin.lockedFolder &&
context.timelineOrigin != TimelineOrigin.archive && context.timelineOrigin != TimelineOrigin.archive &&
context.timelineOrigin != TimelineOrigin.localAlbum && context.timelineOrigin != TimelineOrigin.localAlbum &&
context.timelineOrigin != TimelineOrigin.syncTrash &&
context.isOwner, context.isOwner,
ActionButtonType.cast => context.isCasting || context.asset.hasRemote, ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
}; };

View File

@ -7,12 +7,14 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
@ -67,19 +69,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
); );
} }
buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) { buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing, Color? btnColor}) {
return ListTile( return ListTile(
dense: true, dense: true,
visualDensity: VisualDensity.standard, visualDensity: VisualDensity.standard,
contentPadding: const EdgeInsets.only(left: 30, right: 30), contentPadding: const EdgeInsets.only(left: 30, right: 30),
minLeadingWidth: 40, minLeadingWidth: 40,
leading: SizedBox(child: Icon(icon, color: theme.textTheme.labelLarge?.color?.withAlpha(250), size: 20)), leading: SizedBox(
child: Icon(icon, color: btnColor ?? theme.textTheme.labelLarge?.color?.withAlpha(250), size: 20),
),
title: Text( title: Text(
text, text,
style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), style: theme.textTheme.labelLarge?.copyWith(
color: btnColor ?? theme.textTheme.labelLarge?.color?.withAlpha(250),
),
).tr(), ).tr(),
onTap: onTap, onTap: onTap,
trailing: trailing, trailing: trailing,
iconColor: btnColor,
); );
} }
@ -87,6 +94,25 @@ class ImmichAppBarDialog extends HookConsumerWidget {
return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute()));
} }
Widget buildOutOfSyncButton() {
return Consumer(
builder: (context, ref, _) {
final count = ref.watch(outOfSyncCountProvider).value ?? 0;
if (count == 0) {
return const SizedBox.shrink();
}
final btnColor = const Color.fromARGB(255, 243, 188, 106);
return buildActionButton(
Icons.warning_amber_rounded,
'review_out_of_sync_changes'.t(),
() => context.pushRoute(const DriftTrashSyncReviewRoute()),
trailing: Text('($count)', style: theme.textTheme.labelLarge?.copyWith(color: btnColor)),
btnColor: btnColor,
);
},
);
}
buildAppLogButton() { buildAppLogButton() {
return buildActionButton( return buildActionButton(
Icons.assignment_outlined, Icons.assignment_outlined,
@ -269,6 +295,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
const AppBarProfileInfoBox(), const AppBarProfileInfoBox(),
buildStorageInformation(), buildStorageInformation(),
const AppBarServerInfo(), const AppBarServerInfo(),
buildOutOfSyncButton(),
if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(),
buildAppLogButton(), buildAppLogButton(),
buildSettingButton(), buildSettingButton(),

View File

@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@ -123,6 +124,7 @@ class _ProfileIndicator extends ConsumerWidget {
final serverInfoState = ref.watch(serverInfoProvider); final serverInfoState = ref.watch(serverInfoProvider);
const widgetSize = 30.0; const widgetSize = 30.0;
final outOfSyncCount = ref.watch(outOfSyncCountProvider).maybeWhen(data: (count) => count, orElse: () => 0);
void toggleReadonlyMode() { void toggleReadonlyMode() {
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
@ -159,7 +161,7 @@ class _ProfileIndicator extends ConsumerWidget {
), ),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
isLabelVisible: versionWarningPresent, isLabelVisible: versionWarningPresent || outOfSyncCount > 0,
offset: const Offset(-2, -12), offset: const Offset(-2, -12),
child: user == null child: user == null
? const Icon(Icons.face_outlined, size: widgetSize) ? const Icon(Icons.face_outlined, size: widgetSize)

View File

@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
@ -18,8 +19,10 @@ import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart';
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart'; import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -32,9 +35,7 @@ class AdvancedSettings extends HookConsumerWidget {
bool isLoggedIn = ref.read(currentUserProvider) != null; bool isLoggedIn = ref.read(currentUserProvider) != null;
final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
final isManageMediaSupported = useState(false); final isManageMediaSupported = useState(false);
final manageMediaAndroidPermission = useState(false);
final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert); final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert);
@ -58,11 +59,6 @@ class AdvancedSettings extends HookConsumerWidget {
useEffect(() { useEffect(() {
() async { () async {
isManageMediaSupported.value = await checkAndroidVersion(); isManageMediaSupported.value = await checkAndroidVersion();
if (isManageMediaSupported.value) {
manageMediaAndroidPermission.value = await ref
.read(localFilesManagerRepositoryProvider)
.hasManageMediaPermission();
}
}(); }();
return null; return null;
}, []); }, []);
@ -74,36 +70,7 @@ class AdvancedSettings extends HookConsumerWidget {
title: "advanced_settings_troubleshooting_title".tr(), title: "advanced_settings_troubleshooting_title".tr(),
subtitle: "advanced_settings_troubleshooting_subtitle".tr(), subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
), ),
if (isManageMediaSupported.value) if (isManageMediaSupported.value) const _TrashSyncModeSelector(),
Column(
children: [
SettingsSwitchListTile(
enabled: true,
valueNotifier: manageLocalMediaAndroid,
title: "advanced_settings_sync_remote_deletions_title".tr(),
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
onChanged: (value) async {
if (value) {
final result = await ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission();
manageLocalMediaAndroid.value = result;
manageMediaAndroidPermission.value = result;
}
},
),
SettingsActionTile(
title: "manage_media_access_title".tr(),
statusText: manageMediaAndroidPermission.value ? "allowed".tr() : "not_allowed".tr(),
subtitle: "manage_media_access_rationale".tr(),
statusColor: manageLocalMediaAndroid.value && !manageMediaAndroidPermission.value
? const Color.fromARGB(255, 243, 188, 106)
: null,
onActionTap: () async {
final result = await ref.read(localFilesManagerRepositoryProvider).manageMediaPermission();
manageMediaAndroidPermission.value = result;
},
),
],
),
SettingsSliderListTile( SettingsSliderListTile(
text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}), text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}),
valueNotifier: levelId, valueNotifier: levelId,
@ -158,3 +125,120 @@ class AdvancedSettings extends HookConsumerWidget {
return SettingsSubPageScaffold(settings: advancedSettings); return SettingsSubPageScaffold(settings: advancedSettings);
} }
} }
enum TrashSyncMode { none, auto, review }
final manageMediaPermissionProvider = FutureProvider<bool>((ref) async {
return ref.watch(localFilesManagerRepositoryProvider).hasManageMediaPermission();
});
class _TrashSyncModeSelector extends HookConsumerWidget {
const _TrashSyncModeSelector();
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoSyncChanges = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
final reviewOutOfSyncChanges = useAppSettingsState(AppSettingsEnum.reviewOutOfSyncChangesAndroid);
final manageMediaAndroidPermission = ref.watch(manageMediaPermissionProvider);
final manageMediaAndroidPermissionValue = manageMediaAndroidPermission.valueOrNull;
final selectedTrashSyncMode = autoSyncChanges.value
? TrashSyncMode.auto
: reviewOutOfSyncChanges.value
? TrashSyncMode.review
: TrashSyncMode.none;
Future<void> attemptToEnableSetting(AppSettingsEnum key) async {
final result = await ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission();
ref.invalidate(manageMediaPermissionProvider);
if (key == AppSettingsEnum.manageLocalMediaAndroid) {
autoSyncChanges.value = result;
if (result) {
reviewOutOfSyncChanges.value = false;
}
}
if (key == AppSettingsEnum.reviewOutOfSyncChangesAndroid) {
reviewOutOfSyncChanges.value = result;
if (result) {
autoSyncChanges.value = false;
}
}
ref.invalidate(appSettingsServiceProvider);
}
Future<void> handleTrashSyncModeChange(TrashSyncMode? mode) async {
if (mode == null) {
return;
}
switch (mode) {
case TrashSyncMode.none:
if (!autoSyncChanges.value && !reviewOutOfSyncChanges.value) {
break;
}
autoSyncChanges.value = false;
reviewOutOfSyncChanges.value = false;
ref.invalidate(appSettingsServiceProvider);
break;
case TrashSyncMode.auto:
if (autoSyncChanges.value) {
break;
}
await attemptToEnableSetting(AppSettingsEnum.manageLocalMediaAndroid);
break;
case TrashSyncMode.review:
if (reviewOutOfSyncChanges.value) {
break;
}
await attemptToEnableSetting(AppSettingsEnum.reviewOutOfSyncChangesAndroid);
break;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "advanced_settings_sync_remote_deletions_selector_title".tr()),
SettingsRadioListTile(
groups: [
SettingsRadioGroup(
title: 'off'.tr(),
subtitle: 'advanced_settings_sync_remote_deletions_off_subtitle'.tr(),
value: TrashSyncMode.none,
),
SettingsRadioGroup(
title: 'advanced_settings_sync_remote_deletions_title'.tr(),
subtitle: 'advanced_settings_sync_remote_deletions_subtitle'.tr(),
value: TrashSyncMode.auto,
),
SettingsRadioGroup(
title: 'advanced_settings_review_remote_deletions_title'.tr(),
subtitle: 'advanced_settings_review_remote_deletions_subtitle'.tr(),
value: TrashSyncMode.review,
),
],
groupBy: selectedTrashSyncMode,
onRadioChanged: (mode) => handleTrashSyncModeChange(mode),
),
SettingsActionTile(
title: "manage_media_access_title".tr(),
statusText: manageMediaAndroidPermissionValue == null
? null
: manageMediaAndroidPermissionValue == true
? "allowed".tr()
: "not_allowed".tr(),
subtitle: "manage_media_access_rationale".tr(),
statusColor:
manageMediaAndroidPermissionValue == false && (autoSyncChanges.value || reviewOutOfSyncChanges.value)
? const Color.fromARGB(255, 243, 188, 106)
: null,
onActionTap: () async {
await ref.read(localFilesManagerRepositoryProvider).manageMediaPermission();
ref.invalidate(manageMediaPermissionProvider);
},
),
],
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.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';
@ -366,8 +367,10 @@ class _SyncStatsCounts extends ConsumerWidget {
), ),
), ),
// To be removed once the experimental feature is stable // To be removed once the experimental feature is stable
if (CurrentPlatform.isAndroid && if ((kDebugMode || kProfileMode) &&
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[ CurrentPlatform.isAndroid &&
(appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid) ||
appSettingsService.getSetting<bool>(AppSettingsEnum.reviewOutOfSyncChangesAndroid))) ...[
_SectionHeaderText(text: "trash".t(context: context)), _SectionHeaderText(text: "trash".t(context: context)),
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
class SettingsRadioGroup<T> { class SettingsRadioGroup<T> {
final String title; final String title;
final String? subtitle;
final T value; final T value;
const SettingsRadioGroup({required this.title, required this.value}); const SettingsRadioGroup({required this.title, this.subtitle, required this.value});
} }
class SettingsRadioListTile<T> extends StatelessWidget { class SettingsRadioListTile<T> extends StatelessWidget {
@ -28,6 +30,12 @@ class SettingsRadioListTile<T> extends StatelessWidget {
dense: true, dense: true,
activeColor: context.primaryColor, activeColor: context.primaryColor,
title: Text(g.title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)), title: Text(g.title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)),
subtitle: g.subtitle != null
? Text(
g.subtitle!,
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
)
: null,
value: g.value, value: g.value,
controlAffinity: ListTileControlAffinity.trailing, controlAffinity: ListTileControlAffinity.trailing,
), ),

View File

@ -11,6 +11,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
@ -26,6 +27,7 @@ void main() {
late LocalSyncService sut; late LocalSyncService sut;
late DriftLocalAlbumRepository mockLocalAlbumRepository; late DriftLocalAlbumRepository mockLocalAlbumRepository;
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository; late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository;
late DriftTrashSyncRepository mockTrashSyncRepo;
late LocalFilesManagerRepository mockLocalFilesManager; late LocalFilesManagerRepository mockLocalFilesManager;
late StorageRepository mockStorageRepository; late StorageRepository mockStorageRepository;
late MockNativeSyncApi mockNativeSyncApi; late MockNativeSyncApi mockNativeSyncApi;
@ -48,6 +50,7 @@ void main() {
setUp(() async { setUp(() async {
mockLocalAlbumRepository = MockLocalAlbumRepository(); mockLocalAlbumRepository = MockLocalAlbumRepository();
mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository(); mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository();
mockTrashSyncRepo = MockTrashSyncRepository();
mockLocalFilesManager = MockLocalFilesManagerRepository(); mockLocalFilesManager = MockLocalFilesManagerRepository();
mockStorageRepository = MockStorageRepository(); mockStorageRepository = MockStorageRepository();
mockNativeSyncApi = MockNativeSyncApi(); mockNativeSyncApi = MockNativeSyncApi();
@ -70,6 +73,7 @@ void main() {
localFilesManager: mockLocalFilesManager, localFilesManager: mockLocalFilesManager,
storageRepository: mockStorageRepository, storageRepository: mockStorageRepository,
nativeSyncApi: mockNativeSyncApi, nativeSyncApi: mockNativeSyncApi,
trashSyncRepository: mockTrashSyncRepo,
); );
await Store.put(StoreKey.manageLocalMediaAndroid, false); await Store.put(StoreKey.manageLocalMediaAndroid, false);

View File

@ -16,6 +16,7 @@ import 'package:immich_mobile/infrastructure/repositories/storage.repository.dar
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -48,6 +49,7 @@ void main() {
late SyncApiRepository mockSyncApiRepo; late SyncApiRepository mockSyncApiRepo;
late DriftLocalAssetRepository mockLocalAssetRepo; late DriftLocalAssetRepository mockLocalAssetRepo;
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo; late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
late DriftTrashSyncRepository mockTrashSyncRepo;
late LocalFilesManagerRepository mockLocalFilesManagerRepo; late LocalFilesManagerRepository mockLocalFilesManagerRepo;
late StorageRepository mockStorageRepo; late StorageRepository mockStorageRepo;
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback; late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
@ -79,6 +81,7 @@ void main() {
mockLocalAssetRepo = MockLocalAssetRepository(); mockLocalAssetRepo = MockLocalAssetRepository();
mockTrashedLocalAssetRepo = MockTrashedLocalAssetRepository(); mockTrashedLocalAssetRepo = MockTrashedLocalAssetRepository();
mockLocalFilesManagerRepo = MockLocalFilesManagerRepository(); mockLocalFilesManagerRepo = MockLocalFilesManagerRepository();
mockTrashSyncRepo = MockTrashSyncRepository();
mockStorageRepo = MockStorageRepository(); mockStorageRepo = MockStorageRepository();
mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
mockResetCallbackWrapper = _MockAbortCallbackWrapper(); mockResetCallbackWrapper = _MockAbortCallbackWrapper();
@ -135,9 +138,10 @@ void main() {
trashedLocalAssetRepository: mockTrashedLocalAssetRepo, trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
localFilesManager: mockLocalFilesManagerRepo, localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo, storageRepository: mockStorageRepo,
trashSyncRepository: mockTrashSyncRepo,
); );
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {}); when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>())).thenAnswer((_) async => {});
when(() => mockTrashedLocalAssetRepo.trashLocalAsset(any())).thenAnswer((_) async {}); when(() => mockTrashedLocalAssetRepo.trashLocalAsset(any())).thenAnswer((_) async {});
when(() => mockTrashedLocalAssetRepo.getToRestore()).thenAnswer((_) async => []); when(() => mockTrashedLocalAssetRepo.getToRestore()).thenAnswer((_) async => []);
when(() => mockTrashedLocalAssetRepo.applyRestoredAssets(any())).thenAnswer((_) async {}); when(() => mockTrashedLocalAssetRepo.applyRestoredAssets(any())).thenAnswer((_) async {});
@ -216,6 +220,7 @@ void main() {
localFilesManager: mockLocalFilesManagerRepo, localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo, storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call, cancelChecker: cancellationChecker.call,
trashSyncRepository: mockTrashSyncRepo,
); );
await sut.sync(); await sut.sync();
@ -255,6 +260,7 @@ void main() {
localFilesManager: mockLocalFilesManagerRepo, localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo, storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call, cancelChecker: cancellationChecker.call,
trashSyncRepository: mockTrashSyncRepo,
); );
await sut.sync(); await sut.sync();
@ -386,9 +392,16 @@ void main() {
'album-a': [localAsset], 'album-a': [localAsset],
'album-b': [mergedAsset], 'album-b': [mergedAsset],
}; };
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async { when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>())).thenAnswer((invocation) async {
final Iterable<String> requestedChecksums = invocation.positionalArguments.first as Iterable<String>; final Map<String, DateTime> trashedAssetsMap = invocation.positionalArguments.first as Map<String, DateTime>;
expect(requestedChecksums.toSet(), equals({'checksum-local', 'checksum-merged', 'checksum-remote-only'})); expect(
trashedAssetsMap,
equals({
localAsset.checksum!: DateTime(2025, 5, 1),
mergedAsset.checksum!: DateTime(2025, 5, 2),
'checksum-remote-only': DateTime(2025, 5, 3),
}),
);
return assetsByAlbum; return assetsByAlbum;
}); });
@ -445,7 +458,7 @@ void main() {
await simulateEvents(events); await simulateEvents(events);
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).called(1); verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>())).called(1);
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any())); verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any())); verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
}); });
@ -455,7 +468,7 @@ void main() {
await simulateEvents(events); await simulateEvents(events);
verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())); verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>()));
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any())); verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1); verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
}); });
@ -474,11 +487,7 @@ void main() {
}); });
final events = [ final events = [
SyncStreamStub.assetModified( SyncStreamStub.assetModified(id: 'remote-1', checksum: 'checksum-trash', ack: 'asset-remote-1-11'),
id: 'remote-1',
checksum: 'checksum-trash',
ack: 'asset-remote-1-11',
),
]; ];
await simulateEvents(events); await simulateEvents(events);

View File

@ -17,6 +17,7 @@ import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12; import 'schema_v12.dart' as v12;
import 'schema_v13.dart' as v13; import 'schema_v13.dart' as v13;
import 'schema_v14.dart' as v14; import 'schema_v14.dart' as v14;
import 'schema_v15.dart' as v15;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -50,10 +51,28 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v13.DatabaseAtV13(db); return v13.DatabaseAtV13(db);
case 14: case 14:
return v14.DatabaseAtV14(db); return v14.DatabaseAtV14(db);
case 15:
return v15.DatabaseAtV15(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; static const versions = const [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
];
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/repositories/storage.repository.dar
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trash_sync.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.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.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
@ -40,6 +41,8 @@ class MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {}
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {} class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
class MockTrashSyncRepository extends Mock implements DriftTrashSyncRepository {}
class MockStorageRepository extends Mock implements StorageRepository {} class MockStorageRepository extends Mock implements StorageRepository {}
class MockDriftBackupRepository extends Mock implements DriftBackupRepository {} class MockDriftBackupRepository extends Mock implements DriftBackupRepository {}

View File

@ -83,6 +83,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -114,6 +115,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -130,6 +132,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -149,6 +152,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -166,6 +170,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -183,6 +188,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -202,6 +208,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -219,6 +226,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -236,6 +244,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -253,6 +262,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -270,6 +280,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -289,6 +300,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -306,6 +318,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -323,6 +336,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -342,6 +356,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -359,6 +374,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -376,6 +392,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -395,6 +412,7 @@ void main() {
isStacked: false, isStacked: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -412,6 +430,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
isStacked: false, isStacked: false,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -431,6 +450,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -448,6 +468,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -467,6 +488,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -484,6 +506,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -503,6 +526,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -522,6 +546,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -541,6 +566,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -558,6 +584,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -574,6 +601,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -593,6 +621,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -612,6 +641,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -628,6 +658,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -647,6 +678,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -664,6 +696,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -681,6 +714,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -697,6 +731,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -715,6 +750,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: true, advancedTroubleshooting: true,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -731,6 +767,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -751,6 +788,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: true, isStacked: true,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -768,6 +806,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -785,6 +824,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -807,6 +847,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
}); });
@ -826,6 +867,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
final widget = buttonType.buildButton(contextWithAlbum); final widget = buttonType.buildButton(contextWithAlbum);
@ -840,6 +882,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
final widget = buttonType.buildButton(contextWithAlbum); final widget = buttonType.buildButton(contextWithAlbum);
@ -855,6 +898,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: true, isStacked: true,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
final widget = buttonType.buildButton(contextWithAlbum); final widget = buttonType.buildButton(contextWithAlbum);
@ -879,6 +923,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -900,6 +945,7 @@ void main() {
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -919,6 +965,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -939,6 +986,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@ -953,6 +1001,7 @@ void main() {
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false, isStacked: false,
isWaitingForTrashApproval: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );