Merge 30139d13f2 into 5ade152bc5
commit
9a9c4cd9e9
20
i18n/en.json
20
i18n/en.json
|
|
@ -440,10 +440,14 @@
|
|||
"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_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_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_title": "Sync remote deletions [EXPERIMENTAL]",
|
||||
"advanced_settings_sync_remote_deletions_off_subtitle": "Cloud trash changes are ignored",
|
||||
"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_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||
|
|
@ -542,6 +546,12 @@
|
|||
"asset_list_settings_title": "Photo Grid",
|
||||
"asset_offline": "Asset Offline",
|
||||
"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_skipped": "Skipped",
|
||||
"asset_skipped_in_trash": "In trash",
|
||||
|
|
@ -555,11 +565,13 @@
|
|||
"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_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_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
||||
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
||||
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
|
||||
"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_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",
|
||||
|
|
@ -1249,6 +1261,8 @@
|
|||
"jobs": "Jobs",
|
||||
"keep": "Keep",
|
||||
"keep_all": "Keep All",
|
||||
"keep_in_trash": "Keep in trash",
|
||||
"keep_on_device": "Keep on device",
|
||||
"keep_this_delete_others": "Keep this, delete others",
|
||||
"kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||
|
|
@ -1499,6 +1513,7 @@
|
|||
"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",
|
||||
"ocr": "OCR",
|
||||
"off": "Off",
|
||||
"official_immich_resources": "Official Immich Resources",
|
||||
"offline": "Offline",
|
||||
"offset": "Offset",
|
||||
|
|
@ -1758,6 +1773,7 @@
|
|||
"retry_upload": "Retry upload",
|
||||
"review_duplicates": "Review duplicates",
|
||||
"review_large_files": "Review large files",
|
||||
"review_out_of_sync_changes": "Review out-of-sync changes",
|
||||
"role": "Role",
|
||||
"role_editor": "Editor",
|
||||
"role_viewer": "Viewer",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -17,6 +17,7 @@ sealed class BaseAsset {
|
|||
final AssetType type;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? durationInSeconds;
|
||||
|
|
@ -29,6 +30,7 @@ sealed class BaseAsset {
|
|||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.width,
|
||||
this.height,
|
||||
this.durationInSeconds,
|
||||
|
|
@ -67,6 +69,7 @@ sealed class BaseAsset {
|
|||
type: $type,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
deletedAt: $deletedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
|
|
@ -82,6 +85,7 @@ sealed class BaseAsset {
|
|||
type == other.type &&
|
||||
createdAt == other.createdAt &&
|
||||
updatedAt == other.updatedAt &&
|
||||
deletedAt == other.deletedAt &&
|
||||
width == other.width &&
|
||||
height == other.height &&
|
||||
durationInSeconds == other.durationInSeconds &&
|
||||
|
|
@ -96,6 +100,7 @@ sealed class BaseAsset {
|
|||
type.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
deletedAt.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
durationInSeconds.hashCode ^
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class LocalAsset extends BaseAsset {
|
|||
required super.type,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
super.deletedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
|
|
@ -50,15 +51,16 @@ class LocalAsset extends BaseAsset {
|
|||
type: $type,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
deletedAt: $deletedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
remoteId: ${remoteId ?? "<NA>"}
|
||||
isFavorite: $isFavorite,
|
||||
orientation: $orientation,
|
||||
adjustmentTime: $adjustmentTime,
|
||||
latitude: ${latitude ?? "<NA>"},
|
||||
longitude: ${longitude ?? "<NA>"},
|
||||
orientation: $orientation,
|
||||
adjustmentTime: $adjustmentTime,
|
||||
latitude: ${latitude ?? "<NA>"},
|
||||
longitude: ${longitude ?? "<NA>"},
|
||||
}''';
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +100,7 @@ class LocalAsset extends BaseAsset {
|
|||
int? durationInSeconds,
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
DateTime? deletedAt,
|
||||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
|
|
@ -118,6 +121,7 @@ class LocalAsset extends BaseAsset {
|
|||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class RemoteAsset extends BaseAsset {
|
|||
required super.type,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
super.deletedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
|
|
@ -51,6 +52,7 @@ class RemoteAsset extends BaseAsset {
|
|||
type: $type,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
deletedAt: ${deletedAt ?? "<NA>"},
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
|
|
@ -104,6 +106,7 @@ class RemoteAsset extends BaseAsset {
|
|||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
DateTime? deletedAt,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -122,6 +125,7 @@ class RemoteAsset extends BaseAsset {
|
|||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ enum StoreKey<T> {
|
|||
useWifiForUploadPhotos<bool>._(1005),
|
||||
needBetaMigration<bool>._(1006),
|
||||
// TODO: Remove this after patching open-api
|
||||
shouldResetSync<bool>._(1007);
|
||||
shouldResetSync<bool>._(1007),
|
||||
reviewOutOfSyncChangesAndroid<bool>._(1008);
|
||||
|
||||
const StoreKey._(this.id);
|
||||
final int id;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/entities/store.entity.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/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/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
|
|
@ -20,6 +21,7 @@ class LocalSyncService {
|
|||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final DriftTrashSyncRepository _trashSyncRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
final StorageRepository _storageRepository;
|
||||
final Logger _log = Logger("DeviceSyncService");
|
||||
|
|
@ -27,11 +29,13 @@ class LocalSyncService {
|
|||
LocalSyncService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required DriftTrashSyncRepository trashSyncRepository,
|
||||
required LocalFilesManagerRepository localFilesManager,
|
||||
required StorageRepository storageRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_trashSyncRepository = trashSyncRepository,
|
||||
_localFilesManager = localFilesManager,
|
||||
_storageRepository = storageRepository,
|
||||
_nativeSyncApi = nativeSyncApi;
|
||||
|
|
@ -39,7 +43,8 @@ class LocalSyncService {
|
|||
Future<void> sync({bool full = false}) async {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
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();
|
||||
if (hasPermission) {
|
||||
await _syncTrashedAssets();
|
||||
|
|
@ -333,22 +338,34 @@ class LocalSyncService {
|
|||
} else {
|
||||
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
||||
}
|
||||
|
||||
final reviewMode = Store.get(StoreKey.reviewOutOfSyncChangesAndroid, false);
|
||||
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
final mediaUrls = await Future.wait(
|
||||
localAssetsToTrash.values
|
||||
.expand((e) => e)
|
||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
||||
);
|
||||
_log.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
if (result) {
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
||||
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(
|
||||
localAssetsToTrash.values
|
||||
.expand((e) => e)
|
||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
||||
);
|
||||
_log.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
if (result) {
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/sync_event.model.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/sync_api.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/repositories/local_files_manager.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -20,6 +22,7 @@ class SyncStreamService {
|
|||
final SyncStreamRepository _syncStreamRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final DriftTrashSyncRepository _trashSyncRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
final StorageRepository _storageRepository;
|
||||
final bool Function()? _cancelChecker;
|
||||
|
|
@ -29,6 +32,7 @@ class SyncStreamService {
|
|||
required SyncStreamRepository syncStreamRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required DriftTrashSyncRepository trashSyncRepository,
|
||||
required LocalFilesManagerRepository localFilesManager,
|
||||
required StorageRepository storageRepository,
|
||||
bool Function()? cancelChecker,
|
||||
|
|
@ -36,6 +40,7 @@ class SyncStreamService {
|
|||
_syncStreamRepository = syncStreamRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_trashSyncRepository = trashSyncRepository,
|
||||
_localFilesManager = localFilesManager,
|
||||
_storageRepository = storageRepository,
|
||||
_cancelChecker = cancelChecker;
|
||||
|
|
@ -104,11 +109,21 @@ class SyncStreamService {
|
|||
case SyncEntityType.assetV1:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||
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();
|
||||
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();
|
||||
if (reviewMode) {
|
||||
await _trashSyncRepository.deleteOutdated();
|
||||
final result = await _trashSyncRepository.deleteOutdated();
|
||||
_logger.info("syncTrashedAssets, outdated deleted: $result");
|
||||
}
|
||||
} else {
|
||||
_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 {
|
||||
if (checksums.isEmpty) {
|
||||
Future<void> _handleRemoteTrashed(Map<String, DateTime> trashedAssetsMap, bool reviewMode) async {
|
||||
if (trashedAssetsMap.isEmpty) {
|
||||
return Future.value();
|
||||
} else {
|
||||
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
|
||||
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(trashedAssetsMap);
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
final mediaUrls = await Future.wait(
|
||||
localAssetsToTrash.values
|
||||
.expand((e) => e)
|
||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
||||
);
|
||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
if (result) {
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
||||
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(
|
||||
localAssetsToTrash.values
|
||||
.expand((e) => e)
|
||||
.map(
|
||||
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
|
||||
),
|
||||
);
|
||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
if (result) {
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
||||
}
|
||||
}
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ enum TimelineOrigin {
|
|||
search,
|
||||
deepLink,
|
||||
albumActivities,
|
||||
syncTrash,
|
||||
}
|
||||
|
||||
class TimelineFactory {
|
||||
|
|
@ -65,6 +66,8 @@ class TimelineFactory {
|
|||
|
||||
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 lockedFolder(String userId) => TimelineService(_timelineRepository.locked(userId, groupBy));
|
||||
|
|
@ -93,6 +96,7 @@ class TimelineService {
|
|||
StreamSubscription? _bucketSubscription;
|
||||
|
||||
int _totalAssets = 0;
|
||||
|
||||
int get totalAssets => _totalAssets;
|
||||
|
||||
TimelineService(TimelineQuery query)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -3,8 +3,9 @@ import 'stack.entity.dart';
|
|||
import 'local_asset.entity.dart';
|
||||
import 'local_album.entity.dart';
|
||||
import 'local_album_asset.entity.dart';
|
||||
import 'trash_sync.entity.dart';
|
||||
|
||||
mergedAsset:
|
||||
mergedAsset:
|
||||
SELECT
|
||||
rae.id as remote_id,
|
||||
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
|
||||
|
|
@ -12,6 +13,7 @@ SELECT
|
|||
rae."type",
|
||||
rae.created_at as created_at,
|
||||
rae.updated_at,
|
||||
rae.deleted_at,
|
||||
rae.width,
|
||||
rae.height,
|
||||
rae.duration_in_seconds,
|
||||
|
|
@ -21,19 +23,31 @@ SELECT
|
|||
rae.owner_id,
|
||||
rae.live_photo_video_id,
|
||||
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
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
stack_entity se ON rae.stack_id = se.id
|
||||
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.owner_id IN :user_ids
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
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
|
||||
|
||||
|
|
@ -44,6 +58,7 @@ SELECT
|
|||
lae."type",
|
||||
lae.created_at as created_at,
|
||||
lae.updated_at,
|
||||
NULL as deleted_at,
|
||||
lae.width,
|
||||
lae.height,
|
||||
lae.duration_in_seconds,
|
||||
|
|
@ -53,7 +68,8 @@ SELECT
|
|||
NULL as owner_id,
|
||||
NULL as live_photo_video_id,
|
||||
lae.orientation,
|
||||
NULL as stack_id
|
||||
NULL as stack_id,
|
||||
false as sync_rejected
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
|
@ -88,13 +104,21 @@ FROM
|
|||
LEFT JOIN
|
||||
stack_entity se ON rae.stack_id = se.id
|
||||
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.owner_id in :user_ids
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
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
|
||||
SELECT
|
||||
lae.created_at
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
|||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
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;
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i8;
|
||||
|
||||
class MergedAssetDrift extends i1.ModularAccessor {
|
||||
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||
|
|
@ -29,7 +31,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
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: [
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables,
|
||||
|
|
@ -37,6 +39,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
trashSyncEntity,
|
||||
stackEntity,
|
||||
localAlbumAssetEntity,
|
||||
localAlbumEntity,
|
||||
|
|
@ -52,6 +55,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
),
|
||||
createdAt: row.read<DateTime>('created_at'),
|
||||
updatedAt: row.read<DateTime>('updated_at'),
|
||||
deletedAt: row.readNullable<DateTime>('deleted_at'),
|
||||
width: row.readNullable<int>('width'),
|
||||
height: row.readNullable<int>('height'),
|
||||
durationInSeconds: row.readNullable<int>('duration_in_seconds'),
|
||||
|
|
@ -62,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
|
||||
orientation: row.read<int>('orientation'),
|
||||
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);
|
||||
$arrayStartIndex += userIds.length;
|
||||
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: [
|
||||
i0.Variable<int>(groupBy),
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
|
|
@ -83,6 +88,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
trashSyncEntity,
|
||||
localAlbumAssetEntity,
|
||||
localAlbumEntity,
|
||||
},
|
||||
|
|
@ -103,13 +109,16 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||
i3.$LocalAssetEntityTable get localAssetEntity => i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).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(
|
||||
attachedDatabase,
|
||||
).resultSet<i6.$LocalAlbumAssetEntityTable>('local_album_asset_entity');
|
||||
i7.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer(
|
||||
).resultSet<i7.$LocalAlbumAssetEntityTable>('local_album_asset_entity');
|
||||
i8.$LocalAlbumEntityTable get localAlbumEntity => i1.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i7.$LocalAlbumEntityTable>('local_album_entity');
|
||||
).resultSet<i8.$LocalAlbumEntityTable>('local_album_entity');
|
||||
}
|
||||
|
||||
class MergedAssetResult {
|
||||
|
|
@ -119,6 +128,7 @@ class MergedAssetResult {
|
|||
final i2.AssetType type;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? durationInSeconds;
|
||||
|
|
@ -129,6 +139,7 @@ class MergedAssetResult {
|
|||
final String? livePhotoVideoId;
|
||||
final int orientation;
|
||||
final String? stackId;
|
||||
final bool syncRejected;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
|
|
@ -136,6 +147,7 @@ class MergedAssetResult {
|
|||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.width,
|
||||
this.height,
|
||||
this.durationInSeconds,
|
||||
|
|
@ -146,6 +158,7 @@ class MergedAssetResult {
|
|||
this.livePhotoVideoId,
|
||||
required this.orientation,
|
||||
this.stackId,
|
||||
required this.syncRejected,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)',
|
||||
);
|
||||
|
|
@ -95,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 14;
|
||||
int get schemaVersion => 15;
|
||||
|
||||
@override
|
||||
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.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);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,39 +9,41 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
|||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
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;
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
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;
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
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;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
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 {
|
||||
$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 i4.$LocalAssetEntityTable localAssetEntity = i4
|
||||
.$LocalAssetEntityTable(this);
|
||||
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5
|
||||
late final i5.$TrashSyncEntityTable trashSyncEntity = i5
|
||||
.$TrashSyncEntityTable(this);
|
||||
late final i6.$RemoteAlbumEntityTable remoteAlbumEntity = i6
|
||||
.$RemoteAlbumEntityTable(this);
|
||||
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6
|
||||
late final i7.$LocalAlbumEntityTable localAlbumEntity = i7
|
||||
.$LocalAlbumEntityTable(this);
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
|
||||
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i8
|
||||
.$LocalAlbumAssetEntityTable(this);
|
||||
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable(
|
||||
late final i9.$AuthUserEntityTable authUserEntity = i9.$AuthUserEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i9.$UserMetadataEntityTable userMetadataEntity = i9
|
||||
late final i10.$UserMetadataEntityTable userMetadataEntity = i10
|
||||
.$UserMetadataEntityTable(this);
|
||||
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
|
||||
late final i11.$PartnerEntityTable partnerEntity = i11.$PartnerEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
|
||||
late final i12.$RemoteExifEntityTable remoteExifEntity = i12
|
||||
.$RemoteExifEntityTable(this);
|
||||
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12
|
||||
late final i13.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i13
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
|
||||
late final i14.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i14
|
||||
.$RemoteAlbumUserEntityTable(this);
|
||||
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
|
||||
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
|
||||
late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this);
|
||||
late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16
|
||||
.$MemoryAssetEntityTable(this);
|
||||
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
|
||||
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
|
||||
late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this);
|
||||
late final i18.$AssetFaceEntityTable assetFaceEntity = i18
|
||||
.$AssetFaceEntityTable(this);
|
||||
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
|
||||
late final i19.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i19
|
||||
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
|
||||
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer(
|
||||
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new);
|
||||
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
|
|
@ -93,9 +97,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
trashSyncEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
i5.idxTrashSyncChecksum,
|
||||
i5.idxTrashSyncStatus,
|
||||
i5.idxTrashSyncChecksumStatus,
|
||||
i4.idxLocalAssetChecksum,
|
||||
i2.idxRemoteAssetOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerChecksum,
|
||||
|
|
@ -113,9 +121,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
i11.idxLatLng,
|
||||
i19.idxTrashedLocalAssetChecksum,
|
||||
i19.idxTrashedLocalAssetAlbum,
|
||||
i12.idxLatLng,
|
||||
i20.idxTrashedLocalAssetChecksum,
|
||||
i20.idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
|
|
@ -312,39 +320,41 @@ class $DriftManager {
|
|||
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
i5.$$TrashSyncEntityTableTableManager get trashSyncEntity =>
|
||||
i5.$$TrashSyncEntityTableTableManager(_db, _db.trashSyncEntity);
|
||||
i6.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i6.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i8.$$AuthUserEntityTableTableManager get authUserEntity =>
|
||||
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
|
||||
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i10.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
i9.$$AuthUserEntityTableTableManager get authUserEntity =>
|
||||
i9.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
|
||||
i10.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i10.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i11.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i11.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i12.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i12.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i13.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i13.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.remoteAlbumAssetEntity,
|
||||
);
|
||||
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
|
||||
i14.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i14
|
||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||
i14.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i16.$$PersonEntityTableTableManager get personEntity =>
|
||||
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i18.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager(
|
||||
i15.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i17.$$PersonEntityTableTableManager get personEntity =>
|
||||
i17.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i18.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i19.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.trashedLocalAssetEntity,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5941,6 +5941,470 @@ i1.GeneratedColumn<DateTime> _column_96(String aliasedName) =>
|
|||
true,
|
||||
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({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
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, Schema13 schema) from12To13,
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -6023,6 +6488,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from13To14(migrator, schema);
|
||||
return 14;
|
||||
case 14:
|
||||
final schema = Schema15(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from14To15(migrator, schema);
|
||||
return 15;
|
||||
default:
|
||||
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, Schema13 schema) from12To13,
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
|
|
@ -6058,5 +6529,6 @@ i1.OnUpgrade stepByStep({
|
|||
from11To12: from11To12,
|
||||
from12To13: from12To13,
|
||||
from13To14: from13To14,
|
||||
from14To15: from14To15,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -99,14 +99,14 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
}
|
||||
|
||||
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
|
||||
if (checksums.isEmpty) {
|
||||
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Map<String, DateTime> trashedAssetsMap) async {
|
||||
if (trashedAssetsMap.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
final result = <String, List<LocalAsset>>{};
|
||||
|
||||
for (final slice in checksums.toSet().slices(kDriftMaxChunk)) {
|
||||
for (final slice in trashedAssetsMap.keys.toSet().slices(kDriftMaxChunk)) {
|
||||
final rows =
|
||||
await (_db.select(_db.localAlbumAssetEntity).join([
|
||||
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
|
||||
|
|
@ -120,10 +120,16 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||
for (final row in rows) {
|
||||
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||
final assetData = row.readTable(_db.localAssetEntity);
|
||||
final asset = assetData.toDto();
|
||||
final asset = assetData.toDto().copyWith(deletedAt: trashedAssetsMap[assetData.checksum]);
|
||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,4 +258,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||
Future<int> getCount() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
return _db.mergedAssetDrift
|
||||
.mergedAsset(userIds: userIds, limit: (_) => Limit(count, offset))
|
||||
.map(
|
||||
(row) => row.remoteId != null && row.ownerId != null
|
||||
(row) => row.remoteId != null && row.ownerId != null && !row.syncRejected
|
||||
? RemoteAsset(
|
||||
id: row.remoteId!,
|
||||
localId: row.localId,
|
||||
|
|
@ -63,6 +63,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
type: row.type,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
deletedAt: row.deletedAt,
|
||||
thumbHash: row.thumbHash,
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
|
|
@ -73,12 +74,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
)
|
||||
: LocalAsset(
|
||||
id: row.localId!,
|
||||
remoteId: row.remoteId,
|
||||
remoteId: row.syncRejected ? null : row.remoteId,
|
||||
name: row.name,
|
||||
checksum: row.checksum,
|
||||
type: row.type,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
deletedAt: row.deletedAt,
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
isFavorite: row.isFavorite,
|
||||
|
|
@ -277,6 +279,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
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(
|
||||
filter: (row) =>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +224,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
|
||||
for (final row in rows) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,7 +457,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
}
|
||||
|
||||
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 newAsset = await timelineService.getAssetAsync(index);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.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/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_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/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/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/providers/infrastructure/asset_viewer/current_asset.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/user.provider.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 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) {
|
||||
opacity = 0;
|
||||
}
|
||||
|
|
@ -41,17 +51,23 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
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),
|
||||
|
||||
if (!isInLockedView) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
if (!isInLockedView) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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/current_album.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/user.provider.dart';
|
||||
|
||||
|
|
@ -49,6 +50,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final isWaitingForSyncApproval = ref.watch(isWaitingForSyncApprovalProvider(asset.checksum)).value == true;
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
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),
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite && !isWaitingForSyncApproval)
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
|
|
|
|||
|
|
@ -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/setting.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/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
|
@ -35,6 +36,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final isWaitingForTrashApproval = ref.watch(isWaitingForSyncApprovalProvider(asset.checksum)).value == true;
|
||||
|
||||
final actionContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
|
|
@ -49,6 +51,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||
isCasting: isCasting,
|
||||
timelineOrigin: timelineOrigin,
|
||||
originalTheme: originalTheme,
|
||||
isWaitingForTrashApproval: isWaitingForTrashApproval,
|
||||
);
|
||||
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,11 +88,15 @@ class ThumbnailTile extends ConsumerWidget {
|
|||
child: _TileOverlayIcon(Icons.cloud_outlined),
|
||||
),
|
||||
),
|
||||
AssetState.merged => const Align(
|
||||
AssetState.merged => Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.cloud_done_outlined),
|
||||
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
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 {
|
||||
final Duration duration;
|
||||
|
||||
const _VideoIndicator(this.duration);
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -6,3 +6,8 @@ part 'app_settings.provider.g.dart';
|
|||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppSettingsService appSettingsService(Ref _) => const AppSettingsService();
|
||||
|
||||
final appSettingStreamProvider = StreamProvider.family.autoDispose<bool, AppSettingsEnum<bool>>((ref, setting) {
|
||||
final service = ref.watch(appSettingsServiceProvider);
|
||||
return service.watchSetting(setting);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -421,6 +421,22 @@ class ActionNotifier extends Notifier<void> {
|
|||
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> {
|
||||
|
|
|
|||
|
|
@ -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/platform.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';
|
||||
|
||||
final syncStreamServiceProvider = Provider(
|
||||
|
|
@ -19,6 +20,7 @@ final syncStreamServiceProvider = Provider(
|
|||
syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
|
||||
localAssetRepository: ref.watch(localAssetRepository),
|
||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||
trashSyncRepository: ref.watch(trashSyncRepositoryProvider),
|
||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||
storageRepository: ref.watch(storageRepositoryProvider),
|
||||
cancelChecker: ref.watch(cancellationProvider),
|
||||
|
|
@ -33,6 +35,7 @@ final localSyncServiceProvider = Provider(
|
|||
(ref) => LocalSyncService(
|
||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||
trashSyncRepository: ref.watch(trashSyncRepositoryProvider),
|
||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||
storageRepository: ref.watch(storageRepositoryProvider),
|
||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,45 @@
|
|||
import 'package:async/async.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/services/app_settings.service.dart';
|
||||
|
||||
import 'db.provider.dart';
|
||||
|
||||
typedef TrashedAssetsCount = ({int total, int hashed});
|
||||
|
||||
final trashSyncRepositoryProvider = Provider<DriftTrashSyncRepository>(
|
||||
(ref) => DriftTrashSyncRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
||||
final trashedAssetsCountProvider = StreamProvider<TrashedAssetsCount>((ref) {
|
||||
final repo = ref.watch(trashedLocalAssetRepository);
|
||||
final total$ = repo.watchCount();
|
||||
final hashed$ = repo.watchHashedCount();
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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_trash.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/editing/drift_crop.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: DriftFavoriteRoute.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: DriftLockedFolderRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftVideoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// [DriftUploadDetailPage]
|
||||
class DriftUploadDetailRoute extends PageRouteInfo<void> {
|
||||
|
|
|
|||
|
|
@ -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/remote_album.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/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_media.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/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_mobile/widgets/common/date_time_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:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
|
|
@ -28,8 +34,12 @@ final actionServiceProvider = Provider<ActionService>(
|
|||
ref.watch(localAssetRepository),
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(trashSyncRepositoryProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
ref.watch(downloadRepositoryProvider),
|
||||
ref.watch(storageRepositoryProvider),
|
||||
ref.watch(localFilesManagerRepositoryProvider),
|
||||
Logger('ActionService'),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -39,8 +49,12 @@ class ActionService {
|
|||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftTrashSyncRepository _trashSyncRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final DownloadRepository _downloadRepository;
|
||||
final StorageRepository _storageRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
final Logger _logger;
|
||||
|
||||
const ActionService(
|
||||
this._assetApiRepository,
|
||||
|
|
@ -48,8 +62,12 @@ class ActionService {
|
|||
this._localAssetRepository,
|
||||
this._albumApiRepository,
|
||||
this._remoteAlbumRepository,
|
||||
this._trashSyncRepository,
|
||||
this._assetMediaRepository,
|
||||
this._downloadRepository,
|
||||
this._storageRepository,
|
||||
this._localFilesManager,
|
||||
this._logger,
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
|
|
@ -242,4 +260,28 @@ class ActionService {
|
|||
Future<List<bool>> downloadAll(List<RemoteAsset> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ enum AppSettingsEnum<T> {
|
|||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
reviewOutOfSyncChangesAndroid<bool>(StoreKey.reviewOutOfSyncChangesAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
|
|
@ -72,4 +73,11 @@ class AppSettingsService {
|
|||
Future<void> setSetting<T>(AppSettingsEnum<T> setting, T 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class ActionButtonContext {
|
|||
final bool isStacked;
|
||||
final RemoteAlbum? currentAlbum;
|
||||
final bool advancedTroubleshooting;
|
||||
final bool isWaitingForTrashApproval;
|
||||
final ActionSource source;
|
||||
final bool isCasting;
|
||||
final TimelineOrigin timelineOrigin;
|
||||
|
|
@ -52,6 +53,7 @@ class ActionButtonContext {
|
|||
required this.isInLockedView,
|
||||
required this.currentAlbum,
|
||||
required this.advancedTroubleshooting,
|
||||
required this.isWaitingForTrashApproval,
|
||||
required this.source,
|
||||
this.isCasting = false,
|
||||
this.timelineOrigin = TimelineOrigin.main,
|
||||
|
|
@ -92,7 +94,8 @@ enum ActionButtonType {
|
|||
context.isOwner && //
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote && //
|
||||
!context.isArchived,
|
||||
!context.isArchived &&
|
||||
!context.isWaitingForTrashApproval,
|
||||
ActionButtonType.unarchive =>
|
||||
context.isOwner && //
|
||||
!context.isInLockedView && //
|
||||
|
|
@ -106,27 +109,31 @@ enum ActionButtonType {
|
|||
context.isOwner && //
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote && //
|
||||
context.isTrashEnabled,
|
||||
context.isTrashEnabled &&
|
||||
!context.isWaitingForTrashApproval,
|
||||
ActionButtonType.deletePermanent =>
|
||||
context.isOwner && //
|
||||
context.asset.hasRemote && //
|
||||
!context.isTrashEnabled ||
|
||||
context.isInLockedView,
|
||||
context.isInLockedView && !context.isWaitingForTrashApproval,
|
||||
ActionButtonType.delete =>
|
||||
context.isOwner && //
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
context.asset.hasRemote &&
|
||||
!context.isWaitingForTrashApproval,
|
||||
ActionButtonType.moveToLockFolder =>
|
||||
context.isOwner && //
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
context.asset.hasRemote &&
|
||||
!context.isWaitingForTrashApproval,
|
||||
ActionButtonType.removeFromLockFolder =>
|
||||
context.isOwner && //
|
||||
context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
ActionButtonType.deleteLocal =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasLocal,
|
||||
context.asset.hasLocal &&
|
||||
!context.isWaitingForTrashApproval,
|
||||
ActionButtonType.upload =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.storage == AssetState.local,
|
||||
|
|
@ -154,6 +161,7 @@ enum ActionButtonType {
|
|||
context.timelineOrigin != TimelineOrigin.lockedFolder &&
|
||||
context.timelineOrigin != TimelineOrigin.archive &&
|
||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
context.timelineOrigin != TimelineOrigin.syncTrash &&
|
||||
context.isOwner,
|
||||
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.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/providers/asset.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/manual_upload.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/user.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(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.standard,
|
||||
contentPadding: const EdgeInsets.only(left: 30, right: 30),
|
||||
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(
|
||||
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(),
|
||||
onTap: onTap,
|
||||
trailing: trailing,
|
||||
iconColor: btnColor,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +94,25 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||
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() {
|
||||
return buildActionButton(
|
||||
Icons.assignment_outlined,
|
||||
|
|
@ -269,6 +295,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||
const AppBarProfileInfoBox(),
|
||||
buildStorageInformation(),
|
||||
const AppBarServerInfo(),
|
||||
buildOutOfSyncButton(),
|
||||
if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(),
|
||||
buildAppLogButton(),
|
||||
buildSettingButton(),
|
||||
|
|
|
|||
|
|
@ -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/infrastructure/readonly_mode.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/sync_status.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
|
@ -123,6 +124,7 @@ class _ProfileIndicator extends ConsumerWidget {
|
|||
final serverInfoState = ref.watch(serverInfoProvider);
|
||||
|
||||
const widgetSize = 30.0;
|
||||
final outOfSyncCount = ref.watch(outOfSyncCountProvider).maybeWhen(data: (count) => count, orElse: () => 0);
|
||||
|
||||
void toggleReadonlyMode() {
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
|
@ -159,7 +161,7 @@ class _ProfileIndicator extends ConsumerWidget {
|
|||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
alignment: Alignment.bottomRight,
|
||||
isLabelVisible: versionWarningPresent,
|
||||
isLabelVisible: versionWarningPresent || outOfSyncCount > 0,
|
||||
offset: const Offset(-2, -12),
|
||||
child: user == null
|
||||
? const Icon(Icons.face_outlined, size: widgetSize)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.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/user.provider.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/local_storage_settings.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_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/ssl_client_cert_settings.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -32,9 +35,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||
bool isLoggedIn = ref.read(currentUserProvider) != null;
|
||||
|
||||
final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
||||
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
final isManageMediaSupported = useState(false);
|
||||
final manageMediaAndroidPermission = useState(false);
|
||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||
final allowSelfSignedSSLCert = useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert);
|
||||
|
|
@ -58,11 +59,6 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||
useEffect(() {
|
||||
() async {
|
||||
isManageMediaSupported.value = await checkAndroidVersion();
|
||||
if (isManageMediaSupported.value) {
|
||||
manageMediaAndroidPermission.value = await ref
|
||||
.read(localFilesManagerRepositoryProvider)
|
||||
.hasManageMediaPermission();
|
||||
}
|
||||
}();
|
||||
return null;
|
||||
}, []);
|
||||
|
|
@ -74,36 +70,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||
title: "advanced_settings_troubleshooting_title".tr(),
|
||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||
),
|
||||
if (isManageMediaSupported.value)
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isManageMediaSupported.value) const _TrashSyncModeSelector(),
|
||||
SettingsSliderListTile(
|
||||
text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}),
|
||||
valueNotifier: levelId,
|
||||
|
|
@ -158,3 +125,120 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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
|
||||
if (CurrentPlatform.isAndroid &&
|
||||
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
||||
if ((kDebugMode || kProfileMode) &&
|
||||
CurrentPlatform.isAndroid &&
|
||||
(appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid) ||
|
||||
appSettingsService.getSetting<bool>(AppSettingsEnum.reviewOutOfSyncChangesAndroid))) ...[
|
||||
_SectionHeaderText(text: "trash".t(context: context)),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class SettingsRadioGroup<T> {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
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 {
|
||||
|
|
@ -28,6 +30,12 @@ class SettingsRadioListTile<T> extends StatelessWidget {
|
|||
dense: true,
|
||||
activeColor: context.primaryColor,
|
||||
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,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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/storage.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/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
|
|
@ -26,6 +27,7 @@ void main() {
|
|||
late LocalSyncService sut;
|
||||
late DriftLocalAlbumRepository mockLocalAlbumRepository;
|
||||
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository;
|
||||
late DriftTrashSyncRepository mockTrashSyncRepo;
|
||||
late LocalFilesManagerRepository mockLocalFilesManager;
|
||||
late StorageRepository mockStorageRepository;
|
||||
late MockNativeSyncApi mockNativeSyncApi;
|
||||
|
|
@ -48,6 +50,7 @@ void main() {
|
|||
setUp(() async {
|
||||
mockLocalAlbumRepository = MockLocalAlbumRepository();
|
||||
mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository();
|
||||
mockTrashSyncRepo = MockTrashSyncRepository();
|
||||
mockLocalFilesManager = MockLocalFilesManagerRepository();
|
||||
mockStorageRepository = MockStorageRepository();
|
||||
mockNativeSyncApi = MockNativeSyncApi();
|
||||
|
|
@ -70,6 +73,7 @@ void main() {
|
|||
localFilesManager: mockLocalFilesManager,
|
||||
storageRepository: mockStorageRepository,
|
||||
nativeSyncApi: mockNativeSyncApi,
|
||||
trashSyncRepository: mockTrashSyncRepo,
|
||||
);
|
||||
|
||||
await Store.put(StoreKey.manageLocalMediaAndroid, false);
|
||||
|
|
|
|||
|
|
@ -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/sync_api.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/repositories/local_files_manager.repository.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
|
@ -48,6 +49,7 @@ void main() {
|
|||
late SyncApiRepository mockSyncApiRepo;
|
||||
late DriftLocalAssetRepository mockLocalAssetRepo;
|
||||
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
|
||||
late DriftTrashSyncRepository mockTrashSyncRepo;
|
||||
late LocalFilesManagerRepository mockLocalFilesManagerRepo;
|
||||
late StorageRepository mockStorageRepo;
|
||||
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
|
||||
|
|
@ -79,6 +81,7 @@ void main() {
|
|||
mockLocalAssetRepo = MockLocalAssetRepository();
|
||||
mockTrashedLocalAssetRepo = MockTrashedLocalAssetRepository();
|
||||
mockLocalFilesManagerRepo = MockLocalFilesManagerRepository();
|
||||
mockTrashSyncRepo = MockTrashSyncRepository();
|
||||
mockStorageRepo = MockStorageRepository();
|
||||
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
|
||||
mockResetCallbackWrapper = _MockAbortCallbackWrapper();
|
||||
|
|
@ -135,9 +138,10 @@ void main() {
|
|||
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
|
||||
localFilesManager: mockLocalFilesManagerRepo,
|
||||
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.getToRestore()).thenAnswer((_) async => []);
|
||||
when(() => mockTrashedLocalAssetRepo.applyRestoredAssets(any())).thenAnswer((_) async {});
|
||||
|
|
@ -216,6 +220,7 @@ void main() {
|
|||
localFilesManager: mockLocalFilesManagerRepo,
|
||||
storageRepository: mockStorageRepo,
|
||||
cancelChecker: cancellationChecker.call,
|
||||
trashSyncRepository: mockTrashSyncRepo,
|
||||
);
|
||||
await sut.sync();
|
||||
|
||||
|
|
@ -255,6 +260,7 @@ void main() {
|
|||
localFilesManager: mockLocalFilesManagerRepo,
|
||||
storageRepository: mockStorageRepo,
|
||||
cancelChecker: cancellationChecker.call,
|
||||
trashSyncRepository: mockTrashSyncRepo,
|
||||
);
|
||||
|
||||
await sut.sync();
|
||||
|
|
@ -386,9 +392,16 @@ void main() {
|
|||
'album-a': [localAsset],
|
||||
'album-b': [mergedAsset],
|
||||
};
|
||||
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
|
||||
final Iterable<String> requestedChecksums = invocation.positionalArguments.first as Iterable<String>;
|
||||
expect(requestedChecksums.toSet(), equals({'checksum-local', 'checksum-merged', 'checksum-remote-only'}));
|
||||
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>())).thenAnswer((invocation) async {
|
||||
final Map<String, DateTime> trashedAssetsMap = invocation.positionalArguments.first as Map<String, DateTime>;
|
||||
expect(
|
||||
trashedAssetsMap,
|
||||
equals({
|
||||
localAsset.checksum!: DateTime(2025, 5, 1),
|
||||
mergedAsset.checksum!: DateTime(2025, 5, 2),
|
||||
'checksum-remote-only': DateTime(2025, 5, 3),
|
||||
}),
|
||||
);
|
||||
return assetsByAlbum;
|
||||
});
|
||||
|
||||
|
|
@ -445,7 +458,7 @@ void main() {
|
|||
|
||||
await simulateEvents(events);
|
||||
|
||||
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).called(1);
|
||||
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>())).called(1);
|
||||
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
|
||||
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
|
||||
});
|
||||
|
|
@ -455,7 +468,7 @@ void main() {
|
|||
|
||||
await simulateEvents(events);
|
||||
|
||||
verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any()));
|
||||
verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any<Map<String, DateTime>>()));
|
||||
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
|
||||
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
|
||||
});
|
||||
|
|
@ -474,11 +487,7 @@ void main() {
|
|||
});
|
||||
|
||||
final events = [
|
||||
SyncStreamStub.assetModified(
|
||||
id: 'remote-1',
|
||||
checksum: 'checksum-trash',
|
||||
ack: 'asset-remote-1-11',
|
||||
),
|
||||
SyncStreamStub.assetModified(id: 'remote-1', checksum: 'checksum-trash', ack: 'asset-remote-1-11'),
|
||||
];
|
||||
|
||||
await simulateEvents(events);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'schema_v11.dart' as v11;
|
|||
import 'schema_v12.dart' as v12;
|
||||
import 'schema_v13.dart' as v13;
|
||||
import 'schema_v14.dart' as v14;
|
||||
import 'schema_v15.dart' as v15;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -50,10 +51,28 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v13.DatabaseAtV13(db);
|
||||
case 14:
|
||||
return v14.DatabaseAtV14(db);
|
||||
case 15:
|
||||
return v15.DatabaseAtV15(db);
|
||||
default:
|
||||
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
|
|
@ -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/sync_api.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/user.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 MockTrashSyncRepository extends Mock implements DriftTrashSyncRepository {}
|
||||
|
||||
class MockStorageRepository extends Mock implements StorageRepository {}
|
||||
|
||||
class MockDriftBackupRepository extends Mock implements DriftBackupRepository {}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -114,7 +115,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.share.shouldShow(context), isTrue);
|
||||
|
|
@ -130,7 +132,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.share.shouldShow(context), isTrue);
|
||||
|
|
@ -149,7 +152,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.shareLink.shouldShow(context), isTrue);
|
||||
|
|
@ -166,7 +170,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.shareLink.shouldShow(context), isFalse);
|
||||
|
|
@ -183,7 +188,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.shareLink.shouldShow(context), isFalse);
|
||||
|
|
@ -202,7 +208,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.archive.shouldShow(context), isTrue);
|
||||
|
|
@ -219,7 +226,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.archive.shouldShow(context), isFalse);
|
||||
|
|
@ -236,7 +244,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.archive.shouldShow(context), isFalse);
|
||||
|
|
@ -253,7 +262,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.archive.shouldShow(context), isFalse);
|
||||
|
|
@ -270,7 +280,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.archive.shouldShow(context), isFalse);
|
||||
|
|
@ -289,7 +300,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.unarchive.shouldShow(context), isTrue);
|
||||
|
|
@ -306,7 +318,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.unarchive.shouldShow(context), isFalse);
|
||||
|
|
@ -323,7 +336,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.unarchive.shouldShow(context), isFalse);
|
||||
|
|
@ -342,7 +356,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.download.shouldShow(context), isTrue);
|
||||
|
|
@ -359,7 +374,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.download.shouldShow(context), isFalse);
|
||||
|
|
@ -376,7 +392,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.download.shouldShow(context), isFalse);
|
||||
|
|
@ -395,7 +412,8 @@ void main() {
|
|||
isStacked: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.similarPhotos.shouldShow(context), isTrue);
|
||||
|
|
@ -412,7 +430,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
isStacked: false,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.similarPhotos.shouldShow(context), isFalse);
|
||||
|
|
@ -431,7 +450,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.trash.shouldShow(context), isTrue);
|
||||
|
|
@ -448,7 +468,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.trash.shouldShow(context), isFalse);
|
||||
|
|
@ -467,7 +488,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue);
|
||||
|
|
@ -484,7 +506,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse);
|
||||
|
|
@ -503,7 +526,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.delete.shouldShow(context), isTrue);
|
||||
|
|
@ -522,7 +546,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.moveToLockFolder.shouldShow(context), isTrue);
|
||||
|
|
@ -541,7 +566,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue);
|
||||
|
|
@ -558,7 +584,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.deleteLocal.shouldShow(context), isFalse);
|
||||
|
|
@ -574,7 +601,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue);
|
||||
|
|
@ -593,7 +621,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.upload.shouldShow(context), isTrue);
|
||||
|
|
@ -612,7 +641,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.removeFromAlbum.shouldShow(context), isTrue);
|
||||
|
|
@ -628,7 +658,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.removeFromAlbum.shouldShow(context), isFalse);
|
||||
|
|
@ -647,7 +678,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.likeActivity.shouldShow(context), isTrue);
|
||||
|
|
@ -664,7 +696,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
|
||||
|
|
@ -681,7 +714,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
|
||||
|
|
@ -697,7 +731,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
|
||||
|
|
@ -715,7 +750,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: true,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.advancedInfo.shouldShow(context), isTrue);
|
||||
|
|
@ -731,7 +767,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.advancedInfo.shouldShow(context), isFalse);
|
||||
|
|
@ -751,6 +788,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: true,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -768,6 +806,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -785,6 +824,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -807,6 +847,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
});
|
||||
|
|
@ -826,7 +867,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
final widget = buttonType.buildButton(contextWithAlbum);
|
||||
expect(widget, isA<Widget>());
|
||||
|
|
@ -840,7 +882,8 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
final widget = buttonType.buildButton(contextWithAlbum);
|
||||
expect(widget, isA<Widget>());
|
||||
|
|
@ -855,7 +898,8 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: true,
|
||||
source: ActionSource.timeline,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
final widget = buttonType.buildButton(contextWithAlbum);
|
||||
expect(widget, isA<Widget>());
|
||||
|
|
@ -879,6 +923,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -900,6 +945,7 @@ void main() {
|
|||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -919,6 +965,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -939,6 +986,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
@ -953,6 +1001,7 @@ void main() {
|
|||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
isWaitingForTrashApproval: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue