mirror of
https://github.com/immich-app/immich.git
synced 2026-05-16 04:22:17 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b560249eed | |||
| 14978ece65 | |||
| 289194a356 | |||
| 3f93fda4e4 | |||
| 108e10f175 | |||
| 7023143bce | |||
| e4a8e77c67 | |||
| 8e5ce7f684 | |||
| f86f073d50 | |||
| d7580a3413 | |||
| 324890e182 | |||
| 7cccd216c2 | |||
| c98a8e1bb1 | |||
| 989a406721 | |||
| 7f20d689ea | |||
| f168f9e117 | |||
| 8bd90669c0 | |||
| 8f7e06bebd | |||
| eac150114f | |||
| 296ae54335 |
@@ -180,4 +180,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
extension LocalAlbumIsarHelper on IsarCollection<LocalAlbum> {
|
||||
Future<void> store(LocalAlbum a) async {
|
||||
await put(a);
|
||||
await a.thumb.save();
|
||||
await a.assets.save();
|
||||
}
|
||||
}
|
||||
|
||||
extension RemoteAlbumIsarHelper on IsarCollection<RemoteAlbum> {
|
||||
Future<void> store(RemoteAlbum a) async {
|
||||
await put(a);
|
||||
await a.owner.save();
|
||||
await a.thumb.save();
|
||||
await a.sharedUsers.save();
|
||||
await a.assets.save();
|
||||
}
|
||||
}
|
||||
|
||||
extension BackupAlbumIsarHelper on IsarCollection<BackupAlbum> {
|
||||
Future<void> store(BackupAlbum a) async {
|
||||
await put(a);
|
||||
await a.album.save();
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
String get eTagKeyAssetCount => "device-album-$id-asset-count";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
extension OptionalCast on Object {
|
||||
T? tryCast<T>() => this is T ? this as T : null;
|
||||
}
|
||||
|
||||
extension NullUtilities on Object? {
|
||||
/// Returns true if object is null or is empty
|
||||
bool get isNullOrEmpty => (this == null || _isIterableAndEmpty);
|
||||
|
||||
bool get _isIterableAndEmpty =>
|
||||
this is Iterable ? (this as Iterable).isEmpty : false;
|
||||
}
|
||||
+11
-13
@@ -9,20 +9,18 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
@@ -73,14 +71,15 @@ Future<void> initApp() async {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
log.severe(
|
||||
'FlutterError - Catch all',
|
||||
"${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
|
||||
'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}',
|
||||
details,
|
||||
details.stack,
|
||||
);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
log.severe('PlatformDispatcher - Catch all error: $error', error, stack);
|
||||
debugPrint("PlatformDispatcher - Catch all error: $error $stack");
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -94,14 +93,13 @@ Future<Isar> loadDb() async {
|
||||
StoreValueSchema,
|
||||
ExifInfoSchema,
|
||||
AssetSchema,
|
||||
AlbumSchema,
|
||||
UserSchema,
|
||||
BackupAlbumSchema,
|
||||
DuplicatedAssetSchema,
|
||||
LocalAlbumSchema,
|
||||
RemoteAlbumSchema,
|
||||
UserSchema,
|
||||
LoggerMessageSchema,
|
||||
ETagSchema,
|
||||
if (Platform.isAndroid) AndroidDeviceAssetSchema,
|
||||
if (Platform.isIOS) IOSDeviceAssetSchema,
|
||||
DeviceAssetSchema,
|
||||
],
|
||||
directory: dir.path,
|
||||
maxSizeMiB: 256,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/activities/providers/activity.provider.dar
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
@@ -22,15 +23,15 @@ class ActivitiesPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Album has to be set in the provider before reaching this page
|
||||
final album = ref.watch(currentAlbumProvider)!;
|
||||
// Album has to be set in the provider before reaching this page and has to be a RemoteAlbum
|
||||
final album = ref.watch(currentAlbumProvider)! as RemoteAlbum;
|
||||
final asset = ref.watch(currentAssetProvider);
|
||||
final user = ref.watch(currentUserProvider);
|
||||
|
||||
final activityNotifier = ref
|
||||
.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier);
|
||||
final activityNotifier =
|
||||
ref.read(albumActivityProvider(album.id, asset?.remoteId).notifier);
|
||||
final activities =
|
||||
ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId));
|
||||
ref.watch(albumActivityProvider(album.id, asset?.remoteId));
|
||||
|
||||
final listViewScrollController = useScrollController();
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ class ActivityTextField extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(currentAlbumProvider)!;
|
||||
final asset = ref.watch(currentAssetProvider);
|
||||
final activityNotifier = ref
|
||||
.read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier);
|
||||
final activityNotifier =
|
||||
ref.read(albumActivityProvider(album.id, asset?.remoteId).notifier);
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final inputController = useTextEditingController();
|
||||
final inputFocusNode = useFocusNode();
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// ignore_for_file: add-copy-with
|
||||
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
part 'album.model.g.dart';
|
||||
|
||||
/// Acts as a common class for RemoteAlbums and LocalAlbums to perform generic album handling irrespective of
|
||||
/// where the album is from
|
||||
sealed class Album {
|
||||
Id get isarId => fastHash(id);
|
||||
@Index(unique: true, replace: true, type: IndexType.hash)
|
||||
final String id;
|
||||
String name;
|
||||
DateTime modifiedAt;
|
||||
final IsarLink<Asset> thumb = IsarLink<Asset>();
|
||||
|
||||
static const assetsLinkId = 'assets';
|
||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||
|
||||
@ignore
|
||||
int get assetCount => assets.length;
|
||||
|
||||
@ignore
|
||||
Asset? get thumbnail => thumb.value;
|
||||
|
||||
Album({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.modifiedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Album(id: $id, name: $name, assetCount: $assetCount)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Album other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.name == name &&
|
||||
other.modifiedAt == modifiedAt &&
|
||||
other.thumb == thumb &&
|
||||
other.assetCount == assetCount;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
name.hashCode ^
|
||||
modifiedAt.hashCode ^
|
||||
thumb.hashCode ^
|
||||
assetCount.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@Collection()
|
||||
class LocalAlbum extends Album {
|
||||
static const isAllId = 'isAll';
|
||||
|
||||
@Backlink(to: BackupAlbum.albumLinkId)
|
||||
final IsarLink<BackupAlbum> backup = IsarLink<BackupAlbum>();
|
||||
|
||||
LocalAlbum({
|
||||
required super.id,
|
||||
required super.name,
|
||||
required super.modifiedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocalAlbum(id: $id, name: $name, assetCount: $assetCount)';
|
||||
}
|
||||
|
||||
static LocalAlbum fromAssetPathEntity(
|
||||
AssetPathEntity ape, {
|
||||
Asset? thumbnail,
|
||||
Iterable<Asset>? assets,
|
||||
}) {
|
||||
final album = LocalAlbum(
|
||||
id: ape.id,
|
||||
name: ape.name,
|
||||
modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||
);
|
||||
|
||||
if (assets != null) {
|
||||
album.assets.addAll(assets);
|
||||
}
|
||||
|
||||
album.thumb.value = thumbnail;
|
||||
return album;
|
||||
}
|
||||
}
|
||||
|
||||
@Collection()
|
||||
class RemoteAlbum extends Album {
|
||||
DateTime createdAt;
|
||||
DateTime? startDate;
|
||||
DateTime? endDate;
|
||||
DateTime? lastModifiedAssetTimestamp;
|
||||
bool shared;
|
||||
bool activityEnabled;
|
||||
final IsarLink<User> owner = IsarLink<User>();
|
||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||
|
||||
@ignore
|
||||
String? get ownerId => owner.value?.id;
|
||||
|
||||
@ignore
|
||||
String? get ownerName {
|
||||
// Guard null owner
|
||||
if (owner.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final name = <String>[];
|
||||
if (owner.value?.name != null) {
|
||||
name.add(owner.value!.name);
|
||||
}
|
||||
|
||||
return name.join(' ');
|
||||
}
|
||||
|
||||
RemoteAlbum({
|
||||
required super.id,
|
||||
required super.name,
|
||||
required super.modifiedAt,
|
||||
required this.createdAt,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.lastModifiedAssetTimestamp,
|
||||
this.shared = false,
|
||||
this.activityEnabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RemoteAlbum(id: $id, name: $name, assetCount: $assetCount, createdAt: $createdAt, startDate: $startDate, endDate: $endDate, lastModifiedAssetTimestamp: $lastModifiedAssetTimestamp, shared: $shared, activityEnabled: $activityEnabled)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant RemoteAlbum other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
final lastModifiedAssetTimestampIsSetAndEqual =
|
||||
lastModifiedAssetTimestamp != null &&
|
||||
other.lastModifiedAssetTimestamp != null
|
||||
? lastModifiedAssetTimestamp!
|
||||
.isAtSameMomentAs(other.lastModifiedAssetTimestamp!)
|
||||
: true;
|
||||
|
||||
return super == other &&
|
||||
other.createdAt == createdAt &&
|
||||
other.startDate == startDate &&
|
||||
other.endDate == endDate &&
|
||||
lastModifiedAssetTimestampIsSetAndEqual &&
|
||||
other.shared == shared &&
|
||||
other.activityEnabled == activityEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return super.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
startDate.hashCode ^
|
||||
endDate.hashCode ^
|
||||
lastModifiedAssetTimestamp.hashCode ^
|
||||
shared.hashCode ^
|
||||
activityEnabled.hashCode;
|
||||
}
|
||||
|
||||
static Future<RemoteAlbum> fromDto(AlbumResponseDto dto, Isar db) async {
|
||||
final album = RemoteAlbum(
|
||||
id: dto.id,
|
||||
name: dto.albumName,
|
||||
createdAt: dto.createdAt,
|
||||
modifiedAt: dto.updatedAt,
|
||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||
shared: dto.shared,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
activityEnabled: dto.isActivityEnabled,
|
||||
);
|
||||
|
||||
album.owner.value = await db.users.getById(dto.ownerId);
|
||||
|
||||
if (dto.albumThumbnailAssetId != null) {
|
||||
album.thumb.value = await db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
if (dto.sharedUsers.isNotEmpty) {
|
||||
final users = await db.users
|
||||
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
|
||||
album.sharedUsers.addAll(users.cast());
|
||||
}
|
||||
|
||||
if (dto.assets.isNotEmpty) {
|
||||
final assets =
|
||||
await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id));
|
||||
album.assets.addAll(assets);
|
||||
}
|
||||
|
||||
return album;
|
||||
}
|
||||
}
|
||||
+2605
File diff suppressed because it is too large
Load Diff
@@ -1,74 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
AlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||
final query = db.albums
|
||||
.filter()
|
||||
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
|
||||
query.findAll().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
}
|
||||
final AlbumService _albumService;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
Future<void> getAllAlbums() => Future.wait([
|
||||
_albumService.refreshDeviceAlbums(),
|
||||
_albumService.refreshRemoteAlbums(isShared: false),
|
||||
]);
|
||||
|
||||
Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
|
||||
|
||||
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||
|
||||
Future<Album?> createAlbum(
|
||||
String albumTitle,
|
||||
Set<Asset> assets,
|
||||
) =>
|
||||
_albumService.createAlbum(albumTitle, assets, []);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final albumProvider =
|
||||
StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
|
||||
return AlbumNotifier(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final albumWatcher =
|
||||
StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
|
||||
final remoteAlbumWatcher =
|
||||
StreamProvider.autoDispose.family<RemoteAlbum, int>((ref, albumId) async* {
|
||||
final db = ref.watch(dbProvider);
|
||||
final a = await db.albums.get(albumId);
|
||||
final a = await db.remoteAlbums.get(albumId);
|
||||
if (a != null) yield a;
|
||||
await for (final a in db.albums.watchObject(albumId, fireImmediately: true)) {
|
||||
await for (final a
|
||||
in db.remoteAlbums.watchObject(albumId, fireImmediately: true)) {
|
||||
if (a != null) yield a;
|
||||
}
|
||||
});
|
||||
|
||||
final albumRenderlistProvider =
|
||||
final remoteAlbumRenderlistProvider =
|
||||
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
||||
final album = ref.watch(albumWatcher(albumId)).value;
|
||||
final album = ref.watch(remoteAlbumWatcher(albumId)).value;
|
||||
if (album != null) {
|
||||
final query =
|
||||
album.assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
|
||||
@@ -76,3 +28,25 @@ final albumRenderlistProvider =
|
||||
}
|
||||
return const Stream.empty();
|
||||
});
|
||||
|
||||
final localAlbumWatcher =
|
||||
StreamProvider.autoDispose.family<LocalAlbum, int>((ref, albumId) async* {
|
||||
final db = ref.watch(dbProvider);
|
||||
final a = await db.localAlbums.get(albumId);
|
||||
if (a != null) yield a;
|
||||
await for (final a
|
||||
in db.localAlbums.watchObject(albumId, fireImmediately: true)) {
|
||||
if (a != null) yield a;
|
||||
}
|
||||
});
|
||||
|
||||
final localAlbumRenderlistProvider =
|
||||
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
||||
final album = ref.watch(localAlbumWatcher(albumId)).value;
|
||||
if (album != null) {
|
||||
final query =
|
||||
album.assets.filter().localIdIsNotNull().sortByFileCreatedAtDesc();
|
||||
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
|
||||
}
|
||||
return const Stream.empty();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'album_sort_by_options.provider.g.dart';
|
||||
@@ -11,9 +11,13 @@ typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
|
||||
class _AlbumSortHandlers {
|
||||
const _AlbumSortHandlers._();
|
||||
|
||||
/// Sorts a List<Album> based on their created date.
|
||||
///
|
||||
/// ! This is not support for LocalAlbums and they are filtered out from the result
|
||||
static const AlbumSortFn created = _sortByCreated;
|
||||
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sortedBy((album) => album.createdAt);
|
||||
final sorted =
|
||||
albums.whereType<RemoteAlbum>().sortedBy((album) => album.createdAt);
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
@@ -36,9 +40,12 @@ class _AlbumSortHandlers {
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
/// Sorts a List<Album> based on the most recent assets.
|
||||
///
|
||||
/// ! This is not support for LocalAlbums and they are filtered out from the result
|
||||
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
||||
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final sorted = albums.whereType<RemoteAlbum>().sorted((a, b) {
|
||||
if (a.endDate != null && b.endDate != null) {
|
||||
return a.endDate!.compareTo(b.endDate!);
|
||||
}
|
||||
@@ -49,9 +56,12 @@ class _AlbumSortHandlers {
|
||||
return (isReverse ? sorted.reversed : sorted).toList();
|
||||
}
|
||||
|
||||
/// Sorts a List<Album> based on the most oldest assets.
|
||||
///
|
||||
/// ! This is not support for LocalAlbums and they are filtered out from the result
|
||||
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
||||
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final sorted = albums.whereType<RemoteAlbum>().sorted((a, b) {
|
||||
if (a.startDate != null && b.startDate != null) {
|
||||
return a.startDate!.compareTo(b.startDate!);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
AlbumViewerNotifier(this.ref)
|
||||
@@ -31,7 +31,7 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
}
|
||||
|
||||
Future<bool> changeAlbumTitle(
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
String newAlbumTitle,
|
||||
) async {
|
||||
AlbumService service = ref.watch(albumServiceProvider);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'current_album.provider.g.dart';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album_service.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'local_album.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class LocalAlbums extends _$LocalAlbums {
|
||||
@override
|
||||
Stream<List<LocalAlbum>> build() async* {
|
||||
final db = ref.read(dbProvider);
|
||||
final stream =
|
||||
db.localAlbums.where().watch().listen((v) => state = AsyncData(v));
|
||||
ref.onDispose(() => stream.cancel());
|
||||
yield await db.localAlbums.where().findAll();
|
||||
}
|
||||
|
||||
Future<void> getDeviceAlbums() async {
|
||||
final hasChanges =
|
||||
await ref.read(localAlbumServiceProvider).refreshDeviceAlbums();
|
||||
if (hasChanges) {
|
||||
ref.read(backupAlbumsProvider.notifier).refreshAlbumAssetsState();
|
||||
ref.invalidate(deviceAssetsProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'local_album.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$localAlbumsHash() => r'15274162ef40aa87f498a5cf6f8f8a5e0d4c69a8';
|
||||
|
||||
/// See also [LocalAlbums].
|
||||
@ProviderFor(LocalAlbums)
|
||||
final localAlbumsProvider =
|
||||
AutoDisposeStreamNotifierProvider<LocalAlbums, List<LocalAlbum>>.internal(
|
||||
LocalAlbums.new,
|
||||
name: r'localAlbumsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$localAlbumsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LocalAlbums = AutoDisposeStreamNotifier<List<LocalAlbum>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:immich_mobile/modules/album/services/local_album.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'local_album_service.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
LocalAlbumService localAlbumService(LocalAlbumServiceRef ref) =>
|
||||
LocalAlbumService(
|
||||
ref.watch(dbProvider),
|
||||
ref.read(hashServiceProvider),
|
||||
ref.read(syncServiceProvider),
|
||||
ref.read(backupAlbumServiceProvider),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'local_album_service.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$localAlbumServiceHash() => r'362ac6700cf340ace2d6de4c691e1e900d3a28c8';
|
||||
|
||||
/// See also [localAlbumService].
|
||||
@ProviderFor(localAlbumService)
|
||||
final localAlbumServiceProvider = Provider<LocalAlbumService>.internal(
|
||||
localAlbumService,
|
||||
name: r'localAlbumServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$localAlbumServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef LocalAlbumServiceRef = ProviderRef<LocalAlbumService>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'remote_album.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class RemoteAlbums extends _$RemoteAlbums {
|
||||
@override
|
||||
Stream<List<RemoteAlbum>> build() async* {
|
||||
final db = ref.read(dbProvider);
|
||||
final stream =
|
||||
db.remoteAlbums.where().watch().listen((v) => state = AsyncData(v));
|
||||
ref.onDispose(() => stream.cancel());
|
||||
yield await db.remoteAlbums.where().findAll();
|
||||
}
|
||||
|
||||
Future<void> getRemoteAlbums([bool isShared = false]) =>
|
||||
ref.read(albumServiceProvider).refreshRemoteAlbums(isShared: isShared);
|
||||
|
||||
Future<bool> deleteAlbum(RemoteAlbum album) =>
|
||||
ref.read(albumServiceProvider).deleteAlbum(album);
|
||||
|
||||
Future<RemoteAlbum?> createAlbum(
|
||||
String albumTitle,
|
||||
Set<Asset> assets,
|
||||
) =>
|
||||
ref.read(albumServiceProvider).createAlbum(albumTitle, assets, []);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'remote_album.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$remoteAlbumsHash() => r'bae71f612bbfcd3e2f193dd63eef53ea1d822af2';
|
||||
|
||||
/// See also [RemoteAlbums].
|
||||
@ProviderFor(RemoteAlbums)
|
||||
final remoteAlbumsProvider =
|
||||
AutoDisposeStreamNotifierProvider<RemoteAlbums, List<RemoteAlbum>>.internal(
|
||||
RemoteAlbums.new,
|
||||
name: r'remoteAlbumsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$remoteAlbumsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$RemoteAlbums = AutoDisposeStreamNotifier<List<RemoteAlbum>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -2,16 +2,17 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
class SharedAlbumNotifier extends StateNotifier<List<RemoteAlbum>> {
|
||||
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
||||
final query =
|
||||
db.remoteAlbums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
||||
query.findAll().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
@@ -21,9 +22,9 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
final AlbumService _albumService;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
late final StreamSubscription<List<RemoteAlbum>> _streamSub;
|
||||
|
||||
Future<Album?> createSharedAlbum(
|
||||
Future<RemoteAlbum?> createSharedAlbum(
|
||||
String albumName,
|
||||
Iterable<Asset> assets,
|
||||
Iterable<User> sharedUsers,
|
||||
@@ -43,9 +44,10 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
Future<void> getAllSharedAlbums() =>
|
||||
_albumService.refreshRemoteAlbums(isShared: true);
|
||||
|
||||
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||
Future<bool> deleteAlbum(RemoteAlbum album) =>
|
||||
_albumService.deleteAlbum(album);
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
Future<bool> leaveAlbum(RemoteAlbum album) async {
|
||||
var res = await _albumService.leaveAlbum(album);
|
||||
|
||||
if (res) {
|
||||
@@ -56,11 +58,11 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
|
||||
Future<bool> removeAssetFromAlbum(RemoteAlbum album, Iterable<Asset> assets) {
|
||||
return _albumService.removeAssetFromAlbum(album, assets);
|
||||
}
|
||||
|
||||
Future<bool> removeUserFromAlbum(Album album, User user) async {
|
||||
Future<bool> removeUserFromAlbum(RemoteAlbum album, User user) async {
|
||||
final result = await _albumService.removeUserFromAlbum(album, user);
|
||||
|
||||
if (result && album.sharedUsers.isEmpty) {
|
||||
@@ -70,7 +72,7 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
|
||||
Future<bool> setActivityEnabled(RemoteAlbum album, bool activityEnabled) {
|
||||
return _albumService.setActivityEnabled(album, activityEnabled);
|
||||
}
|
||||
|
||||
@@ -82,7 +84,8 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
final sharedAlbumProvider =
|
||||
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
|
||||
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<RemoteAlbum>>(
|
||||
(ref) {
|
||||
return SharedAlbumNotifier(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'sorted_album.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
List<Album> sortedAlbum(SortedAlbumRef ref, List<Album> albums) {
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
return albumSortOption.sortFn(albums, albumSortIsReverse);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sorted_album.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$sortedAlbumHash() => r'350f537324a42ba9e01e50ca3722878ec3a8330c';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [sortedAlbum].
|
||||
@ProviderFor(sortedAlbum)
|
||||
const sortedAlbumProvider = SortedAlbumFamily();
|
||||
|
||||
/// See also [sortedAlbum].
|
||||
class SortedAlbumFamily extends Family<List<Album>> {
|
||||
/// See also [sortedAlbum].
|
||||
const SortedAlbumFamily();
|
||||
|
||||
/// See also [sortedAlbum].
|
||||
SortedAlbumProvider call(
|
||||
List<Album> albums,
|
||||
) {
|
||||
return SortedAlbumProvider(
|
||||
albums,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SortedAlbumProvider getProviderOverride(
|
||||
covariant SortedAlbumProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.albums,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'sortedAlbumProvider';
|
||||
}
|
||||
|
||||
/// See also [sortedAlbum].
|
||||
class SortedAlbumProvider extends AutoDisposeProvider<List<Album>> {
|
||||
/// See also [sortedAlbum].
|
||||
SortedAlbumProvider(
|
||||
List<Album> albums,
|
||||
) : this._internal(
|
||||
(ref) => sortedAlbum(
|
||||
ref as SortedAlbumRef,
|
||||
albums,
|
||||
),
|
||||
from: sortedAlbumProvider,
|
||||
name: r'sortedAlbumProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$sortedAlbumHash,
|
||||
dependencies: SortedAlbumFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SortedAlbumFamily._allTransitiveDependencies,
|
||||
albums: albums,
|
||||
);
|
||||
|
||||
SortedAlbumProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.albums,
|
||||
}) : super.internal();
|
||||
|
||||
final List<Album> albums;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
List<Album> Function(SortedAlbumRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SortedAlbumProvider._internal(
|
||||
(ref) => create(ref as SortedAlbumRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
albums: albums,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<List<Album>> createElement() {
|
||||
return _SortedAlbumProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SortedAlbumProvider && other.albums == albums;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, albums.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin SortedAlbumRef on AutoDisposeProviderRef<List<Album>> {
|
||||
/// The parameter `albums` of this provider.
|
||||
List<Album> get albums;
|
||||
}
|
||||
|
||||
class _SortedAlbumProviderElement
|
||||
extends AutoDisposeProviderElement<List<Album>> with SortedAlbumRef {
|
||||
_SortedAlbumProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
List<Album> get albums => (origin as SortedAlbumProvider).albums;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -1,14 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/album_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/add_asset_response.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
@@ -18,9 +14,7 @@ import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final albumServiceProvider = Provider(
|
||||
(ref) => AlbumService(
|
||||
@@ -28,7 +22,6 @@ final albumServiceProvider = Provider(
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -37,9 +30,6 @@ class AlbumService {
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final Isar _db;
|
||||
final BackupService _backupService;
|
||||
final Logger _log = Logger('AlbumService');
|
||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
||||
|
||||
AlbumService(
|
||||
@@ -47,98 +37,8 @@ class AlbumService {
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
this._backupService,
|
||||
);
|
||||
|
||||
/// Checks all selected device albums for changes of albums and their assets
|
||||
/// Updates the local database and returns `true` if there were any changes
|
||||
Future<bool> refreshDeviceAlbums() async {
|
||||
if (!_localCompleter.isCompleted) {
|
||||
// guard against concurrent calls
|
||||
_log.info("refreshDeviceAlbums is already in progress");
|
||||
return _localCompleter.future;
|
||||
}
|
||||
_localCompleter = Completer();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
bool changes = false;
|
||||
try {
|
||||
final List<String> excludedIds =
|
||||
await _backupService.excludedAlbumsQuery().idProperty().findAll();
|
||||
final List<String> selectedIds =
|
||||
await _backupService.selectedAlbumsQuery().idProperty().findAll();
|
||||
if (selectedIds.isEmpty) {
|
||||
final numLocal = await _db.albums.where().localIdIsNotNull().count();
|
||||
if (numLocal > 0) {
|
||||
_syncService.removeAllLocalAlbumsAndAssets();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final List<AssetPathEntity> onDevice =
|
||||
await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||
);
|
||||
_log.info("Found ${onDevice.length} device albums");
|
||||
Set<String>? excludedAssets;
|
||||
if (excludedIds.isNotEmpty) {
|
||||
if (Platform.isIOS) {
|
||||
// iOS and Android device album working principle differ significantly
|
||||
// on iOS, an asset can be in multiple albums
|
||||
// on Android, an asset can only be in exactly one album (folder!) at the same time
|
||||
// thus, on Android, excluding an album can be done by ignoring that album
|
||||
// however, on iOS, it it necessary to load the assets from all excluded
|
||||
// albums and check every asset from any selected album against the set
|
||||
// of excluded assets
|
||||
excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds);
|
||||
_log.info("Found ${excludedAssets.length} assets to exclude");
|
||||
}
|
||||
// remove all excluded albums
|
||||
onDevice.removeWhere((e) => excludedIds.contains(e.id));
|
||||
_log.info(
|
||||
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
|
||||
);
|
||||
}
|
||||
final hasAll = selectedIds
|
||||
.map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
|
||||
.whereNotNull()
|
||||
.any((a) => a.isAll);
|
||||
if (hasAll) {
|
||||
if (Platform.isAndroid) {
|
||||
// remove the virtual "Recent" album and keep and individual albums
|
||||
// on Android, the virtual "Recent" `lastModified` value is always null
|
||||
onDevice.removeWhere((e) => e.isAll);
|
||||
_log.info("'Recents' is selected, keeping all individual albums");
|
||||
}
|
||||
} else {
|
||||
// keep only the explicitly selected albums
|
||||
onDevice.removeWhere((e) => !selectedIds.contains(e.id));
|
||||
_log.info("'Recents' is not selected, keeping only selected albums");
|
||||
}
|
||||
changes =
|
||||
await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets);
|
||||
_log.info("Syncing completed. Changes: $changes");
|
||||
} finally {
|
||||
_localCompleter.complete(changes);
|
||||
}
|
||||
debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
|
||||
return changes;
|
||||
}
|
||||
|
||||
Future<Set<String>> _loadExcludedAssetIds(
|
||||
List<AssetPathEntity> albums,
|
||||
List<String> excludedAlbumIds,
|
||||
) async {
|
||||
final Set<String> result = HashSet<String>();
|
||||
for (AssetPathEntity a in albums) {
|
||||
if (excludedAlbumIds.contains(a.id)) {
|
||||
final List<AssetEntity> assets =
|
||||
await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
|
||||
result.addAll(assets.map((e) => e.id));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks remote albums (owned if `isShared` is false) for changes,
|
||||
/// updates the local database and returns `true` if there were any changes
|
||||
Future<bool> refreshRemoteAlbums({required bool isShared}) async {
|
||||
@@ -170,7 +70,7 @@ class AlbumService {
|
||||
return changes;
|
||||
}
|
||||
|
||||
Future<Album?> createAlbum(
|
||||
Future<RemoteAlbum?> createAlbum(
|
||||
String albumName,
|
||||
Iterable<Asset> assets, [
|
||||
Iterable<User> sharedUsers = const [],
|
||||
@@ -184,8 +84,8 @@ class AlbumService {
|
||||
),
|
||||
);
|
||||
if (remote != null) {
|
||||
Album album = await Album.remote(remote);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
RemoteAlbum album = await RemoteAlbum.fromDto(remote, _db);
|
||||
await _db.writeTxn(() => _db.remoteAlbums.store(album));
|
||||
return album;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -203,13 +103,16 @@ class AlbumService {
|
||||
final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
|
||||
|
||||
if (null ==
|
||||
await _db.albums.filter().nameEqualTo(proposedName).findFirst()) {
|
||||
await _db.remoteAlbums
|
||||
.filter()
|
||||
.nameEqualTo(proposedName)
|
||||
.findFirst()) {
|
||||
return proposedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Album?> createAlbumWithGeneratedName(
|
||||
Future<RemoteAlbum?> createAlbumWithGeneratedName(
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
return createAlbum(
|
||||
@@ -221,11 +124,17 @@ class AlbumService {
|
||||
|
||||
Future<AddAssetsResponse?> addAdditionalAssetToAlbum(
|
||||
Iterable<Asset> assets,
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
) async {
|
||||
try {
|
||||
final remoteAlbum =
|
||||
await _db.remoteAlbums.where().idEqualTo(album.id).findFirst();
|
||||
if (remoteAlbum == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await _apiService.albumApi.addAssetsToAlbum(
|
||||
album.remoteId!,
|
||||
remoteAlbum.id,
|
||||
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
||||
);
|
||||
|
||||
@@ -244,10 +153,10 @@ class AlbumService {
|
||||
}
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await album.assets.update(link: successAssets);
|
||||
final a = await _db.albums.get(album.id);
|
||||
await remoteAlbum.assets.update(link: successAssets);
|
||||
final a = await _db.remoteAlbums.get(remoteAlbum.isarId);
|
||||
// trigger watcher
|
||||
await _db.albums.put(a!);
|
||||
await _db.remoteAlbums.put(a!);
|
||||
});
|
||||
|
||||
return AddAssetsResponse(
|
||||
@@ -264,11 +173,11 @@ class AlbumService {
|
||||
|
||||
Future<bool> addAdditionalUserToAlbum(
|
||||
List<String> sharedUserIds,
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
) async {
|
||||
try {
|
||||
final result = await _apiService.albumApi.addUsersToAlbum(
|
||||
album.remoteId!,
|
||||
album.id,
|
||||
AddUsersDto(sharedUserIds: sharedUserIds),
|
||||
);
|
||||
if (result != null) {
|
||||
@@ -276,7 +185,7 @@ class AlbumService {
|
||||
.addAll((await _db.users.getAllById(sharedUserIds)).cast());
|
||||
album.shared = result.shared;
|
||||
await _db.writeTxn(() async {
|
||||
await _db.albums.put(album);
|
||||
await _db.remoteAlbums.put(album);
|
||||
await album.sharedUsers.save();
|
||||
});
|
||||
return true;
|
||||
@@ -287,15 +196,15 @@ class AlbumService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||
Future<bool> setActivityEnabled(RemoteAlbum album, bool enabled) async {
|
||||
try {
|
||||
final result = await _apiService.albumApi.updateAlbumInfo(
|
||||
album.remoteId!,
|
||||
album.id,
|
||||
UpdateAlbumDto(isActivityEnabled: enabled),
|
||||
);
|
||||
if (result != null) {
|
||||
album.activityEnabled = enabled;
|
||||
await _db.writeTxn(() => _db.albums.put(album));
|
||||
await _db.writeTxn(() => _db.remoteAlbums.put(album));
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -304,20 +213,20 @@ class AlbumService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> deleteAlbum(Album album) async {
|
||||
Future<bool> deleteAlbum(RemoteAlbum album) async {
|
||||
try {
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
if (album.owner.value?.isarId == userId) {
|
||||
await _apiService.albumApi.deleteAlbum(album.remoteId!);
|
||||
await _apiService.albumApi.deleteAlbum(album.id);
|
||||
}
|
||||
if (album.shared) {
|
||||
final foreignAssets =
|
||||
await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
|
||||
await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
final List<Album> albums =
|
||||
await _db.albums.filter().sharedEqualTo(true).findAll();
|
||||
await _db.writeTxn(() => _db.remoteAlbums.delete(album.isarId));
|
||||
final List<RemoteAlbum> albums =
|
||||
await _db.remoteAlbums.filter().sharedEqualTo(true).findAll();
|
||||
final List<Asset> existing = [];
|
||||
for (Album a in albums) {
|
||||
for (RemoteAlbum a in albums) {
|
||||
existing.addAll(
|
||||
await a.assets.filter().not().ownerIdEqualTo(userId).findAll(),
|
||||
);
|
||||
@@ -328,7 +237,7 @@ class AlbumService {
|
||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove));
|
||||
}
|
||||
} else {
|
||||
await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
await _db.writeTxn(() => _db.remoteAlbums.delete(album.isarId));
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -337,9 +246,9 @@ class AlbumService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
Future<bool> leaveAlbum(RemoteAlbum album) async {
|
||||
try {
|
||||
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
|
||||
await _apiService.albumApi.removeUserFromAlbum(album.id, "me");
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||
@@ -348,21 +257,21 @@ class AlbumService {
|
||||
}
|
||||
|
||||
Future<bool> removeAssetFromAlbum(
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumApi.removeAssetFromAlbum(
|
||||
album.remoteId!,
|
||||
album.id,
|
||||
BulkIdsDto(
|
||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||
),
|
||||
);
|
||||
await _db.writeTxn(() async {
|
||||
await album.assets.update(unlink: assets);
|
||||
final a = await _db.albums.get(album.id);
|
||||
final a = await _db.remoteAlbums.get(album.isarId);
|
||||
// trigger watcher
|
||||
await _db.albums.put(a!);
|
||||
await _db.remoteAlbums.put(a!);
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -373,21 +282,21 @@ class AlbumService {
|
||||
}
|
||||
|
||||
Future<bool> removeUserFromAlbum(
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
User user,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumApi.removeUserFromAlbum(
|
||||
album.remoteId!,
|
||||
album.id,
|
||||
user.id,
|
||||
);
|
||||
|
||||
album.sharedUsers.remove(user);
|
||||
await _db.writeTxn(() async {
|
||||
await album.sharedUsers.update(unlink: [user]);
|
||||
final a = await _db.albums.get(album.id);
|
||||
final a = await _db.remoteAlbums.get(album.isarId);
|
||||
// trigger watcher
|
||||
await _db.albums.put(a!);
|
||||
await _db.remoteAlbums.put(a!);
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -398,18 +307,18 @@ class AlbumService {
|
||||
}
|
||||
|
||||
Future<bool> changeTitleAlbum(
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
String newAlbumTitle,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumApi.updateAlbumInfo(
|
||||
album.remoteId!,
|
||||
album.id,
|
||||
UpdateAlbumDto(
|
||||
albumName: newAlbumTitle,
|
||||
),
|
||||
);
|
||||
album.name = newAlbumTitle;
|
||||
await _db.writeTxn(() => _db.albums.put(album));
|
||||
await _db.writeTxn(() => _db.remoteAlbums.put(album));
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/album_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class LocalAlbumService {
|
||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||
final Logger _log = Logger('LocalAlbumService');
|
||||
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final SyncService _syncService;
|
||||
final BackupAlbumService _backupAlbumService;
|
||||
|
||||
LocalAlbumService(
|
||||
this._db,
|
||||
this._hashService,
|
||||
this._syncService,
|
||||
this._backupAlbumService,
|
||||
);
|
||||
|
||||
Future<bool> refreshDeviceAlbums() async =>
|
||||
SyncService.lock.run(_refreshDeviceAlbums);
|
||||
|
||||
/// Checks all selected device albums for changes of albums and their assets
|
||||
/// Updates the local database and returns `true` if there were any changes
|
||||
Future<bool> _refreshDeviceAlbums() async {
|
||||
if (!_localCompleter.isCompleted) {
|
||||
// guard against concurrent calls
|
||||
_log.info("refreshDeviceAlbums is already in progress");
|
||||
return _localCompleter.future;
|
||||
}
|
||||
|
||||
_localCompleter = Completer();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
bool changes = false;
|
||||
try {
|
||||
final List<AssetPathEntity> onDevice =
|
||||
await PhotoManager.getAssetPathList(
|
||||
filterOption: FilterOptionGroup(
|
||||
containsPathModified: true,
|
||||
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
||||
// title is needed to create Assets
|
||||
imageOption: const FilterOption(needTitle: true),
|
||||
videoOption: const FilterOption(needTitle: true),
|
||||
),
|
||||
);
|
||||
_log.fine("Found ${onDevice.length} device albums");
|
||||
changes = await _syncLocalAlbumAssetsToDb(onDevice);
|
||||
_log.fine("Syncing completed. Changes: $changes");
|
||||
} finally {
|
||||
_localCompleter.complete(changes);
|
||||
}
|
||||
_log.fine("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> _syncLocalAlbumAssetsToDb(List<AssetPathEntity> onDevice) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final inDb = await _db.localAlbums.where().sortById().findAll();
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.id.compareTo(b.id)), "sort!");
|
||||
final bool anyChanges = await diffSortedLists(
|
||||
onDevice,
|
||||
inDb,
|
||||
compare: (AssetPathEntity a, LocalAlbum b) => a.id.compareTo(b.id),
|
||||
both: (AssetPathEntity ape, LocalAlbum album) =>
|
||||
_syncAlbumInDbAndOnDevice(ape, album, deleteCandidates, existing),
|
||||
onlyFirst: (AssetPathEntity ape) => _addAlbumFromDevice(ape, existing),
|
||||
onlySecond: (LocalAlbum a) => _removeAlbumFromDb(a, deleteCandidates),
|
||||
);
|
||||
_log.fine(
|
||||
"Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
|
||||
);
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(deleteCandidates, existing, remote: false);
|
||||
_log.fine(
|
||||
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
|
||||
);
|
||||
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.exifInfos.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
|
||||
);
|
||||
}
|
||||
return anyChanges;
|
||||
}
|
||||
|
||||
/// Accumulates all suitable album assets to the `deleteCandidates` and
|
||||
/// removes the album from the database.
|
||||
Future<void> _removeAlbumFromDb(
|
||||
LocalAlbum album,
|
||||
List<Asset> deleteCandidates,
|
||||
) async {
|
||||
_log.info("Removing local album $album from DB");
|
||||
// delete assets in DB unless they are remote or part of some other album
|
||||
deleteCandidates.addAll(
|
||||
await album.assets.filter().remoteIdIsNull().findAll(),
|
||||
);
|
||||
await album.backup.load();
|
||||
final backupAlbum = album.backup.value;
|
||||
try {
|
||||
final ok = await _db.writeTxn(() async {
|
||||
return await _db.localAlbums.delete(album.isarId) &&
|
||||
(backupAlbum == null ||
|
||||
await _db.backupAlbums.delete(album.isarId));
|
||||
});
|
||||
assert(ok);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to remove local album $album from DB: $e", stack);
|
||||
}
|
||||
}
|
||||
|
||||
/// returns a tuple (toDelete toUpdate) when assets are to be deleted
|
||||
(List<int> toDelete, List<Asset> toUpdate) _handleAssetRemoval(
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing, {
|
||||
bool? remote,
|
||||
}) {
|
||||
if (deleteCandidates.isEmpty) {
|
||||
return const ([], []);
|
||||
}
|
||||
deleteCandidates.sort(Asset.compareById);
|
||||
deleteCandidates.uniqueConsecutive(compare: Asset.compareById);
|
||||
existing.sort(Asset.compareById);
|
||||
existing.uniqueConsecutive(compare: Asset.compareById);
|
||||
final (tooAdd, toUpdate, toRemove) = _syncService.diffAssets(
|
||||
existing,
|
||||
deleteCandidates,
|
||||
compare: Asset.compareById,
|
||||
remote: remote,
|
||||
);
|
||||
assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval");
|
||||
return (toRemove.map((e) => e.id).toList(), toUpdate);
|
||||
}
|
||||
|
||||
/// Syncs the device album to the album in the database
|
||||
/// returns `true` if there were any changes
|
||||
/// Accumulates asset candidates to delete and those already existing in DB
|
||||
Future<bool> _syncAlbumInDbAndOnDevice(
|
||||
AssetPathEntity ape,
|
||||
LocalAlbum album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing, [
|
||||
bool forceRefresh = false,
|
||||
]) async {
|
||||
if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
|
||||
_log.fine("Local album ${ape.name} has not changed. Skipping sync.");
|
||||
return false;
|
||||
}
|
||||
if (!forceRefresh && await _syncDeviceAlbumFast(ape, album)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
final inDb = await album.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final List<Asset> onDevice = await _hashService.getHashedAssets(ape);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
_removeDuplicates(onDevice);
|
||||
final (toAdd, toUpdate, toDelete) = _syncService.diffAssets(onDevice, inDb);
|
||||
_log.fine(
|
||||
"Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
|
||||
);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||
_log.fine(
|
||||
"Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update",
|
||||
);
|
||||
deleteCandidates.addAll(toDelete);
|
||||
existing.addAll(existingInDb);
|
||||
album.name = ape.name;
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
if (album.thumb.value != null && toDelete.contains(album.thumbnail)) {
|
||||
album.thumb.value = null;
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.assets
|
||||
.update(link: existingInDb + updated, unlink: toDelete);
|
||||
album.thumb.value ??= await album.assets.filter().findFirst();
|
||||
await _db.localAlbums.store(album);
|
||||
});
|
||||
_updateETagCount(ape);
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e, stack) {
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB: $e", stack);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(
|
||||
AssetPathEntity a,
|
||||
LocalAlbum b,
|
||||
) async {
|
||||
final lastKnownTotal =
|
||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount ?? 0;
|
||||
final hasSameLastModified = !(Platform.isAndroid && a.isAll) &&
|
||||
(a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt));
|
||||
return a.name != b.name ||
|
||||
hasSameLastModified ||
|
||||
await a.assetCountAsync != lastKnownTotal;
|
||||
}
|
||||
|
||||
/// Adds a new album from the device to the database and Accumulates all
|
||||
/// assets already existing in the database to the list of `existing` assets
|
||||
Future<void> _addAlbumFromDevice(
|
||||
AssetPathEntity ape,
|
||||
List<Asset> existing,
|
||||
) async {
|
||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||
|
||||
final assets = await _hashService.getHashedAssets(ape);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
|
||||
);
|
||||
await _syncService.upsertAssetsWithExif(updated);
|
||||
existing.addAll(existingInDb);
|
||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||
final LocalAlbum a = LocalAlbum.fromAssetPathEntity(
|
||||
ape,
|
||||
thumbnail: thumb,
|
||||
assets: existingInDb.followedBy(updated),
|
||||
);
|
||||
try {
|
||||
await _db.writeTxn(() => _db.localAlbums.store(a));
|
||||
_updateETagCount(ape);
|
||||
_log.info("Added a new local album to DB: ${ape.name}");
|
||||
} on IsarError catch (e, stack) {
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB: $e", stack);
|
||||
}
|
||||
await _backupAlbumService.syncWithLocalAlbum(a);
|
||||
}
|
||||
|
||||
/// fast path for common case: only new assets were added to device album
|
||||
/// returns `true` if successfull, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(
|
||||
AssetPathEntity ape,
|
||||
LocalAlbum album,
|
||||
) async {
|
||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await ape.assetCountAsync;
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
|
||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
||||
? await ape.fetchPathProperties(
|
||||
filterOptionGroup: FilterOptionGroup(
|
||||
updateTimeCond: DateTimeCond(
|
||||
min: album.modifiedAt.add(const Duration(seconds: 1)),
|
||||
max: ape.lastModified ?? DateTime.now(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
if (modified == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
||||
|
||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||
return false;
|
||||
}
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await album.assets.update(link: existingInDb + updated);
|
||||
await _db.localAlbums.put(album);
|
||||
});
|
||||
_updateETagCount(ape);
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e, stack) {
|
||||
_log.severe(
|
||||
"Failed to fast sync local album ${ape.name} to DB: $e",
|
||||
stack,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _updateETagCount(AssetPathEntity ape) async {
|
||||
final assetCountOnDevice = await ape.assetCountAsync;
|
||||
return _db.writeTxn(
|
||||
() => _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
assets.uniqueConsecutive(
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
onDuplicate: (a, b) =>
|
||||
_log.fine("Ignoring duplicate assets on device:\n$a\n$b"),
|
||||
);
|
||||
final int duplicates = before - assets.length;
|
||||
if (duplicates > 0) {
|
||||
_log.warning("Ignored $duplicates duplicate assets on device");
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
/// Returns a tuple (existing, updated)
|
||||
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
|
||||
List<Asset> assets,
|
||||
) async {
|
||||
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
|
||||
|
||||
final List<Asset?> inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
assets.map((a) => a.ownerId).toInt64List(),
|
||||
assets.map((a) => a.checksum).toList(growable: false),
|
||||
);
|
||||
assert(inDb.length == assets.length);
|
||||
final List<Asset> existing = [], toUpsert = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
toUpsert.add(assets[i]);
|
||||
continue;
|
||||
}
|
||||
if (b.canUpdate(assets[i])) {
|
||||
final updated = b.updatedCopy(assets[i]);
|
||||
assert(updated.id != Isar.autoIncrement);
|
||||
toUpsert.add(updated);
|
||||
} else {
|
||||
existing.add(b);
|
||||
}
|
||||
}
|
||||
assert(existing.length + toUpsert.length == assets.length);
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
@@ -25,14 +25,14 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final albums = ref.watch(remoteAlbumsProvider).valueOrNull ?? [];
|
||||
final albumService = ref.watch(albumServiceProvider);
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
// Fetch album updates, e.g., cover image
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
|
||||
return null;
|
||||
@@ -40,7 +40,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
void addToAlbum(Album album) async {
|
||||
void addToAlbum(RemoteAlbum album) async {
|
||||
final result = await albumService.addAdditionalAssetToAlbum(
|
||||
assets,
|
||||
album,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_listtile.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
/// The asset to add to an album
|
||||
final List<Album> albums;
|
||||
final List<Album> sharedAlbums;
|
||||
final void Function(Album) onAddToAlbum;
|
||||
final List<RemoteAlbum> albums;
|
||||
final List<RemoteAlbum> sharedAlbums;
|
||||
final void Function(RemoteAlbum) onAddToAlbum;
|
||||
final bool enabled;
|
||||
|
||||
const AddToAlbumSliverList({
|
||||
@@ -46,9 +46,11 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: sortedSharedAlbums.length,
|
||||
itemBuilder: (context, index) => AlbumThumbnailListTile(
|
||||
album: sortedSharedAlbums[index],
|
||||
album: sortedSharedAlbums[index] as RemoteAlbum,
|
||||
onTap: enabled
|
||||
? () => onAddToAlbum(sortedSharedAlbums[index])
|
||||
? () => onAddToAlbum(
|
||||
sortedSharedAlbums[index] as RemoteAlbum,
|
||||
)
|
||||
: () {},
|
||||
),
|
||||
),
|
||||
@@ -59,7 +61,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
|
||||
|
||||
// Build albums list
|
||||
final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0);
|
||||
final album = sortedAlbums[offset];
|
||||
final album = sortedAlbums[offset] as RemoteAlbum;
|
||||
return AlbumThumbnailListTile(
|
||||
album: album,
|
||||
onTap: enabled ? () => onAddToAlbum(album) : () {},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
|
||||
|
||||
class AlbumSortSelector extends ConsumerWidget {
|
||||
final List<AlbumSortMode> sortModes;
|
||||
|
||||
const AlbumSortSelector({required this.sortModes, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumSortMode = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
|
||||
List<PopupMenuEntry<AlbumSortMode>> buildOptions(BuildContext ctx) {
|
||||
{
|
||||
return sortModes.map((option) {
|
||||
final selected = albumSortMode == option;
|
||||
return PopupMenuItem(
|
||||
value: option,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: selected ? context.primaryColor : Colors.transparent,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
option.label.tr(),
|
||||
style: TextStyle(
|
||||
color: selected ? context.primaryColor : null,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
void onSelected(AlbumSortMode mode) {
|
||||
final isAlreadySelected = albumSortMode == mode;
|
||||
// Switch direction
|
||||
if (isAlreadySelected) {
|
||||
return ref
|
||||
.read(albumSortOrderProvider.notifier)
|
||||
.changeSortDirection(!albumSortIsReverse);
|
||||
}
|
||||
return ref.read(albumSortByOptionsProvider.notifier).changeSortMode(mode);
|
||||
}
|
||||
|
||||
return PopupMenuButton(
|
||||
position: PopupMenuPosition.over,
|
||||
itemBuilder: buildOptions,
|
||||
onSelected: onSelected,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Icon(
|
||||
albumSortIsReverse
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
size: 14,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortMode.label.tr(),
|
||||
style: context.textTheme.labelLarge
|
||||
?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/extensions/object_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
|
||||
class AlbumThumbnailCard extends StatelessWidget {
|
||||
final Album album;
|
||||
final Function()? onTap;
|
||||
final bool showAssetCount;
|
||||
final Icon? emptyThumbnailPlaceholder;
|
||||
|
||||
/// Whether or not to show the owner of the album (or "Owned")
|
||||
/// in the subtitle of the album
|
||||
@@ -16,11 +20,11 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
this.showAssetCount = true,
|
||||
this.showOwner = false,
|
||||
this.emptyThumbnailPlaceholder,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
@@ -29,62 +33,6 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
builder: (context, constraints) {
|
||||
var cardSize = constraints.maxWidth;
|
||||
|
||||
buildEmptyThumbnail() {
|
||||
return Container(
|
||||
height: cardSize,
|
||||
width: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[800] : Colors.grey[200],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.no_photography,
|
||||
size: cardSize * .15,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAlbumThumbnail() => ImmichThumbnail(
|
||||
asset: album.thumbnail.value,
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
);
|
||||
|
||||
buildAlbumTextRow() {
|
||||
// Add the owner name to the subtitle
|
||||
String? owner;
|
||||
if (showOwner) {
|
||||
if (album.ownerId == Store.get(StoreKey.currentUser).id) {
|
||||
owner = 'album_thumbnail_owned'.tr();
|
||||
} else if (album.ownerName != null) {
|
||||
owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]);
|
||||
}
|
||||
}
|
||||
|
||||
return RichText(
|
||||
overflow: TextOverflow.fade,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: album.assetCount == 1
|
||||
? 'album_thumbnail_card_item'
|
||||
.tr(args: ['${album.assetCount}'])
|
||||
: 'album_thumbnail_card_items'
|
||||
.tr(args: ['${album.assetCount}']),
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
if (owner != null) const TextSpan(text: ' · '),
|
||||
if (owner != null)
|
||||
TextSpan(
|
||||
text: owner,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Flex(
|
||||
@@ -98,14 +46,46 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: album.thumbnail.value == null
|
||||
? buildEmptyThumbnail()
|
||||
: buildAlbumThumbnail(),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
child: album.thumbnail == null
|
||||
// Empty placeholder
|
||||
? Container(
|
||||
height: cardSize,
|
||||
width: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkTheme
|
||||
? const Color.fromARGB(255, 53, 53, 53)
|
||||
: const Color.fromARGB(
|
||||
255,
|
||||
203,
|
||||
203,
|
||||
203,
|
||||
),
|
||||
),
|
||||
color: isDarkTheme
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[50],
|
||||
),
|
||||
child: Center(
|
||||
child: emptyThumbnailPlaceholder ??
|
||||
Icon(
|
||||
Icons.no_photography,
|
||||
size: cardSize * .15,
|
||||
),
|
||||
),
|
||||
)
|
||||
// Thumbnail image
|
||||
: ImmichThumbnail(
|
||||
asset: album.thumbnail,
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
padding: const EdgeInsets.only(top: 8.0, left: 8.0),
|
||||
child: SizedBox(
|
||||
width: cardSize,
|
||||
child: Text(
|
||||
@@ -118,7 +98,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
buildAlbumTextRow(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: _AlbumTextRow(
|
||||
album: album,
|
||||
showAssetCount: showAssetCount,
|
||||
showOwner: showOwner,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -129,3 +116,55 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumTextRow extends StatelessWidget {
|
||||
final Album album;
|
||||
final bool showAssetCount;
|
||||
|
||||
/// Whether or not to show the owner of the album (or "Owned")
|
||||
/// in the subtitle of the album
|
||||
final bool showOwner;
|
||||
|
||||
const _AlbumTextRow({
|
||||
required this.album,
|
||||
required this.showAssetCount,
|
||||
required this.showOwner,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? owner;
|
||||
|
||||
if (showOwner) {
|
||||
if (album.tryCast<RemoteAlbum>()?.ownerId ==
|
||||
Store.get(StoreKey.currentUser).id) {
|
||||
owner = 'album_thumbnail_owned'.tr();
|
||||
} else if (album.tryCast<RemoteAlbum>()?.ownerName != null) {
|
||||
owner = 'album_thumbnail_shared_by'
|
||||
.tr(args: [(album as RemoteAlbum).ownerName!]);
|
||||
}
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
overflow: TextOverflow.fade,
|
||||
TextSpan(
|
||||
children: [
|
||||
if (showAssetCount)
|
||||
TextSpan(
|
||||
text: album.assetCount == 1
|
||||
? 'album_thumbnail_card_item'
|
||||
.tr(args: ['${album.assetCount}'])
|
||||
: 'album_thumbnail_card_items'
|
||||
.tr(args: ['${album.assetCount}']),
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
if (owner != null) const TextSpan(text: ' · '),
|
||||
TextSpan(
|
||||
text: owner,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -16,7 +16,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
@@ -61,7 +61,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap ??
|
||||
() {
|
||||
context.pushRoute(AlbumViewerRoute(albumId: album.id));
|
||||
context.pushRoute(RemoteAlbumViewerRoute(albumId: album.isarId));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
@@ -70,7 +70,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: album.thumbnail.value == null
|
||||
child: album.thumbnail == null
|
||||
? buildEmptyThumbnail()
|
||||
: buildAlbumThumbnail(),
|
||||
),
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
final FocusNode titleFocusNode;
|
||||
const AlbumViewerEditableTitle({
|
||||
super.key,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class LibraryNavButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Function() onClick;
|
||||
|
||||
const LibraryNavButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 180.0,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onClick,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: context.isDarkTheme
|
||||
? Colors.white
|
||||
: Colors.black.withAlpha(200),
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
backgroundColor:
|
||||
context.isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
icon: Icon(
|
||||
icon,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
-22
@@ -5,17 +5,17 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
|
||||
class AlbumViewerAppbar extends HookConsumerWidget
|
||||
class RemoteAlbumViewerAppbar extends HookConsumerWidget
|
||||
implements PreferredSizeWidget {
|
||||
const AlbumViewerAppbar({
|
||||
const RemoteAlbumViewerAppbar({
|
||||
super.key,
|
||||
required this.album,
|
||||
required this.userId,
|
||||
@@ -25,21 +25,20 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
required this.onActivities,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
final String userId;
|
||||
final FocusNode titleFocusNode;
|
||||
final Function(Album album)? onAddPhotos;
|
||||
final Function(Album album)? onAddUsers;
|
||||
final Function(Album album) onActivities;
|
||||
final Function(RemoteAlbum album)? onAddPhotos;
|
||||
final Function(RemoteAlbum album)? onAddUsers;
|
||||
final Function() onActivities;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||
final isProcessing = useProcessingOverlay();
|
||||
final comments = album.shared
|
||||
? ref.watch(activityStatisticsProvider(album.remoteId!))
|
||||
: 0;
|
||||
final comments =
|
||||
album.shared ? ref.watch(activityStatisticsProvider(album.id)) : 0;
|
||||
|
||||
deleteAlbum() async {
|
||||
isProcessing.value = true;
|
||||
@@ -51,7 +50,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
} else {
|
||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||
success =
|
||||
await ref.watch(remoteAlbumsProvider.notifier).deleteAlbum(album);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
|
||||
}
|
||||
@@ -172,7 +172,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share_rounded),
|
||||
onTap: () {
|
||||
context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId));
|
||||
context.pushRoute(SharedLinkEditRoute(albumId: album.id));
|
||||
context.pop();
|
||||
},
|
||||
title: const Text(
|
||||
@@ -228,9 +228,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
|
||||
Widget buildActivitiesButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
onActivities(album);
|
||||
},
|
||||
onPressed: () => onActivities(),
|
||||
icon: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -291,12 +289,11 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
actions: [
|
||||
if (album.shared && (album.activityEnabled || comments != 0))
|
||||
buildActivitiesButton(),
|
||||
if (album.isRemote)
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
onPressed: buildBottomSheet,
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
),
|
||||
IconButton(
|
||||
splashRadius: 25,
|
||||
onPressed: buildBottomSheet,
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -5,10 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
@@ -16,7 +16,7 @@ import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumOptionsPage extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
|
||||
const AlbumOptionsPage({super.key, required this.album});
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
||||
@@ -194,17 +194,17 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
createNonSharedAlbum() async {
|
||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||
var newAlbum = await ref.watch(remoteAlbumsProvider.notifier).createAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
selectedAssets.value,
|
||||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
selectedAssets.value = {};
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
|
||||
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
|
||||
context.replaceRoute(RemoteAlbumViewerRoute(albumId: newAlbum.isarId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
// ignore_for_file: prefer-sliver-prefix
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/sorted_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_sort_selector.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/library_nav_button.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
@@ -17,180 +24,37 @@ class LibraryPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
ref.read(localAlbumsProvider.notifier).getDeviceAlbums();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Widget buildSortButton() {
|
||||
return PopupMenuButton(
|
||||
position: PopupMenuPosition.over,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return AlbumSortMode.values
|
||||
.map<PopupMenuEntry<AlbumSortMode>>((option) {
|
||||
final selected = albumSortOption == option;
|
||||
return PopupMenuItem(
|
||||
value: option,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color:
|
||||
selected ? context.primaryColor : Colors.transparent,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
option.label.tr(),
|
||||
style: TextStyle(
|
||||
color: selected ? context.primaryColor : null,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (AlbumSortMode value) {
|
||||
final selected = albumSortOption == value;
|
||||
// Switch direction
|
||||
if (selected) {
|
||||
ref
|
||||
.read(albumSortOrderProvider.notifier)
|
||||
.changeSortDirection(!albumSortIsReverse);
|
||||
} else {
|
||||
ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Icon(
|
||||
albumSortIsReverse
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
size: 14,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.label.tr(),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: _LibraryAppBar(),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
_SilverLibraryNavigationButtons(),
|
||||
_SilverLibraryRemoteAlbumHeader(),
|
||||
_SilverLibraryRemoteAlbumGrid(),
|
||||
_SilverLibraryLocalAlbumHeader(),
|
||||
_SilverLibraryLocalAlbumGrid(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildCreateAlbumButton() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var cardSize = constraints.maxWidth;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkTheme
|
||||
? const Color.fromARGB(255, 53, 53, 53)
|
||||
: const Color.fromARGB(255, 203, 203, 203),
|
||||
),
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 28,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 16,
|
||||
),
|
||||
child: Text(
|
||||
'library_page_new_album',
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLibraryNavButton(
|
||||
String label,
|
||||
IconData icon,
|
||||
Function() onClick,
|
||||
) {
|
||||
return Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onClick,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: context.isDarkTheme
|
||||
? Colors.white
|
||||
: Colors.black.withAlpha(200),
|
||||
),
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||
side: BorderSide(
|
||||
color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
icon: Icon(
|
||||
icon,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final remote = albums.where((a) => a.isRemote).toList();
|
||||
final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
|
||||
final local = albums.where((a) => a.isLocal).toList();
|
||||
|
||||
Widget? shareTrashButton() {
|
||||
return trashEnabled
|
||||
class _LibraryAppBar extends ImmichAppBar {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
return ImmichAppBar(
|
||||
action: trashEnabled
|
||||
? InkWell(
|
||||
onTap: () => context.pushRoute(const TrashRoute()),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
@@ -199,133 +63,148 @@ class LibraryPage extends HookConsumerWidget {
|
||||
size: 25,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
action: shareTrashButton(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
top: 24.0,
|
||||
bottom: 12.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
buildLibraryNavButton(
|
||||
"library_page_favorites".tr(), Icons.favorite_border, () {
|
||||
context.navigateTo(const FavoritesRoute());
|
||||
}),
|
||||
const SizedBox(width: 12.0),
|
||||
buildLibraryNavButton(
|
||||
"library_page_archive".tr(), Icons.archive_outlined, () {
|
||||
context.navigateTo(const ArchiveRoute());
|
||||
}),
|
||||
],
|
||||
),
|
||||
class _SilverLibraryNavigationButtons extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
LibraryNavButton(
|
||||
label: "library_page_favorites",
|
||||
icon: Icons.favorite_border,
|
||||
onClick: () => context.navigateTo(const FavoritesRoute()),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
bottom: 20.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'library_page_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
buildSortButton(),
|
||||
],
|
||||
),
|
||||
LibraryNavButton(
|
||||
label: "library_page_archive",
|
||||
icon: Icons.archive_outlined,
|
||||
onClick: () => context.navigateTo(const ArchiveRoute()),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: sorted.length + 1,
|
||||
(context, index) {
|
||||
if (index == 0) {
|
||||
return buildCreateAlbumButton();
|
||||
}
|
||||
|
||||
return AlbumThumbnailCard(
|
||||
album: sorted[index - 1],
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(
|
||||
albumId: sorted[index - 1].id,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
bottom: 20.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'library_page_device_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: local.length,
|
||||
(context, index) => AlbumThumbnailCard(
|
||||
album: local[index],
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(
|
||||
albumId: local[index].id,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SilverLibraryRemoteAlbumHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'library_page_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
const AlbumSortSelector(sortModes: AlbumSortMode.values),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SilverLibraryRemoteAlbumGrid extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final remoteAlbums = ref.watch(remoteAlbumsProvider);
|
||||
final remoteSorted =
|
||||
ref.watch(sortedAlbumProvider(remoteAlbums.valueOrNull ?? []));
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
itemCount: remoteSorted.length + 1,
|
||||
itemBuilder: (ctx, index) {
|
||||
if (index == 0) {
|
||||
Album placeholder = LocalAlbum(
|
||||
id: 'Placeholder',
|
||||
name: 'library_page_new_album'.tr(),
|
||||
modifiedAt: DateTime.now(),
|
||||
);
|
||||
return AlbumThumbnailCard(
|
||||
album: placeholder,
|
||||
showAssetCount: false,
|
||||
emptyThumbnailPlaceholder: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 28,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
onTap: () =>
|
||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
|
||||
);
|
||||
}
|
||||
final remoteAlbum = remoteSorted[index - 1];
|
||||
|
||||
return AlbumThumbnailCard(
|
||||
album: remoteAlbum,
|
||||
onTap: () => context
|
||||
.pushRoute(RemoteAlbumViewerRoute(albumId: remoteAlbum.isarId)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SilverLibraryLocalAlbumHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
||||
child: Text(
|
||||
'library_page_device_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SilverLibraryLocalAlbumGrid extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final localAlbums = ref.watch(localAlbumsProvider).valueOrNull ?? [];
|
||||
final localWithoutRecents =
|
||||
localAlbums.where((e) => e.id != LocalAlbum.isAllId).toList();
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
itemCount: localWithoutRecents.length,
|
||||
itemBuilder: (ctx, index) => AlbumThumbnailCard(
|
||||
album: localWithoutRecents[index],
|
||||
onTap: () => context.pushRoute(
|
||||
LocalAlbumViewerRoute(album: localWithoutRecents[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LocalAlbumViewerPage extends HookConsumerWidget {
|
||||
final LocalAlbum album;
|
||||
final bool selectEnabled;
|
||||
|
||||
const LocalAlbumViewerPage({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.selectEnabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: MultiselectGrid(
|
||||
topWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
child: Text(
|
||||
album.name,
|
||||
style: context.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
renderListProvider: localAlbumRenderlistProvider(album.isarId),
|
||||
selectedEnabled: selectEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
-28
@@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
@@ -17,9 +18,8 @@ import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart
|
||||
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/remote_album_viewer_appbar.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
|
||||
@@ -28,15 +28,15 @@ import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumViewerPage extends HookConsumerWidget {
|
||||
class RemoteAlbumViewerPage extends HookConsumerWidget {
|
||||
final int albumId;
|
||||
|
||||
const AlbumViewerPage({super.key, required this.albumId});
|
||||
const RemoteAlbumViewerPage({super.key, required this.albumId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
FocusNode titleFocusNode = useFocusNode();
|
||||
final album = ref.watch(albumWatcher(albumId));
|
||||
final album = ref.watch(remoteAlbumWatcher(albumId));
|
||||
// Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
|
||||
ref.listen(currentAlbumProvider, (_, __) {});
|
||||
album.whenData(
|
||||
@@ -67,7 +67,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
|
||||
/// Find out if the assets in album exist on the device
|
||||
/// If they exist, add to selected asset state to show they are already selected.
|
||||
void onAddPhotosPressed(Album albumInfo) async {
|
||||
void onAddPhotosPressed(RemoteAlbum albumInfo) async {
|
||||
AssetSelectionPageResult? returnPayload =
|
||||
await context.pushRoute<AssetSelectionPageResult?>(
|
||||
AssetSelectionRoute(
|
||||
@@ -90,7 +90,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void onAddUsersPressed(Album album) async {
|
||||
void onAddUsersPressed(RemoteAlbum album) async {
|
||||
List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
|
||||
SelectAdditionalUserForSharingRoute(album: album),
|
||||
);
|
||||
@@ -106,7 +106,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildControlButton(Album album) {
|
||||
Widget buildControlButton(RemoteAlbum album) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
||||
child: SizedBox(
|
||||
@@ -131,16 +131,16 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTitle(Album album) {
|
||||
Widget buildTitle(RemoteAlbum album) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 24),
|
||||
child: userId == album.ownerId && album.isRemote
|
||||
child: userId == album.ownerId
|
||||
? AlbumViewerEditableTitle(
|
||||
album: album,
|
||||
titleFocusNode: titleFocusNode,
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 8.0, bottom: 24),
|
||||
child: Text(
|
||||
album.name,
|
||||
style: context.textTheme.headlineMedium,
|
||||
@@ -149,7 +149,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAlbumDateRange(Album album) {
|
||||
Widget buildAlbumDateRange(RemoteAlbum album) {
|
||||
final DateTime? startDate = album.startDate;
|
||||
final DateTime? endDate = album.endDate;
|
||||
|
||||
@@ -183,7 +183,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSharedUserIconsRow(Album album) {
|
||||
Widget buildSharedUserIconsRow(RemoteAlbum album) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||
child: SizedBox(
|
||||
@@ -207,49 +207,42 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHeader(Album album) {
|
||||
Widget buildHeader(RemoteAlbum album) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildTitle(album),
|
||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||
if (album.shared) buildSharedUserIconsRow(album),
|
||||
if (album.assets.isNotEmpty) buildAlbumDateRange(album),
|
||||
if (album.shared && album.sharedUsers.isNotEmpty)
|
||||
buildSharedUserIconsRow(album),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
onActivitiesPressed(Album album) {
|
||||
if (album.remoteId != null) {
|
||||
context.pushRoute(
|
||||
const ActivitiesRoute(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ref.watch(multiselectProvider)
|
||||
? null
|
||||
: album.when(
|
||||
data: (data) => AlbumViewerAppbar(
|
||||
data: (data) => RemoteAlbumViewerAppbar(
|
||||
titleFocusNode: titleFocusNode,
|
||||
album: data,
|
||||
userId: userId,
|
||||
onAddPhotos: onAddPhotosPressed,
|
||||
onAddUsers: onAddUsersPressed,
|
||||
onActivities: onActivitiesPressed,
|
||||
onActivities: () => context.pushRoute(const ActivitiesRoute()),
|
||||
),
|
||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||
loading: () => AppBar(),
|
||||
),
|
||||
body: album.widgetWhen(
|
||||
onData: (data) => MultiselectGrid(
|
||||
renderListProvider: albumRenderlistProvider(albumId),
|
||||
renderListProvider: remoteAlbumRenderlistProvider(albumId),
|
||||
topWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildHeader(data),
|
||||
if (data.isRemote) buildControlButton(data),
|
||||
buildControlButton(data),
|
||||
],
|
||||
),
|
||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||
@@ -5,14 +5,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
|
||||
@RoutePage<List<String>?>()
|
||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
|
||||
const SelectAdditionalUserForSharingPage({super.key, required this.album});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||
@@ -51,7 +52,9 @@ class SharingPage extends HookConsumerWidget {
|
||||
album: sharedAlbums[index],
|
||||
showOwner: true,
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
||||
RemoteAlbumViewerRoute(
|
||||
albumId: sharedAlbums[index].isarId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -65,7 +68,7 @@ class SharingPage extends HookConsumerWidget {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final album = sharedAlbums[index];
|
||||
final album = sharedAlbums[index] as RemoteAlbum;
|
||||
final isOwner = album.ownerId == userId;
|
||||
|
||||
return ListTile(
|
||||
@@ -73,7 +76,7 @@ class SharingPage extends HookConsumerWidget {
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ImmichThumbnail(
|
||||
asset: album.thumbnail.value,
|
||||
asset: album.thumbnail,
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
@@ -99,8 +102,11 @@ class SharingPage extends HookConsumerWidget {
|
||||
style: context.textTheme.bodyMedium,
|
||||
)
|
||||
: null,
|
||||
onTap: () => context
|
||||
.pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
|
||||
onTap: () => context.pushRoute(
|
||||
RemoteAlbumViewerRoute(
|
||||
albumId: sharedAlbums[index].isarId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: sharedAlbums.length,
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album_service.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/local_album.service.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
@@ -14,7 +15,7 @@ import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
final ImageViewerService _imageViewerService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
final LocalAlbumService _albumService;
|
||||
|
||||
ImageViewerStateNotifier(
|
||||
this._imageViewerService,
|
||||
@@ -83,6 +84,6 @@ final imageViewerStateProvider =
|
||||
((ref) => ImageViewerStateNotifier(
|
||||
ref.watch(imageViewerServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(localAlbumServiceProvider),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/object_extensions.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
@@ -39,11 +41,15 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
const double iconSize = 22.0;
|
||||
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
||||
final album = ref.watch(currentAlbumProvider);
|
||||
final comments = album != null &&
|
||||
album.remoteId != null &&
|
||||
asset.remoteId != null
|
||||
? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId))
|
||||
: 0;
|
||||
final comments =
|
||||
album != null && album is RemoteAlbum && asset.remoteId != null
|
||||
? ref.watch(
|
||||
activityStatisticsProvider(
|
||||
album.tryCast<RemoteAlbum>()!.id,
|
||||
asset.remoteId,
|
||||
),
|
||||
)
|
||||
: 0;
|
||||
|
||||
Widget buildFavoriteButton(a) {
|
||||
return IconButton(
|
||||
@@ -171,7 +177,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
if (asset.isRemote && !asset.isLocal && !asset.isOffline && isOwner)
|
||||
buildDownloadButton(),
|
||||
if (asset.isRemote && (isOwner || isPartner)) buildAddToAlbumButtom(),
|
||||
if (album != null && album.shared) buildActivitiesButton(),
|
||||
if (album != null && album is RemoteAlbum && album.shared)
|
||||
buildActivitiesButton(),
|
||||
buildMoreInfoButton(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||
@@ -351,7 +352,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
handleActivities() {
|
||||
if (album != null && album.shared && album.remoteId != null) {
|
||||
if (album != null && album is RemoteAlbum && album.shared) {
|
||||
context.pushRoute(const ActivitiesRoute());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,26 @@ import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/main.dart';
|
||||
import 'package:immich_mobile/modules/album/services/local_album.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
@@ -346,48 +350,43 @@ class BackgroundService {
|
||||
AppSettingsService settingService = AppSettingsService();
|
||||
BackupService backupService = BackupService(apiService, db, settingService);
|
||||
AppSettingsService settingsService = AppSettingsService();
|
||||
|
||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||
if (selectedAlbums.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
HashService hashService = HashService(db, this);
|
||||
SyncService syncService = SyncService(db);
|
||||
BackupAlbumService backupAlbumService = BackupAlbumService(db);
|
||||
LocalAlbumService localAlbumService =
|
||||
LocalAlbumService(db, hashService, syncService, backupAlbumService);
|
||||
|
||||
await PhotoManager.setIgnorePermissionCheck(true);
|
||||
|
||||
do {
|
||||
await localAlbumService.refreshDeviceAlbums();
|
||||
|
||||
final idsToBackup = await db.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.idProperty()
|
||||
.findAll();
|
||||
final localAssetsToBackup = await db.assets
|
||||
.where()
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.anyOf(idsToBackup, (q, id) => q.localIdEqualTo(id))
|
||||
.findAll();
|
||||
|
||||
final toUpload =
|
||||
await backupService.remoteAlreadyUploaded(localAssetsToBackup);
|
||||
if (toUpload.isEmpty) {
|
||||
debugPrint("No Asset On Device - Abort Backup Process");
|
||||
return false;
|
||||
}
|
||||
|
||||
final bool backupOk = await _runBackup(
|
||||
backupService,
|
||||
settingsService,
|
||||
selectedAlbums,
|
||||
excludedAlbums,
|
||||
toUpload.toList(),
|
||||
);
|
||||
if (backupOk) {
|
||||
await Store.delete(StoreKey.backupFailedSince);
|
||||
final backupAlbums = [...selectedAlbums, ...excludedAlbums];
|
||||
backupAlbums.sortBy((e) => e.id);
|
||||
db.writeTxnSync(() {
|
||||
final dbAlbums = db.backupAlbums.where().sortById().findAllSync();
|
||||
final List<int> toDelete = [];
|
||||
final List<BackupAlbum> toUpsert = [];
|
||||
// stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
|
||||
diffSortedListsSync(
|
||||
dbAlbums,
|
||||
backupAlbums,
|
||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||
both: (BackupAlbum a, BackupAlbum b) {
|
||||
a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
|
||||
? a.lastBackup
|
||||
: b.lastBackup;
|
||||
toUpsert.add(a);
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (BackupAlbum a) => toUpsert.add(a),
|
||||
onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
|
||||
);
|
||||
db.backupAlbums.deleteAllSync(toDelete);
|
||||
db.backupAlbums.putAllSync(toUpsert);
|
||||
});
|
||||
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
|
||||
Store.put(StoreKey.backupFailedSince, DateTime.now());
|
||||
return false;
|
||||
@@ -402,8 +401,7 @@ class BackgroundService {
|
||||
Future<bool> _runBackup(
|
||||
BackupService backupService,
|
||||
AppSettingsService settingsService,
|
||||
List<BackupAlbum> selectedAlbums,
|
||||
List<BackupAlbum> excludedAlbums,
|
||||
List<Asset> toUpload,
|
||||
) async {
|
||||
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
|
||||
final bool notifyTotalProgress = settingsService
|
||||
@@ -411,32 +409,10 @@ class BackgroundService {
|
||||
final bool notifySingleProgress = settingsService
|
||||
.getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
|
||||
|
||||
if (_canceledBySystem) {
|
||||
if (_canceledBySystem || toUpload.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<AssetEntity> toUpload = await backupService.buildUploadCandidates(
|
||||
selectedAlbums,
|
||||
excludedAlbums,
|
||||
);
|
||||
|
||||
try {
|
||||
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
|
||||
} catch (e) {
|
||||
_showErrorNotification(
|
||||
title: "backup_background_service_error_title".tr(),
|
||||
content: "backup_background_service_connection_failed_message".tr(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_canceledBySystem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toUpload.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
_assetsToUploadCount = toUpload.length;
|
||||
_uploadedAssetsCount = 0;
|
||||
_updateNotification(
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AvailableAlbum {
|
||||
final AssetPathEntity albumEntity;
|
||||
final DateTime? lastBackup;
|
||||
final Uint8List? thumbnailData;
|
||||
AvailableAlbum({
|
||||
required this.albumEntity,
|
||||
this.lastBackup,
|
||||
this.thumbnailData,
|
||||
});
|
||||
|
||||
AvailableAlbum copyWith({
|
||||
AssetPathEntity? albumEntity,
|
||||
DateTime? lastBackup,
|
||||
Uint8List? thumbnailData,
|
||||
}) {
|
||||
return AvailableAlbum(
|
||||
albumEntity: albumEntity ?? this.albumEntity,
|
||||
lastBackup: lastBackup ?? this.lastBackup,
|
||||
thumbnailData: thumbnailData ?? this.thumbnailData,
|
||||
);
|
||||
}
|
||||
|
||||
String get name => albumEntity.name;
|
||||
|
||||
Future<int> get assetCount => albumEntity.assetCountAsync;
|
||||
|
||||
String get id => albumEntity.id;
|
||||
|
||||
bool get isAll => albumEntity.isAll;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup, thumbnailData: $thumbnailData)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is AvailableAlbum && other.albumEntity == albumEntity;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => albumEntity.hashCode;
|
||||
}
|
||||
@@ -1,22 +1,50 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'backup_album.model.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class BackupAlbum {
|
||||
String id;
|
||||
DateTime lastBackup;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
BackupSelection selection;
|
||||
|
||||
BackupAlbum(this.id, this.lastBackup, this.selection);
|
||||
|
||||
Id get isarId => fastHash(id);
|
||||
}
|
||||
|
||||
enum BackupSelection {
|
||||
none,
|
||||
select,
|
||||
exclude;
|
||||
}
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class BackupAlbum {
|
||||
Id get isarId => fastHash(id);
|
||||
String id;
|
||||
@enumerated
|
||||
BackupSelection selection;
|
||||
|
||||
static const albumLinkId = 'album';
|
||||
final album = IsarLink<LocalAlbum>();
|
||||
|
||||
BackupAlbum({
|
||||
required this.id,
|
||||
this.selection = BackupSelection.none,
|
||||
});
|
||||
|
||||
BackupAlbum copyWith({
|
||||
String? id,
|
||||
BackupSelection? selection,
|
||||
}) {
|
||||
return BackupAlbum(
|
||||
id: id ?? this.id,
|
||||
selection: selection ?? this.selection,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'BackupAlbum(id: $id, selection: $selection)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant BackupAlbum other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id && other.selection == selection;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ selection.hashCode;
|
||||
}
|
||||
|
||||
+129
-108
@@ -17,16 +17,16 @@ const BackupAlbumSchema = CollectionSchema(
|
||||
name: r'BackupAlbum',
|
||||
id: 8308487201128361847,
|
||||
properties: {
|
||||
r'id': PropertySchema(
|
||||
r'hashCode': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hashCode',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 1,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'lastBackup': PropertySchema(
|
||||
id: 1,
|
||||
name: r'lastBackup',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'selection': PropertySchema(
|
||||
id: 2,
|
||||
name: r'selection',
|
||||
@@ -40,7 +40,14 @@ const BackupAlbumSchema = CollectionSchema(
|
||||
deserializeProp: _backupAlbumDeserializeProp,
|
||||
idName: r'isarId',
|
||||
indexes: {},
|
||||
links: {},
|
||||
links: {
|
||||
r'album': LinkSchema(
|
||||
id: 4803574038667272895,
|
||||
name: r'album',
|
||||
target: r'LocalAlbum',
|
||||
single: true,
|
||||
)
|
||||
},
|
||||
embeddedSchemas: {},
|
||||
getId: _backupAlbumGetId,
|
||||
getLinks: _backupAlbumGetLinks,
|
||||
@@ -64,8 +71,8 @@ void _backupAlbumSerialize(
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeString(offsets[0], object.id);
|
||||
writer.writeDateTime(offsets[1], object.lastBackup);
|
||||
writer.writeLong(offsets[0], object.hashCode);
|
||||
writer.writeString(offsets[1], object.id);
|
||||
writer.writeByte(offsets[2], object.selection.index);
|
||||
}
|
||||
|
||||
@@ -76,10 +83,10 @@ BackupAlbum _backupAlbumDeserialize(
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = BackupAlbum(
|
||||
reader.readString(offsets[0]),
|
||||
reader.readDateTime(offsets[1]),
|
||||
_BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ??
|
||||
BackupSelection.none,
|
||||
id: reader.readString(offsets[1]),
|
||||
selection:
|
||||
_BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ??
|
||||
BackupSelection.none,
|
||||
);
|
||||
return object;
|
||||
}
|
||||
@@ -92,9 +99,9 @@ P _backupAlbumDeserializeProp<P>(
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readString(offset)) as P;
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 2:
|
||||
return (_BackupAlbumselectionValueEnumMap[
|
||||
reader.readByteOrNull(offset)] ??
|
||||
@@ -120,11 +127,13 @@ Id _backupAlbumGetId(BackupAlbum object) {
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _backupAlbumGetLinks(BackupAlbum object) {
|
||||
return [];
|
||||
return [object.album];
|
||||
}
|
||||
|
||||
void _backupAlbumAttach(
|
||||
IsarCollection<dynamic> col, Id id, BackupAlbum object) {}
|
||||
IsarCollection<dynamic> col, Id id, BackupAlbum object) {
|
||||
object.album.attach(col, col.isar.collection<LocalAlbum>(), r'album', id);
|
||||
}
|
||||
|
||||
extension BackupAlbumQueryWhereSort
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QWhere> {
|
||||
@@ -209,6 +218,61 @@ extension BackupAlbumQueryWhere
|
||||
|
||||
extension BackupAlbumQueryFilter
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> hashCodeEqualTo(
|
||||
int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hashCode',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
hashCodeGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hashCode',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
hashCodeLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hashCode',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> hashCodeBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hashCode',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
@@ -393,62 +457,6 @@ extension BackupAlbumQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
lastBackupEqualTo(DateTime value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'lastBackup',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
lastBackupGreaterThan(
|
||||
DateTime value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'lastBackup',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
lastBackupLessThan(
|
||||
DateTime value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'lastBackup',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
lastBackupBetween(
|
||||
DateTime lower,
|
||||
DateTime upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'lastBackup',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
selectionEqualTo(BackupSelection value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -510,10 +518,35 @@ extension BackupAlbumQueryObject
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {}
|
||||
|
||||
extension BackupAlbumQueryLinks
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {}
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> album(
|
||||
FilterQuery<LocalAlbum> q) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.link(q, r'album');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> albumIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.linkLength(r'album', 0, true, 0, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension BackupAlbumQuerySortBy
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QSortBy> {
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByHashCode() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hashCode', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByHashCodeDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hashCode', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
@@ -526,18 +559,6 @@ extension BackupAlbumQuerySortBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackup() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'lastBackup', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackupDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'lastBackup', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortBySelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'selection', Sort.asc);
|
||||
@@ -553,6 +574,18 @@ extension BackupAlbumQuerySortBy
|
||||
|
||||
extension BackupAlbumQuerySortThenBy
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QSortThenBy> {
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByHashCode() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hashCode', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByHashCodeDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'hashCode', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
@@ -577,18 +610,6 @@ extension BackupAlbumQuerySortThenBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackup() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'lastBackup', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackupDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'lastBackup', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenBySelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'selection', Sort.asc);
|
||||
@@ -604,6 +625,12 @@ extension BackupAlbumQuerySortThenBy
|
||||
|
||||
extension BackupAlbumQueryWhereDistinct
|
||||
on QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> {
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctByHashCode() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hashCode');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -611,12 +638,6 @@ extension BackupAlbumQueryWhereDistinct
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctByLastBackup() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'lastBackup');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctBySelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'selection');
|
||||
@@ -632,15 +653,15 @@ extension BackupAlbumQueryProperty
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, String, QQueryOperations> idProperty() {
|
||||
QueryBuilder<BackupAlbum, int, QQueryOperations> hashCodeProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
return query.addPropertyName(r'hashCode');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, DateTime, QQueryOperations> lastBackupProperty() {
|
||||
QueryBuilder<BackupAlbum, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'lastBackup');
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
|
||||
class BackupAlbumState {
|
||||
final List<BackupAlbum> selectedBackupAlbums;
|
||||
final List<BackupAlbum> excludedBackupAlbums;
|
||||
|
||||
const BackupAlbumState({
|
||||
required this.selectedBackupAlbums,
|
||||
required this.excludedBackupAlbums,
|
||||
});
|
||||
|
||||
BackupAlbumState copyWith({
|
||||
List<BackupAlbum>? selectedBackupAlbums,
|
||||
List<BackupAlbum>? excludedBackupAlbums,
|
||||
}) {
|
||||
return BackupAlbumState(
|
||||
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
||||
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'BackupAlbumState(selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant BackupAlbumState other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return listEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||
listEquals(other.excludedBackupAlbums, excludedBackupAlbums);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
selectedBackupAlbums.hashCode ^ excludedBackupAlbums.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
class BackupSetting {
|
||||
final bool autoBackup;
|
||||
final bool backgroundBackup;
|
||||
final bool backupRequireWifi;
|
||||
final bool backupRequireCharging;
|
||||
final int backupTriggerDelay;
|
||||
|
||||
const BackupSetting({
|
||||
required this.autoBackup,
|
||||
required this.backgroundBackup,
|
||||
required this.backupRequireWifi,
|
||||
required this.backupRequireCharging,
|
||||
required this.backupTriggerDelay,
|
||||
});
|
||||
|
||||
BackupSetting copyWith({
|
||||
bool? autoBackup,
|
||||
bool? backgroundBackup,
|
||||
bool? backupRequireWifi,
|
||||
bool? backupRequireCharging,
|
||||
int? backupTriggerDelay,
|
||||
}) {
|
||||
return BackupSetting(
|
||||
autoBackup: autoBackup ?? this.autoBackup,
|
||||
backgroundBackup: backgroundBackup ?? this.backgroundBackup,
|
||||
backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi,
|
||||
backupRequireCharging:
|
||||
backupRequireCharging ?? this.backupRequireCharging,
|
||||
backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupSettings(autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant BackupSetting other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.autoBackup == autoBackup &&
|
||||
other.backgroundBackup == backgroundBackup &&
|
||||
other.backupRequireWifi == backupRequireWifi &&
|
||||
other.backupRequireCharging == backupRequireCharging &&
|
||||
other.backupTriggerDelay == backupTriggerDelay;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return autoBackup.hashCode ^
|
||||
backgroundBackup.hashCode ^
|
||||
backupRequireWifi.hashCode ^
|
||||
backupRequireCharging.hashCode ^
|
||||
backupTriggerDelay.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart';
|
||||
|
||||
enum BackUpProgressEnum {
|
||||
idle,
|
||||
@@ -19,144 +12,60 @@ enum BackUpProgressEnum {
|
||||
class BackUpState {
|
||||
// enum
|
||||
final BackUpProgressEnum backupProgress;
|
||||
final List<String> allAssetsInDatabase;
|
||||
final double progressInPercentage;
|
||||
final double iCloudDownloadProgress;
|
||||
final CancellationToken cancelToken;
|
||||
final ServerDiskInfo serverInfo;
|
||||
final bool autoBackup;
|
||||
final bool backgroundBackup;
|
||||
final bool backupRequireWifi;
|
||||
final bool backupRequireCharging;
|
||||
final int backupTriggerDelay;
|
||||
|
||||
/// All available albums on the device
|
||||
final List<AvailableAlbum> availableAlbums;
|
||||
final Set<AvailableAlbum> selectedBackupAlbums;
|
||||
final Set<AvailableAlbum> excludedBackupAlbums;
|
||||
|
||||
/// Assets that are not overlapping in selected backup albums and excluded backup albums
|
||||
final Set<AssetEntity> allUniqueAssets;
|
||||
|
||||
/// All assets from the selected albums that have been backup
|
||||
final Set<String> selectedAlbumsBackupAssetsIds;
|
||||
|
||||
// Current Backup Asset
|
||||
final CurrentUploadAsset currentUploadAsset;
|
||||
|
||||
const BackUpState({
|
||||
required this.backupProgress,
|
||||
required this.allAssetsInDatabase,
|
||||
required this.progressInPercentage,
|
||||
required this.iCloudDownloadProgress,
|
||||
required this.cancelToken,
|
||||
required this.serverInfo,
|
||||
required this.autoBackup,
|
||||
required this.backgroundBackup,
|
||||
required this.backupRequireWifi,
|
||||
required this.backupRequireCharging,
|
||||
required this.backupTriggerDelay,
|
||||
required this.availableAlbums,
|
||||
required this.selectedBackupAlbums,
|
||||
required this.excludedBackupAlbums,
|
||||
required this.allUniqueAssets,
|
||||
required this.selectedAlbumsBackupAssetsIds,
|
||||
required this.currentUploadAsset,
|
||||
});
|
||||
|
||||
BackUpState copyWith({
|
||||
BackUpProgressEnum? backupProgress,
|
||||
List<String>? allAssetsInDatabase,
|
||||
double? progressInPercentage,
|
||||
double? iCloudDownloadProgress,
|
||||
CancellationToken? cancelToken,
|
||||
ServerDiskInfo? serverInfo,
|
||||
bool? autoBackup,
|
||||
bool? backgroundBackup,
|
||||
bool? backupRequireWifi,
|
||||
bool? backupRequireCharging,
|
||||
int? backupTriggerDelay,
|
||||
List<AvailableAlbum>? availableAlbums,
|
||||
Set<AvailableAlbum>? selectedBackupAlbums,
|
||||
Set<AvailableAlbum>? excludedBackupAlbums,
|
||||
Set<AssetEntity>? allUniqueAssets,
|
||||
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||
CurrentUploadAsset? currentUploadAsset,
|
||||
}) {
|
||||
return BackUpState(
|
||||
backupProgress: backupProgress ?? this.backupProgress,
|
||||
allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
|
||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||
iCloudDownloadProgress:
|
||||
iCloudDownloadProgress ?? this.iCloudDownloadProgress,
|
||||
cancelToken: cancelToken ?? this.cancelToken,
|
||||
serverInfo: serverInfo ?? this.serverInfo,
|
||||
autoBackup: autoBackup ?? this.autoBackup,
|
||||
backgroundBackup: backgroundBackup ?? this.backgroundBackup,
|
||||
backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi,
|
||||
backupRequireCharging:
|
||||
backupRequireCharging ?? this.backupRequireCharging,
|
||||
backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay,
|
||||
availableAlbums: availableAlbums ?? this.availableAlbums,
|
||||
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
||||
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
||||
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
||||
selectedAlbumsBackupAssetsIds:
|
||||
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
||||
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
||||
return 'BackUpState(backupProgress: $backupProgress, progressInPercentage: $progressInPercentage, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant BackUpState other) {
|
||||
if (identical(this, other)) return true;
|
||||
final collectionEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return other.backupProgress == backupProgress &&
|
||||
collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) &&
|
||||
other.progressInPercentage == progressInPercentage &&
|
||||
other.iCloudDownloadProgress == iCloudDownloadProgress &&
|
||||
other.cancelToken == cancelToken &&
|
||||
other.serverInfo == serverInfo &&
|
||||
other.autoBackup == autoBackup &&
|
||||
other.backgroundBackup == backgroundBackup &&
|
||||
other.backupRequireWifi == backupRequireWifi &&
|
||||
other.backupRequireCharging == backupRequireCharging &&
|
||||
other.backupTriggerDelay == backupTriggerDelay &&
|
||||
collectionEquals(other.availableAlbums, availableAlbums) &&
|
||||
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
||||
collectionEquals(
|
||||
other.selectedAlbumsBackupAssetsIds,
|
||||
selectedAlbumsBackupAssetsIds,
|
||||
) &&
|
||||
other.currentUploadAsset == currentUploadAsset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return backupProgress.hashCode ^
|
||||
allAssetsInDatabase.hashCode ^
|
||||
progressInPercentage.hashCode ^
|
||||
iCloudDownloadProgress.hashCode ^
|
||||
cancelToken.hashCode ^
|
||||
serverInfo.hashCode ^
|
||||
autoBackup.hashCode ^
|
||||
backgroundBackup.hashCode ^
|
||||
backupRequireWifi.hashCode ^
|
||||
backupRequireCharging.hashCode ^
|
||||
backupTriggerDelay.hashCode ^
|
||||
availableAlbums.hashCode ^
|
||||
selectedBackupAlbums.hashCode ^
|
||||
excludedBackupAlbums.hashCode ^
|
||||
allUniqueAssets.hashCode ^
|
||||
selectedAlbumsBackupAssetsIds.hashCode ^
|
||||
currentUploadAsset.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class DeviceAssetState {
|
||||
final int uniqueAssetsToBackup;
|
||||
final int backedUpAssets;
|
||||
|
||||
const DeviceAssetState({
|
||||
required this.uniqueAssetsToBackup,
|
||||
required this.backedUpAssets,
|
||||
});
|
||||
|
||||
int get assetsRemaining => uniqueAssetsToBackup - backedUpAssets;
|
||||
|
||||
DeviceAssetState copyWith({
|
||||
int? uniqueAssetsToBackup,
|
||||
int? backedUpAssets,
|
||||
}) {
|
||||
return DeviceAssetState(
|
||||
uniqueAssetsToBackup: uniqueAssetsToBackup ?? this.uniqueAssetsToBackup,
|
||||
backedUpAssets: backedUpAssets ?? this.backedUpAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'DeviceAssetState(uniqueAssetsToBackup: $uniqueAssetsToBackup, backedUpAssets: $backedUpAssets)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DeviceAssetState other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.uniqueAssetsToBackup == uniqueAssetsToBackup &&
|
||||
other.backedUpAssets == backedUpAssets;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => uniqueAssetsToBackup.hashCode ^ backedUpAssets.hashCode;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'duplicated_asset.model.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class DuplicatedAsset {
|
||||
String id;
|
||||
DuplicatedAsset(this.id);
|
||||
Id get isarId => fastHash(id);
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'duplicated_asset.model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetDuplicatedAssetCollection on Isar {
|
||||
IsarCollection<DuplicatedAsset> get duplicatedAssets => this.collection();
|
||||
}
|
||||
|
||||
const DuplicatedAssetSchema = CollectionSchema(
|
||||
name: r'DuplicatedAsset',
|
||||
id: -2679334728174694496,
|
||||
properties: {
|
||||
r'id': PropertySchema(
|
||||
id: 0,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _duplicatedAssetEstimateSize,
|
||||
serialize: _duplicatedAssetSerialize,
|
||||
deserialize: _duplicatedAssetDeserialize,
|
||||
deserializeProp: _duplicatedAssetDeserializeProp,
|
||||
idName: r'isarId',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _duplicatedAssetGetId,
|
||||
getLinks: _duplicatedAssetGetLinks,
|
||||
attach: _duplicatedAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _duplicatedAssetEstimateSize(
|
||||
DuplicatedAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.id.length * 3;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _duplicatedAssetSerialize(
|
||||
DuplicatedAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeString(offsets[0], object.id);
|
||||
}
|
||||
|
||||
DuplicatedAsset _duplicatedAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = DuplicatedAsset(
|
||||
reader.readString(offsets[0]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _duplicatedAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _duplicatedAssetGetId(DuplicatedAsset object) {
|
||||
return object.isarId;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _duplicatedAssetGetLinks(DuplicatedAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _duplicatedAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, DuplicatedAsset object) {}
|
||||
|
||||
extension DuplicatedAssetQueryWhereSort
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhere> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhere> anyIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQueryWhere
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhereClause> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
|
||||
isarIdEqualTo(Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: isarId,
|
||||
upper: isarId,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
|
||||
isarIdNotEqualTo(Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
|
||||
isarIdGreaterThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
|
||||
isarIdLessThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
|
||||
isarIdBetween(
|
||||
Id lowerIsarId,
|
||||
Id upperIsarId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerIsarId,
|
||||
includeLower: includeLower,
|
||||
upper: upperIsarId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQueryFilter
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'id',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
idIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
isarIdEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
isarIdGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
isarIdLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
|
||||
isarIdBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'isarId',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQueryObject
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {}
|
||||
|
||||
extension DuplicatedAssetQueryLinks
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {}
|
||||
|
||||
extension DuplicatedAssetQuerySortBy
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortBy> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQuerySortThenBy
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortThenBy> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy>
|
||||
thenByIsarIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQueryWhereDistinct
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> {
|
||||
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DuplicatedAssetQueryProperty
|
||||
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QQueryProperty> {
|
||||
QueryBuilder<DuplicatedAsset, int, QQueryOperations> isarIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isarId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DuplicatedAsset, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class BulkUploadCheckResponse {
|
||||
final List<String> toBeUploaded;
|
||||
final List<String> duplicates;
|
||||
|
||||
const BulkUploadCheckResponse({
|
||||
required this.toBeUploaded,
|
||||
required this.duplicates,
|
||||
});
|
||||
|
||||
BulkUploadCheckResponse copyWith({
|
||||
List<String>? toBeUploaded,
|
||||
List<String>? duplicates,
|
||||
}) {
|
||||
return BulkUploadCheckResponse(
|
||||
toBeUploaded: toBeUploaded ?? this.toBeUploaded,
|
||||
duplicates: duplicates ?? this.duplicates,
|
||||
);
|
||||
}
|
||||
|
||||
static BulkUploadCheckResponse fromDto(AssetBulkUploadCheckResponseDto dto) {
|
||||
final duplicates = <String>[];
|
||||
final toBeUploaded = <String>[];
|
||||
for (final result in dto.results) {
|
||||
if (result.action == AssetBulkUploadCheckResultActionEnum.accept) {
|
||||
toBeUploaded.add(result.id);
|
||||
} else if (result.action == AssetBulkUploadCheckResultActionEnum.reject &&
|
||||
result.reason == AssetBulkUploadCheckResultReasonEnum.duplicate) {
|
||||
duplicates.add(result.id);
|
||||
}
|
||||
}
|
||||
return BulkUploadCheckResponse(
|
||||
toBeUploaded: toBeUploaded.toList(),
|
||||
duplicates: duplicates.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'BulkUploadCheckResponse(toBeUploaded: $toBeUploaded, duplicates: $duplicates)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant BulkUploadCheckResponse other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return listEquals(other.toBeUploaded, toBeUploaded) &&
|
||||
listEquals(other.duplicates, duplicates);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => toBeUploaded.hashCode ^ duplicates.hashCode;
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
@@ -29,7 +29,7 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
BackupNotifier(
|
||||
this._backupService,
|
||||
this._serverInfoService,
|
||||
this._serverInfoNotifier,
|
||||
this._authState,
|
||||
this._backgroundService,
|
||||
this._galleryPermissionNotifier,
|
||||
@@ -38,26 +38,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
) : super(
|
||||
BackUpState(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
allAssetsInDatabase: const [],
|
||||
progressInPercentage: 0,
|
||||
cancelToken: CancellationToken(),
|
||||
autoBackup: Store.get(StoreKey.autoBackup, false),
|
||||
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
|
||||
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
|
||||
backupRequireCharging:
|
||||
Store.get(StoreKey.backupRequireCharging, false),
|
||||
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
|
||||
serverInfo: const ServerDiskInfo(
|
||||
diskAvailable: "0",
|
||||
diskSize: "0",
|
||||
diskUse: "0",
|
||||
diskUsagePercentage: 0,
|
||||
),
|
||||
availableAlbums: const [],
|
||||
selectedBackupAlbums: const {},
|
||||
excludedBackupAlbums: const {},
|
||||
allUniqueAssets: const {},
|
||||
selectedAlbumsBackupAssetsIds: const {},
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
@@ -71,412 +53,76 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
final log = Logger('BackupNotifier');
|
||||
final BackupService _backupService;
|
||||
final ServerInfoService _serverInfoService;
|
||||
final ServerInfoNotifier _serverInfoNotifier;
|
||||
final AuthenticationState _authState;
|
||||
final BackgroundService _backgroundService;
|
||||
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
||||
final Isar _db;
|
||||
final Ref ref;
|
||||
|
||||
///
|
||||
/// UI INTERACTION
|
||||
///
|
||||
/// Album selection
|
||||
/// Due to the overlapping assets across multiple albums on the device
|
||||
/// We have method to include and exclude albums
|
||||
/// The total unique assets will be used for backing mechanism
|
||||
///
|
||||
void addAlbumForBackup(AvailableAlbum album) {
|
||||
if (state.excludedBackupAlbums.contains(album)) {
|
||||
removeExcludedAlbumForBackup(album);
|
||||
}
|
||||
|
||||
state = state
|
||||
.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
||||
}
|
||||
|
||||
void addExcludedAlbumForBackup(AvailableAlbum album) {
|
||||
if (state.selectedBackupAlbums.contains(album)) {
|
||||
removeAlbumForBackup(album);
|
||||
}
|
||||
state = state
|
||||
.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
||||
}
|
||||
|
||||
void removeAlbumForBackup(AvailableAlbum album) {
|
||||
Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
|
||||
|
||||
currentSelectedAlbums.removeWhere((a) => a == album);
|
||||
|
||||
state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
|
||||
}
|
||||
|
||||
void removeExcludedAlbumForBackup(AvailableAlbum album) {
|
||||
Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
|
||||
|
||||
currentExcludedAlbums.removeWhere((a) => a == album);
|
||||
|
||||
state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
|
||||
}
|
||||
|
||||
Future<void> backupAlbumSelectionDone() {
|
||||
if (state.selectedBackupAlbums.isEmpty) {
|
||||
// disable any backup
|
||||
cancelBackup();
|
||||
setAutoBackup(false);
|
||||
configureBackgroundBackup(
|
||||
enabled: false,
|
||||
onError: (msg) {},
|
||||
onBatteryInfo: () {},
|
||||
);
|
||||
}
|
||||
return _updateBackupAssetCount();
|
||||
}
|
||||
|
||||
void setAutoBackup(bool enabled) {
|
||||
Store.put(StoreKey.autoBackup, enabled);
|
||||
state = state.copyWith(autoBackup: enabled);
|
||||
}
|
||||
|
||||
void configureBackgroundBackup({
|
||||
bool? enabled,
|
||||
bool? requireWifi,
|
||||
bool? requireCharging,
|
||||
int? triggerDelay,
|
||||
required void Function(String msg) onError,
|
||||
required void Function() onBatteryInfo,
|
||||
}) async {
|
||||
assert(
|
||||
enabled != null ||
|
||||
requireWifi != null ||
|
||||
requireCharging != null ||
|
||||
triggerDelay != null,
|
||||
);
|
||||
final bool wasEnabled = state.backgroundBackup;
|
||||
final bool wasWifi = state.backupRequireWifi;
|
||||
final bool wasCharging = state.backupRequireCharging;
|
||||
final int oldTriggerDelay = state.backupTriggerDelay;
|
||||
state = state.copyWith(
|
||||
backgroundBackup: enabled,
|
||||
backupRequireWifi: requireWifi,
|
||||
backupRequireCharging: requireCharging,
|
||||
backupTriggerDelay: triggerDelay,
|
||||
);
|
||||
|
||||
if (state.backgroundBackup) {
|
||||
bool success = true;
|
||||
if (!wasEnabled) {
|
||||
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
|
||||
onBatteryInfo();
|
||||
}
|
||||
success &= await _backgroundService.enableService(immediate: true);
|
||||
}
|
||||
success &= success &&
|
||||
await _backgroundService.configureService(
|
||||
requireUnmetered: state.backupRequireWifi,
|
||||
requireCharging: state.backupRequireCharging,
|
||||
triggerUpdateDelay: state.backupTriggerDelay,
|
||||
triggerMaxDelay: state.backupTriggerDelay * 10,
|
||||
);
|
||||
if (success) {
|
||||
await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
|
||||
await Store.put(
|
||||
StoreKey.backupRequireCharging,
|
||||
state.backupRequireCharging,
|
||||
);
|
||||
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
|
||||
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
backgroundBackup: wasEnabled,
|
||||
backupRequireWifi: wasWifi,
|
||||
backupRequireCharging: wasCharging,
|
||||
backupTriggerDelay: oldTriggerDelay,
|
||||
);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
} else {
|
||||
final bool success = await _backgroundService.disableService();
|
||||
if (!success) {
|
||||
state = state.copyWith(backgroundBackup: wasEnabled);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Get all album on the device
|
||||
/// Get all selected and excluded album from the user's persistent storage
|
||||
/// If this is the first time performing backup - set the default selected album to be
|
||||
/// the one that has all assets (`Recent` on Android, `Recents` on iOS)
|
||||
///
|
||||
Future<void> _getBackupAlbumsInfo() async {
|
||||
Stopwatch stopwatch = Stopwatch()..start();
|
||||
// Get all albums on the device
|
||||
List<AvailableAlbum> availableAlbums = [];
|
||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
type: RequestType.common,
|
||||
);
|
||||
|
||||
// Map of id -> album for quick album lookup later on.
|
||||
Map<String, AssetPathEntity> albumMap = {};
|
||||
|
||||
log.info('Found ${albums.length} local albums');
|
||||
|
||||
for (AssetPathEntity album in albums) {
|
||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||
|
||||
final assetCountInAlbum = await album.assetCountAsync;
|
||||
if (assetCountInAlbum > 0) {
|
||||
final assetList = await album.getAssetListPaged(page: 0, size: 1);
|
||||
|
||||
// Even though we check assetCountInAlbum to make sure that there are assets in album
|
||||
// The `getAssetListPaged` method still return empty list and cause not assets get rendered
|
||||
if (assetList.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final thumbnailAsset = assetList.first;
|
||||
try {
|
||||
final thumbnailData = await thumbnailAsset
|
||||
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||
availableAlbum =
|
||||
availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||
} catch (e, stack) {
|
||||
log.severe(
|
||||
"Failed to get thumbnail for album ${album.name}",
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
||||
availableAlbums.add(availableAlbum);
|
||||
|
||||
albumMap[album.id] = album;
|
||||
}
|
||||
}
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
final List<BackupAlbum> excludedBackupAlbums =
|
||||
await _backupService.excludedAlbumsQuery().findAll();
|
||||
final List<BackupAlbum> selectedBackupAlbums =
|
||||
await _backupService.selectedAlbumsQuery().findAll();
|
||||
|
||||
// Generate AssetPathEntity from id to add to local state
|
||||
final Set<AvailableAlbum> selectedAlbums = {};
|
||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||
final albumAsset = albumMap[ba.id];
|
||||
|
||||
if (albumAsset != null) {
|
||||
selectedAlbums.add(
|
||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
||||
);
|
||||
} else {
|
||||
log.severe('Selected album not found');
|
||||
}
|
||||
}
|
||||
|
||||
final Set<AvailableAlbum> excludedAlbums = {};
|
||||
for (final BackupAlbum ba in excludedBackupAlbums) {
|
||||
final albumAsset = albumMap[ba.id];
|
||||
|
||||
if (albumAsset != null) {
|
||||
excludedAlbums.add(
|
||||
AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
|
||||
);
|
||||
} else {
|
||||
log.severe('Excluded album not found');
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
selectedBackupAlbums: selectedAlbums,
|
||||
excludedBackupAlbums: excludedAlbums,
|
||||
);
|
||||
|
||||
log.info(
|
||||
"_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums",
|
||||
);
|
||||
debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
///
|
||||
/// From all the selected and albums assets
|
||||
/// Find the assets that are not overlapping between the two sets
|
||||
/// Those assets are unique and are used as the total assets
|
||||
///
|
||||
Future<void> _updateBackupAssetCount() async {
|
||||
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
|
||||
final Set<AssetEntity> assetsFromSelectedAlbums = {};
|
||||
final Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||
|
||||
for (final album in state.selectedBackupAlbums) {
|
||||
final assets = await album.albumEntity.getAssetListRange(
|
||||
start: 0,
|
||||
end: await album.albumEntity.assetCountAsync,
|
||||
);
|
||||
assetsFromSelectedAlbums.addAll(assets);
|
||||
}
|
||||
|
||||
for (final album in state.excludedBackupAlbums) {
|
||||
final assets = await album.albumEntity.getAssetListRange(
|
||||
start: 0,
|
||||
end: await album.albumEntity.assetCountAsync,
|
||||
);
|
||||
assetsFromExcludedAlbums.addAll(assets);
|
||||
}
|
||||
|
||||
final Set<AssetEntity> allUniqueAssets =
|
||||
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
|
||||
|
||||
if (allAssetsInDatabase == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find asset that were backup from selected albums
|
||||
final Set<String> selectedAlbumsBackupAssets =
|
||||
Set.from(allUniqueAssets.map((e) => e.id));
|
||||
|
||||
selectedAlbumsBackupAssets
|
||||
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||
|
||||
// Remove duplicated asset from all unique assets
|
||||
allUniqueAssets.removeWhere(
|
||||
(asset) => duplicatedAssetIds.contains(asset.id),
|
||||
);
|
||||
|
||||
if (allUniqueAssets.isEmpty) {
|
||||
log.info("No assets are selected for back up");
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
allAssetsInDatabase: allAssetsInDatabase,
|
||||
allUniqueAssets: {},
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
allAssetsInDatabase: allAssetsInDatabase,
|
||||
allUniqueAssets: allUniqueAssets,
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
}
|
||||
|
||||
// Save to persistent storage
|
||||
await _updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
/// Get all necessary information for calculating the available albums,
|
||||
/// which albums are selected or excluded
|
||||
/// and then update the UI according to those information
|
||||
Future<void> getBackupInfo() async {
|
||||
final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||
|
||||
state = state.copyWith(backgroundBackup: isEnabled);
|
||||
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
|
||||
Store.put(StoreKey.backgroundBackup, isEnabled);
|
||||
}
|
||||
|
||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||
await _getBackupAlbumsInfo();
|
||||
await updateServerInfo();
|
||||
await _updateBackupAssetCount();
|
||||
} else {
|
||||
log.warning("cannot get backup info - background backup is in progress!");
|
||||
}
|
||||
}
|
||||
|
||||
/// Save user selection of selected albums and excluded albums to database
|
||||
Future<void> _updatePersistentAlbumsSelection() {
|
||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
final selected = state.selectedBackupAlbums.map(
|
||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
|
||||
);
|
||||
final excluded = state.excludedBackupAlbums.map(
|
||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
|
||||
);
|
||||
final backupAlbums = selected.followedBy(excluded).toList();
|
||||
backupAlbums.sortBy((e) => e.id);
|
||||
return _db.writeTxn(() async {
|
||||
final dbAlbums = await _db.backupAlbums.where().sortById().findAll();
|
||||
final List<int> toDelete = [];
|
||||
final List<BackupAlbum> toUpsert = [];
|
||||
// stores the most recent `lastBackup` per album but always keeps the `selection` the user just made
|
||||
diffSortedListsSync(
|
||||
dbAlbums,
|
||||
backupAlbums,
|
||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||
both: (BackupAlbum a, BackupAlbum b) {
|
||||
b.lastBackup =
|
||||
a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup;
|
||||
toUpsert.add(b);
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId),
|
||||
onlySecond: (BackupAlbum b) => toUpsert.add(b),
|
||||
);
|
||||
await _db.backupAlbums.deleteAll(toDelete);
|
||||
await _db.backupAlbums.putAll(toUpsert);
|
||||
});
|
||||
}
|
||||
|
||||
/// Invoke backup process
|
||||
Future<void> startBackupProcess() async {
|
||||
debugPrint("Start backup process");
|
||||
assert(state.backupProgress == BackUpProgressEnum.idle);
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||
|
||||
await getBackupInfo();
|
||||
await _serverInfoNotifier.getServerDiskInfo();
|
||||
|
||||
final hasPermission = _galleryPermissionNotifier.hasPermission;
|
||||
if (hasPermission) {
|
||||
await PhotoManager.clearFileCache();
|
||||
|
||||
if (state.allUniqueAssets.isEmpty) {
|
||||
log.info("No Asset On Device - Abort Backup Process");
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||
return;
|
||||
}
|
||||
|
||||
Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
||||
// Remove item that has already been backed up
|
||||
for (final assetId in state.allAssetsInDatabase) {
|
||||
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
|
||||
}
|
||||
|
||||
if (assetsWillBeBackup.isEmpty) {
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||
}
|
||||
|
||||
// Perform Backup
|
||||
state = state.copyWith(cancelToken: CancellationToken());
|
||||
|
||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||
|
||||
pmProgressHandler?.stream.listen((event) {
|
||||
final double progress = event.progress;
|
||||
state = state.copyWith(iCloudDownloadProgress: progress);
|
||||
});
|
||||
|
||||
await _backupService.backupAsset(
|
||||
assetsWillBeBackup,
|
||||
state.cancelToken,
|
||||
pmProgressHandler,
|
||||
_onAssetUploaded,
|
||||
_onUploadProgress,
|
||||
_onSetCurrentBackupAsset,
|
||||
_onBackupError,
|
||||
);
|
||||
await notifyBackgroundServiceCanRun();
|
||||
} else {
|
||||
if (!hasPermission) {
|
||||
openAppSettings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void setAvailableAlbums(availableAlbums) {
|
||||
state = state.copyWith(
|
||||
availableAlbums: availableAlbums,
|
||||
await PhotoManager.clearFileCache();
|
||||
|
||||
final idsForBackup = await _db.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.idProperty()
|
||||
.findAll();
|
||||
final localAssetsToBackup = await _db.assets
|
||||
.where()
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.anyOf(idsForBackup, (q, id) => q.localIdEqualTo(id))
|
||||
.findAll();
|
||||
|
||||
final assetsToBackup =
|
||||
await _backupService.remoteAlreadyUploaded(localAssetsToBackup);
|
||||
if (assetsToBackup.isEmpty) {
|
||||
log.info("No Asset On Device - Abort Backup Process");
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform Backup
|
||||
state = state.copyWith(cancelToken: CancellationToken());
|
||||
|
||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||
|
||||
pmProgressHandler?.stream.listen((event) {
|
||||
final double progress = event.progress;
|
||||
state = state.copyWith(iCloudDownloadProgress: progress);
|
||||
});
|
||||
|
||||
await _backupService.backupAsset(
|
||||
assetsToBackup,
|
||||
state.cancelToken,
|
||||
pmProgressHandler,
|
||||
_onAssetUploaded,
|
||||
_onUploadProgress,
|
||||
_onSetCurrentBackupAsset,
|
||||
_onBackupError,
|
||||
);
|
||||
|
||||
state.cancelToken.cancel();
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.idle,
|
||||
progressInPercentage: 0.0,
|
||||
);
|
||||
ref.invalidate(deviceAssetsProvider);
|
||||
await notifyBackgroundServiceCanRun();
|
||||
}
|
||||
|
||||
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||
@@ -503,43 +149,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
String deviceId,
|
||||
bool isDuplicated,
|
||||
) {
|
||||
if (isDuplicated) {
|
||||
state = state.copyWith(
|
||||
allUniqueAssets: state.allUniqueAssets
|
||||
.where((asset) => asset.id != deviceAssetId)
|
||||
.toSet(),
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
selectedAlbumsBackupAssetsIds: {
|
||||
...state.selectedAlbumsBackupAssetsIds,
|
||||
deviceAssetId,
|
||||
},
|
||||
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||
);
|
||||
}
|
||||
|
||||
if (state.allUniqueAssets.length -
|
||||
state.selectedAlbumsBackupAssetsIds.length ==
|
||||
0) {
|
||||
final latestAssetBackup =
|
||||
state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
|
||||
(v, e) => e.isAfter(v) ? e : v,
|
||||
);
|
||||
state = state.copyWith(
|
||||
selectedBackupAlbums: state.selectedBackupAlbums
|
||||
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
|
||||
.toSet(),
|
||||
excludedBackupAlbums: state.excludedBackupAlbums
|
||||
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
|
||||
.toSet(),
|
||||
backupProgress: BackUpProgressEnum.done,
|
||||
progressInPercentage: 0.0,
|
||||
);
|
||||
_updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
updateServerInfo();
|
||||
_serverInfoNotifier.getServerDiskInfo();
|
||||
}
|
||||
|
||||
void _onUploadProgress(int sent, int total) {
|
||||
@@ -548,17 +158,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateServerInfo() async {
|
||||
final serverInfo = await _serverInfoService.getServerInfo();
|
||||
|
||||
// Update server info
|
||||
if (serverInfo != null) {
|
||||
state = state.copyWith(
|
||||
serverInfo: serverInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup() async {
|
||||
// Check if user is login
|
||||
final accessKey = Store.tryGet(StoreKey.accessToken);
|
||||
@@ -570,7 +169,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
}
|
||||
|
||||
// Check if this device is enable backup by the user
|
||||
if (state.autoBackup) {
|
||||
if (ref.read(backupSettingsProvider).autoBackup) {
|
||||
// check if backup is already in process - then return
|
||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||
log.info("[_resumeBackup] Auto Backup is already in progress - abort");
|
||||
@@ -595,35 +194,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
}
|
||||
|
||||
Future<void> resumeBackup() async {
|
||||
final List<BackupAlbum> selectedBackupAlbums = await _db.backupAlbums
|
||||
.filter()
|
||||
.selectionEqualTo(BackupSelection.select)
|
||||
.findAll();
|
||||
final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums
|
||||
.filter()
|
||||
.selectionEqualTo(BackupSelection.exclude)
|
||||
.findAll();
|
||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||
if (selectedAlbums.isNotEmpty) {
|
||||
selectedAlbums = _updateAlbumsBackupTime(
|
||||
selectedAlbums,
|
||||
selectedBackupAlbums,
|
||||
);
|
||||
}
|
||||
|
||||
if (excludedAlbums.isNotEmpty) {
|
||||
excludedAlbums = _updateAlbumsBackupTime(
|
||||
excludedAlbums,
|
||||
excludedBackupAlbums,
|
||||
);
|
||||
}
|
||||
final BackUpProgressEnum previous = state.backupProgress;
|
||||
state = state.copyWith(
|
||||
backupProgress: BackUpProgressEnum.inBackground,
|
||||
selectedBackupAlbums: selectedAlbums,
|
||||
excludedBackupAlbums: excludedAlbums,
|
||||
);
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||
|
||||
// assumes the background service is currently running
|
||||
// if true, waits until it has stopped to start the backup
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
@@ -633,26 +206,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
return _resumeBackup();
|
||||
}
|
||||
|
||||
Set<AvailableAlbum> _updateAlbumsBackupTime(
|
||||
Set<AvailableAlbum> albums,
|
||||
List<BackupAlbum> backupAlbums,
|
||||
) {
|
||||
Set<AvailableAlbum> result = {};
|
||||
for (BackupAlbum ba in backupAlbums) {
|
||||
try {
|
||||
AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id);
|
||||
result.add(a.copyWith(lastBackup: ba.lastBackup));
|
||||
} on StateError {
|
||||
log.severe(
|
||||
"[_updateAlbumBackupTime] failed to find album in state",
|
||||
"State Error",
|
||||
StackTrace.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> notifyBackgroundServiceCanRun() async {
|
||||
const allowedStates = [
|
||||
AppStateEnum.inactive,
|
||||
@@ -674,7 +227,7 @@ final backupProvider =
|
||||
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||
return BackupNotifier(
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(serverInfoServiceProvider),
|
||||
ref.watch(serverInfoProvider.notifier),
|
||||
ref.watch(authenticationProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(galleryPermissionNotifier.notifier),
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_album.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'backup_album.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class BackupAlbums extends _$BackupAlbums {
|
||||
@override
|
||||
Future<BackupAlbumState> build() async {
|
||||
final db = ref.read(dbProvider);
|
||||
return BackupAlbumState(
|
||||
selectedBackupAlbums: await db.backupAlbums
|
||||
.filter()
|
||||
.selectionEqualTo(BackupSelection.select)
|
||||
.findAll(),
|
||||
excludedBackupAlbums: await db.backupAlbums
|
||||
.filter()
|
||||
.selectionEqualTo(BackupSelection.exclude)
|
||||
.findAll(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reloadProviders(
|
||||
LocalAlbum album,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
final shouldReload = await ref
|
||||
.read(backupAlbumServiceProvider)
|
||||
.updateAlbumAssetsState(album, selection);
|
||||
if (shouldReload) {
|
||||
ref.invalidate(deviceAssetsProvider);
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> addBackupAlbum(
|
||||
LocalAlbum album,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
await ref.read(backupAlbumServiceProvider).addBackupAlbum(album, selection);
|
||||
_reloadProviders(album, selection);
|
||||
}
|
||||
|
||||
Future<void> syncWithLocalAlbum(LocalAlbum album) async {
|
||||
await ref.read(backupAlbumServiceProvider).syncWithLocalAlbum(album);
|
||||
final albumInDB = await ref
|
||||
.read(dbProvider)
|
||||
.backupAlbums
|
||||
.filter()
|
||||
.idEqualTo(album.id)
|
||||
.findFirst();
|
||||
if (albumInDB?.selection != null) {
|
||||
_reloadProviders(album, albumInDB!.selection);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateAlbumSelection(
|
||||
LocalAlbum localAlbum,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
await ref
|
||||
.read(backupAlbumServiceProvider)
|
||||
.updateAlbumSelection(localAlbum, selection);
|
||||
_reloadProviders(localAlbum, selection);
|
||||
}
|
||||
|
||||
Future<void> refreshAlbumAssetsState() async =>
|
||||
ref.read(backupAlbumServiceProvider).refreshAlbumAssetsState();
|
||||
|
||||
Future<void> selectAlbumForBackup(LocalAlbum album) =>
|
||||
_updateAlbumSelection(album, BackupSelection.select);
|
||||
|
||||
Future<void> excludeAlbumFromBackup(LocalAlbum album) =>
|
||||
_updateAlbumSelection(album, BackupSelection.exclude);
|
||||
|
||||
Future<void> deSelectAlbum(LocalAlbum album) =>
|
||||
_updateAlbumSelection(album, BackupSelection.none);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup_album.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$backupAlbumsHash() => r'f37d088af2a837d61040fc7663bed61703196efd';
|
||||
|
||||
/// See also [BackupAlbums].
|
||||
@ProviderFor(BackupAlbums)
|
||||
final backupAlbumsProvider =
|
||||
AutoDisposeAsyncNotifierProvider<BackupAlbums, BackupAlbumState>.internal(
|
||||
BackupAlbums.new,
|
||||
name: r'backupAlbumsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$backupAlbumsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$BackupAlbums = AutoDisposeAsyncNotifier<BackupAlbumState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_setting.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'backup_settings.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class BackupSettings extends _$BackupSettings {
|
||||
@override
|
||||
BackupSetting build() {
|
||||
return BackupSetting(
|
||||
autoBackup: Store.get(StoreKey.autoBackup, false),
|
||||
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
|
||||
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
|
||||
backupRequireCharging: Store.get(StoreKey.backupRequireCharging, false),
|
||||
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
|
||||
);
|
||||
}
|
||||
|
||||
void setAutoBackup(bool enabled) {
|
||||
Store.put(StoreKey.autoBackup, enabled);
|
||||
state = state.copyWith(autoBackup: enabled);
|
||||
}
|
||||
|
||||
void configureBackgroundBackup({
|
||||
bool? enabled,
|
||||
bool? requireWifi,
|
||||
bool? requireCharging,
|
||||
int? triggerDelay,
|
||||
required void Function(String msg) onError,
|
||||
required void Function() onBatteryInfo,
|
||||
}) async {
|
||||
assert(
|
||||
enabled != null ||
|
||||
requireWifi != null ||
|
||||
requireCharging != null ||
|
||||
triggerDelay != null,
|
||||
);
|
||||
final backgroundService = ref.read(backgroundServiceProvider);
|
||||
|
||||
final bool wasEnabled = state.backgroundBackup;
|
||||
final bool wasWifi = state.backupRequireWifi;
|
||||
final bool wasCharging = state.backupRequireCharging;
|
||||
final int oldTriggerDelay = state.backupTriggerDelay;
|
||||
state = state.copyWith(
|
||||
backgroundBackup: enabled,
|
||||
backupRequireWifi: requireWifi,
|
||||
backupRequireCharging: requireCharging,
|
||||
backupTriggerDelay: triggerDelay,
|
||||
);
|
||||
|
||||
if (state.backgroundBackup) {
|
||||
bool success = true;
|
||||
if (!wasEnabled) {
|
||||
if (!await backgroundService.isIgnoringBatteryOptimizations()) {
|
||||
onBatteryInfo();
|
||||
}
|
||||
success &= await backgroundService.enableService(immediate: true);
|
||||
}
|
||||
success &= success &&
|
||||
await backgroundService.configureService(
|
||||
requireUnmetered: state.backupRequireWifi,
|
||||
requireCharging: state.backupRequireCharging,
|
||||
triggerUpdateDelay: state.backupTriggerDelay,
|
||||
triggerMaxDelay: state.backupTriggerDelay * 10,
|
||||
);
|
||||
if (success) {
|
||||
await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
|
||||
await Store.put(
|
||||
StoreKey.backupRequireCharging,
|
||||
state.backupRequireCharging,
|
||||
);
|
||||
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
|
||||
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
backgroundBackup: wasEnabled,
|
||||
backupRequireWifi: wasWifi,
|
||||
backupRequireCharging: wasCharging,
|
||||
backupTriggerDelay: oldTriggerDelay,
|
||||
);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
} else {
|
||||
final bool success = await backgroundService.disableService();
|
||||
if (!success) {
|
||||
state = state.copyWith(backgroundBackup: wasEnabled);
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup_settings.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$backupSettingsHash() => r'839ad0c6c4b3bd61b9af75c38f8580a8af1d2e0a';
|
||||
|
||||
/// See also [BackupSettings].
|
||||
@ProviderFor(BackupSettings)
|
||||
final backupSettingsProvider =
|
||||
AutoDisposeNotifierProvider<BackupSettings, BackupSetting>.internal(
|
||||
BackupSettings.new,
|
||||
name: r'backupSettingsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$backupSettingsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$BackupSettings = AutoDisposeNotifier<BackupSetting>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/device_album_state.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'device_assets.provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class DeviceAssets extends _$DeviceAssets {
|
||||
@override
|
||||
Future<DeviceAssetState> build() async {
|
||||
final db = ref.read(dbProvider);
|
||||
final idsToBackup = await db.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.idProperty()
|
||||
.findAll();
|
||||
|
||||
return DeviceAssetState(
|
||||
uniqueAssetsToBackup: await db.assets
|
||||
.filter()
|
||||
.anyOf(idsToBackup, (q, id) => q.localIdEqualTo(id))
|
||||
.count(),
|
||||
backedUpAssets: await db.assets
|
||||
.where()
|
||||
.remoteIdIsNotNull()
|
||||
.filter()
|
||||
.anyOf(idsToBackup, (q, id) => q.localIdEqualTo(id))
|
||||
.count(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device_assets.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$deviceAssetsHash() => r'0fd5eb2d0f02d125df780e5676a6dcee2234de2f';
|
||||
|
||||
/// See also [DeviceAssets].
|
||||
@ProviderFor(DeviceAssets)
|
||||
final deviceAssetsProvider =
|
||||
AutoDisposeAsyncNotifierProvider<DeviceAssets, DeviceAssetState>.internal(
|
||||
DeviceAssets.new,
|
||||
name: r'deviceAssetsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$deviceAssetsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$DeviceAssets = AutoDisposeAsyncNotifier<DeviceAssetState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
@@ -115,7 +116,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
bool isDuplicated,
|
||||
) {
|
||||
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
||||
_backupProvider.updateServerInfo();
|
||||
ref.read(serverInfoProvider.notifier).getServerDiskInfo();
|
||||
}
|
||||
|
||||
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
||||
@@ -158,25 +159,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
|
||||
await PhotoManager.clearFileCache();
|
||||
|
||||
// We do not have 1:1 mapping of all AssetEntity fields to Asset. This results in cases
|
||||
// where platform specific fields such as `subtype` used to detect platform specific assets such as
|
||||
// LivePhoto in iOS is lost when we directly fetch the local asset from Asset using Asset.local
|
||||
List<AssetEntity?> allAssetsFromDevice = await Future.wait(
|
||||
allManualUploads
|
||||
// Filter local only assets
|
||||
.where((e) => e.isLocal && !e.isRemote)
|
||||
.map((e) => e.local!.obtainForNewProperties()),
|
||||
);
|
||||
|
||||
if (allAssetsFromDevice.length != allManualUploads.length) {
|
||||
_log.warning(
|
||||
'[_startUpload] Refreshed upload list -> ${allManualUploads.length - allAssetsFromDevice.length} asset will not be uploaded',
|
||||
);
|
||||
}
|
||||
|
||||
Set<AssetEntity> allUploadAssets = allAssetsFromDevice.nonNulls.toSet();
|
||||
|
||||
if (allUploadAssets.isEmpty) {
|
||||
if (allManualUploads.isEmpty) {
|
||||
debugPrint("[_startUpload] No Assets to upload - Abort Process");
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
return false;
|
||||
@@ -184,7 +167,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
|
||||
state = state.copyWith(
|
||||
progressInPercentage: 0,
|
||||
totalAssetsToUpload: allUploadAssets.length,
|
||||
totalAssetsToUpload: allManualUploads.length,
|
||||
successfulUploads: 0,
|
||||
currentAssetIndex: 0,
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
@@ -213,7 +196,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||
|
||||
final bool ok = await ref.read(backupServiceProvider).backupAsset(
|
||||
allUploadAssets,
|
||||
allManualUploads,
|
||||
state.cancelToken,
|
||||
pmProgressHandler,
|
||||
_onAssetUploaded,
|
||||
|
||||
@@ -7,10 +7,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/upload_check_response.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart' hide AssetType;
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
@@ -40,26 +41,38 @@ class BackupService {
|
||||
|
||||
BackupService(this._apiService, this._db, this._appSetting);
|
||||
|
||||
Future<List<String>?> getDeviceBackupAsset() async {
|
||||
final String deviceId = Store.get(StoreKey.deviceId);
|
||||
|
||||
Future<BulkUploadCheckResponse?> _bulkUploadCheck(List<Asset> assets) async {
|
||||
try {
|
||||
return await _apiService.assetApi.getAllUserAssetsByDeviceId(deviceId);
|
||||
final dto = await _apiService.assetApi.checkBulkUpload(
|
||||
AssetBulkUploadCheckDto(
|
||||
assets: assets
|
||||
.where((a) => a.localId != null)
|
||||
.map(
|
||||
(e) => AssetBulkUploadCheckItem(
|
||||
checksum: e.checksum,
|
||||
id: e.localId!,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
|
||||
return dto != null ? BulkUploadCheckResponse.fromDto(dto) : null;
|
||||
} catch (e) {
|
||||
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
|
||||
debugPrint('Error [bulkUploadCheck] ${e.toString()}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) {
|
||||
final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList();
|
||||
return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates));
|
||||
}
|
||||
Future<Iterable<Asset>> remoteAlreadyUploaded(List<Asset> assets) async {
|
||||
final uploadCheck = await _bulkUploadCheck(assets);
|
||||
if (uploadCheck == null) {
|
||||
_log.warning("Cannot determine duplicates. Skipping backup for now");
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Get duplicated asset id from database
|
||||
Future<Set<String>> getDuplicatedAssetIds() async {
|
||||
final duplicates = await _db.duplicatedAssets.where().findAll();
|
||||
return duplicates.map((e) => e.id).toSet();
|
||||
return uploadCheck.toBeUploaded
|
||||
.map((c) => assets.firstWhere((a) => a.localId == c));
|
||||
}
|
||||
|
||||
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
|
||||
@@ -69,138 +82,8 @@ class BackupService {
|
||||
excludedAlbumsQuery() =>
|
||||
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
|
||||
|
||||
/// Returns all assets newer than the last successful backup per album
|
||||
Future<List<AssetEntity>> buildUploadCandidates(
|
||||
List<BackupAlbum> selectedBackupAlbums,
|
||||
List<BackupAlbum> excludedBackupAlbums,
|
||||
) async {
|
||||
final filter = FilterOptionGroup(
|
||||
containsPathModified: true,
|
||||
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
||||
// title is needed to create Assets
|
||||
imageOption: const FilterOption(needTitle: true),
|
||||
videoOption: const FilterOption(needTitle: true),
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final List<AssetPathEntity?> selectedAlbums =
|
||||
await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now);
|
||||
if (selectedAlbums.every((e) => e == null)) {
|
||||
return [];
|
||||
}
|
||||
final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll);
|
||||
if (allIdx != -1) {
|
||||
final List<AssetPathEntity?> excludedAlbums =
|
||||
await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now);
|
||||
final List<AssetEntity> toAdd = await _fetchAssetsAndUpdateLastBackup(
|
||||
selectedAlbums.slice(allIdx, allIdx + 1),
|
||||
selectedBackupAlbums.slice(allIdx, allIdx + 1),
|
||||
now,
|
||||
);
|
||||
final List<AssetEntity> toRemove = await _fetchAssetsAndUpdateLastBackup(
|
||||
excludedAlbums,
|
||||
excludedBackupAlbums,
|
||||
now,
|
||||
);
|
||||
return toAdd.toSet().difference(toRemove.toSet()).toList();
|
||||
} else {
|
||||
return await _fetchAssetsAndUpdateLastBackup(
|
||||
selectedAlbums,
|
||||
selectedBackupAlbums,
|
||||
now,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
|
||||
List<BackupAlbum> albums,
|
||||
FilterOptionGroup filter,
|
||||
DateTime now,
|
||||
) async {
|
||||
List<AssetPathEntity?> result = [];
|
||||
for (BackupAlbum a in albums) {
|
||||
try {
|
||||
final AssetPathEntity album =
|
||||
await AssetPathEntity.obtainPathFromProperties(
|
||||
id: a.id,
|
||||
optionGroup: filter.copyWith(
|
||||
updateTimeCond: DateTimeCond(
|
||||
// subtract 2 seconds to prevent missing assets due to rounding issues
|
||||
min: a.lastBackup.subtract(const Duration(seconds: 2)),
|
||||
max: now,
|
||||
),
|
||||
),
|
||||
maxDateTimeToNow: false,
|
||||
);
|
||||
result.add(album);
|
||||
} on StateError {
|
||||
// either there are no assets matching the filter criteria OR the album no longer exists
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<AssetEntity>> _fetchAssetsAndUpdateLastBackup(
|
||||
List<AssetPathEntity?> albums,
|
||||
List<BackupAlbum> backupAlbums,
|
||||
DateTime now,
|
||||
) async {
|
||||
List<AssetEntity> result = [];
|
||||
for (int i = 0; i < albums.length; i++) {
|
||||
final AssetPathEntity? a = albums[i];
|
||||
if (a != null &&
|
||||
a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) {
|
||||
result.addAll(
|
||||
await a.getAssetListRange(start: 0, end: await a.assetCountAsync),
|
||||
);
|
||||
backupAlbums[i].lastBackup = now;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns a new list of assets not yet uploaded
|
||||
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
|
||||
List<AssetEntity> candidates,
|
||||
) async {
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
}
|
||||
final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
|
||||
candidates = duplicatedAssetIds.isEmpty
|
||||
? candidates
|
||||
: candidates
|
||||
.whereNot((asset) => duplicatedAssetIds.contains(asset.id))
|
||||
.toList();
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
}
|
||||
final Set<String> existing = {};
|
||||
try {
|
||||
final String deviceId = Store.get(StoreKey.deviceId);
|
||||
final CheckExistingAssetsResponseDto? duplicates =
|
||||
await _apiService.assetApi.checkExistingAssets(
|
||||
CheckExistingAssetsDto(
|
||||
deviceAssetIds: candidates.map((e) => e.id).toList(),
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
if (duplicates != null) {
|
||||
existing.addAll(duplicates.existingIds);
|
||||
}
|
||||
} on ApiException {
|
||||
// workaround for older server versions or when checking for too many assets at once
|
||||
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
||||
if (allAssetsInDatabase != null) {
|
||||
existing.addAll(allAssetsInDatabase);
|
||||
}
|
||||
}
|
||||
return existing.isEmpty
|
||||
? candidates
|
||||
: candidates.whereNot((e) => existing.contains(e.id)).toList();
|
||||
}
|
||||
|
||||
Future<bool> backupAsset(
|
||||
Iterable<AssetEntity> assetList,
|
||||
Iterable<Asset> assetList,
|
||||
http.CancellationToken cancelToken,
|
||||
PMProgressHandler? pmProgressHandler,
|
||||
Function(String, String, bool) uploadSuccessCb,
|
||||
@@ -223,26 +106,36 @@ class BackupService {
|
||||
final String deviceId = Store.get(StoreKey.deviceId);
|
||||
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
bool anyErrors = false;
|
||||
final List<String> duplicatedAssetIds = [];
|
||||
|
||||
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
|
||||
if (Platform.isIOS) {
|
||||
await PhotoManager.requestPermissionExtend();
|
||||
}
|
||||
|
||||
List<AssetEntity> assetsToUpload = sortAssets
|
||||
List<Asset> assetsToUpload = sortAssets
|
||||
// Upload images before video assets
|
||||
// these are further sorted by using their creation date
|
||||
? assetList.sorted(
|
||||
(a, b) {
|
||||
final cmp = a.typeInt - b.typeInt;
|
||||
final cmp = a.type.index - b.type.index;
|
||||
if (cmp != 0) return cmp;
|
||||
return a.createDateTime.compareTo(b.createDateTime);
|
||||
return a.fileCreatedAt.compareTo(b.fileCreatedAt);
|
||||
},
|
||||
)
|
||||
: assetList.toList();
|
||||
|
||||
for (var entity in assetsToUpload) {
|
||||
for (final asset in assetsToUpload) {
|
||||
if (!asset.isLocal) {
|
||||
_log.severe("Asset - ${asset.id} is not a local asset");
|
||||
continue;
|
||||
}
|
||||
|
||||
final entity = (await AssetEntity.fromId(asset.localId!));
|
||||
if (entity == null) {
|
||||
_log.severe("Cannot fetch asset entity for - ${asset.localId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
File? file;
|
||||
File? livePhotoFile;
|
||||
|
||||
@@ -353,8 +246,6 @@ class BackupService {
|
||||
await httpClient.send(req, cancellationToken: cancelToken);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// asset is a duplicate (already exists on the server)
|
||||
duplicatedAssetIds.add(entity.id);
|
||||
uploadSuccessCb(entity.id, deviceId, true);
|
||||
} else if (response.statusCode == 201) {
|
||||
// stored a new asset on the server
|
||||
@@ -405,9 +296,6 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (duplicatedAssetIds.isNotEmpty) {
|
||||
await _saveDuplicatedAssetIds(duplicatedAssetIds);
|
||||
}
|
||||
return !anyErrors;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/album_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final backupAlbumServiceProvider = Provider(
|
||||
(ref) => BackupAlbumService(
|
||||
ref.watch(dbProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class BackupAlbumService {
|
||||
final Isar _db;
|
||||
final Logger _log = Logger("BackupAlbumService");
|
||||
|
||||
BackupAlbumService(this._db);
|
||||
|
||||
Future<void> addBackupAlbum(
|
||||
LocalAlbum album,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
final albumInDB =
|
||||
await _db.backupAlbums.filter().idEqualTo(album.id).findFirst();
|
||||
|
||||
final backupAlbum = albumInDB ??
|
||||
BackupAlbum(
|
||||
id: album.id,
|
||||
selection: selection,
|
||||
);
|
||||
|
||||
backupAlbum.selection = selection;
|
||||
backupAlbum.album.value = album;
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await _db.backupAlbums.store(backupAlbum);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWithLocalAlbum(LocalAlbum album) async {
|
||||
final albumInDB =
|
||||
await _db.backupAlbums.filter().idEqualTo(album.id).findFirst();
|
||||
if (albumInDB == null) {
|
||||
_log.fine("No backup album for local album - ${album.name}");
|
||||
return;
|
||||
}
|
||||
albumInDB.album.value = album;
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await _db.backupAlbums.store(albumInDB);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refreshAlbumAssetsState() async {
|
||||
final backupAlbums = await _db.backupAlbums.where().findAll();
|
||||
for (final album in backupAlbums) {
|
||||
final localAlbum = album.album.value;
|
||||
if (localAlbum != null) {
|
||||
await updateAlbumAssetsState(localAlbum, album.selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAlbumSelection(
|
||||
LocalAlbum localAlbum,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
await localAlbum.backup.load();
|
||||
final backupAlbum = localAlbum.backup.value;
|
||||
|
||||
if (backupAlbum == null) {
|
||||
return addBackupAlbum(localAlbum, selection);
|
||||
}
|
||||
backupAlbum.selection = selection;
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await _db.backupAlbums.store(backupAlbum);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> _getAllLocalAlbumWithAsset(Asset asset) async {
|
||||
return await _db.localAlbums
|
||||
.filter()
|
||||
.assets((q) => q.idEqualTo(asset.id))
|
||||
.findAll();
|
||||
}
|
||||
|
||||
Future<bool> updateAlbumAssetsState(
|
||||
LocalAlbum album,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
final assets = await _updateDeviceAssetsToSelection(album, selection);
|
||||
|
||||
if (assets.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _db.writeTxn(() async {
|
||||
await _db.deviceAssets.putAll(assets);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<DeviceAsset>> _updateDeviceAssetsToSelection(
|
||||
LocalAlbum album,
|
||||
BackupSelection selection,
|
||||
) async {
|
||||
await album.assets.load();
|
||||
final assets = album.assets.toList();
|
||||
final updatedAssets = <DeviceAsset>[];
|
||||
|
||||
for (final asset in assets) {
|
||||
if (!asset.isLocal) {
|
||||
_log.warning("Local id not available for asset ID - ${asset.id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
final deviceAsset =
|
||||
await _db.deviceAssets.where().idEqualTo(asset.localId!).findFirst();
|
||||
if (deviceAsset == null) {
|
||||
_log.warning(
|
||||
"Device asset not available for local asset ID - ${asset.id}",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deviceAsset.backupSelection == selection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exclude takes priority
|
||||
if (selection == BackupSelection.exclude) {
|
||||
deviceAsset.backupSelection = selection;
|
||||
} else if (selection == BackupSelection.select) {
|
||||
bool shouldExclude = false;
|
||||
final localAlbums = await _getAllLocalAlbumWithAsset(asset);
|
||||
for (final a in localAlbums) {
|
||||
await a.backup.load();
|
||||
// Check if there is any other excluded albums in which the asset is present
|
||||
if (a.backup.value?.selection == BackupSelection.exclude &&
|
||||
a.id != album.id) {
|
||||
shouldExclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force exclude ignoring selection if asset is part of another excluded album
|
||||
deviceAsset.backupSelection =
|
||||
shouldExclude ? BackupSelection.exclude : BackupSelection.select;
|
||||
} else if (selection == BackupSelection.none) {
|
||||
bool setToNone = true;
|
||||
BackupSelection? oldSelection;
|
||||
final localAlbums = await _getAllLocalAlbumWithAsset(asset);
|
||||
for (final a in localAlbums) {
|
||||
await a.backup.load();
|
||||
// Check if there is any other albums in which the asset is present
|
||||
if (a.backup.value?.selection != BackupSelection.none &&
|
||||
a.id != album.id) {
|
||||
setToNone = false;
|
||||
oldSelection = a.backup.value?.selection;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only set to none when the asset is not part of any other selected or excluded albums
|
||||
if (setToNone) {
|
||||
deviceAsset.backupSelection = BackupSelection.none;
|
||||
} else if (oldSelection != null) {
|
||||
deviceAsset.backupSelection = oldSelection;
|
||||
}
|
||||
}
|
||||
|
||||
updatedAssets.add(deviceAsset);
|
||||
}
|
||||
|
||||
return updatedAssets;
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoCard extends HookConsumerWidget {
|
||||
final Uint8List? imageData;
|
||||
final AvailableAlbum albumInfo;
|
||||
|
||||
const AlbumInfoCard({super.key, this.imageData, required this.albumInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bool isSelected =
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
context.primaryColor.withAlpha(100),
|
||||
BlendMode.darken,
|
||||
);
|
||||
ColorFilter excludedFilter =
|
||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||
ColorFilter unselectedFilter =
|
||||
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||
|
||||
buildSelectedTextBox() {
|
||||
if (isSelected) {
|
||||
return Chip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
label: Text(
|
||||
"album_info_card_backup_album_included",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDarkTheme ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
backgroundColor: context.primaryColor,
|
||||
);
|
||||
} else if (isExcluded) {
|
||||
return Chip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
label: Text(
|
||||
"album_info_card_backup_album_excluded",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDarkTheme ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
backgroundColor: Colors.red[300],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
buildImageFilter() {
|
||||
if (isSelected) {
|
||||
return selectedFilter;
|
||||
} else if (isExcluded) {
|
||||
return excludedFilter;
|
||||
} else {
|
||||
return unselectedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
|
||||
} else {
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
|
||||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.removeExcludedAlbumForBackup(albumInfo);
|
||||
} else {
|
||||
// Add to exclude album list
|
||||
|
||||
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'Cannot exclude album contains all assets',
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.addExcludedAlbumForBackup(albumInfo);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: const EdgeInsets.all(1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12), // if you need this
|
||||
side: BorderSide(
|
||||
color: isDarkTheme
|
||||
? const Color.fromARGB(255, 37, 35, 35)
|
||||
: const Color(0xFFC9C9C9),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
elevation: 0,
|
||||
borderOnForeground: false,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: buildImageFilter(),
|
||||
child: Image(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: imageData != null
|
||||
? MemoryImage(imageData!)
|
||||
: const AssetImage(
|
||||
'assets/immich-logo-no-outline.png',
|
||||
) as ImageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
right: 25,
|
||||
child: buildSelectedTextBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 25,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
albumInfo.name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: FutureBuilder(
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
snapshot.data.toString() +
|
||||
(albumInfo.isAll
|
||||
? " (${'backup_all'.tr()})"
|
||||
: ""),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text("0");
|
||||
}),
|
||||
future: albumInfo.assetCount,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.image_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
splashRadius: 25,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoListTile extends HookConsumerWidget {
|
||||
final Uint8List? imageData;
|
||||
final AvailableAlbum albumInfo;
|
||||
|
||||
const AlbumInfoListTile({super.key, this.imageData, required this.albumInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bool isSelected =
|
||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
context.primaryColor.withAlpha(100),
|
||||
BlendMode.darken,
|
||||
);
|
||||
ColorFilter excludedFilter =
|
||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||
ColorFilter unselectedFilter =
|
||||
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||
|
||||
var assetCount = useState(0);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
albumInfo.assetCount.then((value) => assetCount.value = value);
|
||||
return null;
|
||||
},
|
||||
[albumInfo],
|
||||
);
|
||||
|
||||
buildImageFilter() {
|
||||
if (isSelected) {
|
||||
return selectedFilter;
|
||||
} else if (isExcluded) {
|
||||
return excludedFilter;
|
||||
} else {
|
||||
return unselectedFilter;
|
||||
}
|
||||
}
|
||||
|
||||
buildTileColor() {
|
||||
if (isSelected) {
|
||||
return context.isDarkTheme
|
||||
? context.primaryColor.withAlpha(100)
|
||||
: context.primaryColor.withAlpha(25);
|
||||
} else if (isExcluded) {
|
||||
return context.isDarkTheme
|
||||
? Colors.red[300]?.withAlpha(150)
|
||||
: Colors.red[100]?.withAlpha(150);
|
||||
} else {
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.removeExcludedAlbumForBackup(albumInfo);
|
||||
} else {
|
||||
// Add to exclude album list
|
||||
|
||||
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'Cannot exclude album contains all assets',
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ref
|
||||
.read(backupProvider.notifier)
|
||||
.addExcludedAlbumForBackup(albumInfo);
|
||||
}
|
||||
},
|
||||
child: ListTile(
|
||||
tileColor: buildTileColor(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
|
||||
} else {
|
||||
ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
|
||||
}
|
||||
},
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
width: 80,
|
||||
child: ColorFiltered(
|
||||
colorFilter: buildImageFilter(),
|
||||
child: Image(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: imageData != null
|
||||
? MemoryImage(imageData!)
|
||||
: const AssetImage(
|
||||
'assets/immich-logo-no-outline.png',
|
||||
) as ImageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
albumInfo.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(assetCount.value.toString()),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.image_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
splashRadius: 25,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class BackupAlbumInfoListItem extends ConsumerWidget {
|
||||
final LocalAlbum album;
|
||||
|
||||
const BackupAlbumInfoListItem({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final backupAlbums = ref.watch(backupAlbumsProvider);
|
||||
final backupAlbumNotifier = ref.read(backupAlbumsProvider.notifier);
|
||||
final isSelected =
|
||||
backupAlbums.value?.selectedBackupAlbums.any((a) => a.id == album.id) ??
|
||||
false;
|
||||
final isExcluded =
|
||||
backupAlbums.value?.excludedBackupAlbums.any((a) => a.id == album.id) ??
|
||||
false;
|
||||
final backupSelection = isSelected
|
||||
? BackupSelection.select
|
||||
: isExcluded
|
||||
? BackupSelection.exclude
|
||||
: BackupSelection.none;
|
||||
|
||||
void onTap() {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isSelected || isExcluded) {
|
||||
backupAlbumNotifier.deSelectAlbum(album);
|
||||
} else {
|
||||
backupAlbumNotifier.selectAlbumForBackup(album);
|
||||
}
|
||||
}
|
||||
|
||||
void onDoubleTap() {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
backupAlbumNotifier.deSelectAlbum(album);
|
||||
} else {
|
||||
if (album.id == LocalAlbum.isAllId || album.name == 'Recents') {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'Cannot exclude album contains all assets',
|
||||
toastType: ToastType.error,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
backupAlbumNotifier.excludeAlbumFromBackup(album);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
child: context.isMobile
|
||||
? _AlbumDetailListTile(album, backupSelection)
|
||||
: _AlbumDetailCard(album, backupSelection),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumFilteredThumbnail extends StatelessWidget {
|
||||
final Asset? thumbnail;
|
||||
final BackupSelection selection;
|
||||
|
||||
const _AlbumFilteredThumbnail(this.thumbnail, this.selection);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
context.primaryColor.withAlpha(100),
|
||||
BlendMode.darken,
|
||||
);
|
||||
|
||||
ColorFilter excludedFilter =
|
||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||
|
||||
ColorFilter unselectedFilter =
|
||||
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||
|
||||
return ColorFiltered(
|
||||
colorFilter: switch (selection) {
|
||||
BackupSelection.select => selectedFilter,
|
||||
BackupSelection.exclude => excludedFilter,
|
||||
BackupSelection.none => unselectedFilter,
|
||||
},
|
||||
child: ImmichImage(thumbnail),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Portrait list components
|
||||
|
||||
class _AlbumDetailListTile extends StatelessWidget {
|
||||
final LocalAlbum album;
|
||||
final BackupSelection selection;
|
||||
|
||||
const _AlbumDetailListTile(this.album, this.selection);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Wrapped ListTile with Material to prevent tileColor overflow
|
||||
// https://github.com/flutter/flutter/issues/86584
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTile(
|
||||
tileColor: switch (selection) {
|
||||
BackupSelection.select => context.isDarkTheme
|
||||
? context.primaryColor.withAlpha(100)
|
||||
: context.primaryColor.withAlpha(25),
|
||||
BackupSelection.exclude => context.isDarkTheme
|
||||
? Colors.red[300]?.withAlpha(150)
|
||||
: Colors.red[100]?.withAlpha(150),
|
||||
BackupSelection.none => Colors.transparent,
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
width: 80,
|
||||
child: _AlbumFilteredThumbnail(album.thumbnail, selection),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
album.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(album.assetCount.toString()),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_AlbumDetailCardChip(selection),
|
||||
_AlbumPreviewButton(album),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Landscape card components
|
||||
|
||||
class _AlbumDetailCard extends StatelessWidget {
|
||||
final LocalAlbum album;
|
||||
final BackupSelection selection;
|
||||
|
||||
const _AlbumDetailCard(this.album, this.selection);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: const EdgeInsets.all(1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(12)), // if you need this
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 37, 35, 35)
|
||||
: const Color(0xFFC9C9C9),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
elevation: 0,
|
||||
borderOnForeground: false,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
_AlbumFilteredThumbnail(
|
||||
album.thumbnail,
|
||||
selection,
|
||||
),
|
||||
if (selection != BackupSelection.none)
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
right: 25,
|
||||
child: _AlbumDetailCardChip(selection),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 25),
|
||||
child: _AlbumDetailCardDetails(album),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumDetailCardChip extends StatelessWidget {
|
||||
final BackupSelection selection;
|
||||
|
||||
const _AlbumDetailCardChip(this.selection);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selection == BackupSelection.none) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Chip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
label: Text(
|
||||
selection == BackupSelection.select
|
||||
? "album_info_card_backup_album_included"
|
||||
: "album_info_card_backup_album_excluded",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
backgroundColor: selection == BackupSelection.select
|
||||
? context.primaryColor
|
||||
: Colors.red[300],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumDetailCardDetails extends StatelessWidget {
|
||||
final LocalAlbum album;
|
||||
|
||||
const _AlbumDetailCardDetails(this.album);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: FutureBuilder(
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
album.assetCount.toString() +
|
||||
(snapshot.data!.isAll
|
||||
? " (${'backup_all'.tr()})"
|
||||
: ""),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text("0");
|
||||
}),
|
||||
future: AssetPathEntity.fromId(album.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_AlbumPreviewButton(album),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumPreviewButton extends StatelessWidget {
|
||||
final LocalAlbum album;
|
||||
|
||||
const _AlbumPreviewButton(this.album);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () => context.pushRoute(
|
||||
LocalAlbumViewerRoute(
|
||||
album: album,
|
||||
selectEnabled: false,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.image_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
splashRadius: 25,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumPreviewPage extends HookConsumerWidget {
|
||||
final AssetPathEntity album;
|
||||
const AlbumPreviewPage({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assets = useState<List<AssetEntity>>([]);
|
||||
|
||||
getAssetsInAlbum() async {
|
||||
assets.value = await album.getAssetListRange(
|
||||
start: 0,
|
||||
end: await album.assetCountAsync,
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
getAssetsInAlbum();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
title: Column(
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
"ID ${album.id}",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
),
|
||||
),
|
||||
body: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
crossAxisSpacing: 2,
|
||||
mainAxisSpacing: 2,
|
||||
),
|
||||
itemCount: assets.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
Future<Uint8List?> thumbData =
|
||||
assets.value[index].thumbnailDataWithSize(
|
||||
const ThumbnailSize(200, 200),
|
||||
quality: 50,
|
||||
);
|
||||
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: thumbData,
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Image.memory(
|
||||
snapshot.data!,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: ImmichLoadingIndicator(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/backup_album_info_list_item.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -15,194 +15,19 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
const BackupAlbumSelectionPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
final albums = ref.watch(backupProvider).availableAlbums;
|
||||
final localAlbums = ref.watch(localAlbumsProvider);
|
||||
final searchValue = useValueNotifier('');
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||
ref.read(localAlbumsProvider.notifier).getDeviceAlbums();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
buildAlbumSelectionList() {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
((context, index) {
|
||||
var thumbnailData = albums[index].thumbnailData;
|
||||
return AlbumInfoListTile(
|
||||
imageData: thumbnailData,
|
||||
albumInfo: albums[index],
|
||||
);
|
||||
}),
|
||||
childCount: albums.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAlbumSelectionGrid() {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 300,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
itemCount: albums.length,
|
||||
itemBuilder: ((context, index) {
|
||||
var thumbnailData = albums[index].thumbnailData;
|
||||
return AlbumInfoCard(
|
||||
imageData: thumbnailData,
|
||||
albumInfo: albums[index],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildSelectedAlbumNameChip() {
|
||||
return selectedBackupAlbums.map((album) {
|
||||
void removeSelection() =>
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: GestureDetector(
|
||||
onTap: removeSelection,
|
||||
child: Chip(
|
||||
label: Text(
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDarkTheme ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: context.primaryColor,
|
||||
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
|
||||
deleteIcon: const Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 15,
|
||||
),
|
||||
onDeleted: removeSelection,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toSet();
|
||||
}
|
||||
|
||||
buildExcludedAlbumNameChip() {
|
||||
return excludedBackupAlbums.map((album) {
|
||||
void removeSelection() {
|
||||
ref
|
||||
.watch(backupProvider.notifier)
|
||||
.removeExcludedAlbumForBackup(album);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: removeSelection,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Chip(
|
||||
label: Text(
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.red[300],
|
||||
deleteIconColor:
|
||||
isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||
deleteIcon: const Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 15,
|
||||
),
|
||||
onDeleted: removeSelection,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toSet();
|
||||
}
|
||||
|
||||
// buildSearchBar() {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
|
||||
// child: TextFormField(
|
||||
// onChanged: (searchValue) {
|
||||
// // if (searchValue.isEmpty) {
|
||||
// // albums = availableAlbums;
|
||||
// // } else {
|
||||
// // albums.value = availableAlbums
|
||||
// // .where(
|
||||
// // (album) => album.name
|
||||
// // .toLowerCase()
|
||||
// // .contains(searchValue.toLowerCase()),
|
||||
// // )
|
||||
// // .toList();
|
||||
// // }
|
||||
// },
|
||||
// decoration: InputDecoration(
|
||||
// contentPadding: const EdgeInsets.symmetric(
|
||||
// horizontal: 8.0,
|
||||
// vertical: 8.0,
|
||||
// ),
|
||||
// hintText: "Search",
|
||||
// hintStyle: TextStyle(
|
||||
// color: isDarkTheme ? Colors.white : Colors.grey,
|
||||
// fontSize: 14.0,
|
||||
// ),
|
||||
// prefixIcon: const Icon(
|
||||
// Icons.search,
|
||||
// color: Colors.grey,
|
||||
// ),
|
||||
// border: OutlineInputBorder(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// borderSide: BorderSide.none,
|
||||
// ),
|
||||
// filled: true,
|
||||
// fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: const Text(
|
||||
"backup_album_selection_page_select_albums",
|
||||
).tr(),
|
||||
elevation: 0,
|
||||
),
|
||||
appBar: _AppBar(),
|
||||
body: CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
@@ -210,105 +35,26 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Text(
|
||||
"backup_album_selection_page_selection_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).tr(),
|
||||
),
|
||||
// Selected Album Chips
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
...buildSelectedAlbumNameChip(),
|
||||
...buildExcludedAlbumNameChip(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: Text(
|
||||
"backup_album_selection_page_albums_device".tr(
|
||||
args: [
|
||||
ref
|
||||
.watch(backupProvider)
|
||||
.availableAlbums
|
||||
.length
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
style: context.textTheme.titleSmall,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_albums_tap",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
trailing: IconButton(
|
||||
splashRadius: 16,
|
||||
icon: Icon(
|
||||
Icons.info,
|
||||
size: 20,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
onPressed: () {
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
'backup_album_selection_page_selection_info',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text(
|
||||
'backup_album_selection_page_assets_scatter',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// buildSearchBar(),
|
||||
_AlbumBackupInfoRow(localAlbums.valueOrNull?.length ?? 0),
|
||||
_AlbumSearchBar(onSearch: (value) => searchValue.value = value),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.crossAxisExtent > 600) {
|
||||
return buildAlbumSelectionGrid();
|
||||
} else {
|
||||
return buildAlbumSelectionList();
|
||||
}
|
||||
ValueListenableBuilder(
|
||||
valueListenable: searchValue,
|
||||
builder: (ctx, search, _) {
|
||||
final filteredAlbums = searchValue.value.isEmpty
|
||||
? localAlbums
|
||||
: localAlbums.whenData(
|
||||
(albums) => albums
|
||||
.where(
|
||||
(a) => a.name
|
||||
.toLowerCase()
|
||||
.contains(searchValue.value.toLowerCase()),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
return _SilverLocalAlbumSelectionList(albums: filteredAlbums);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -316,3 +62,172 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBar extends ImmichAppBar {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: const Text(
|
||||
"backup_album_selection_page_select_albums",
|
||||
).tr(),
|
||||
elevation: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumBackupInfoRow extends StatelessWidget {
|
||||
final int albumCount;
|
||||
|
||||
const _AlbumBackupInfoRow(this.albumCount);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void showBackupSelectionInfoDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
'backup_album_selection_page_selection_info',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text(
|
||||
'backup_album_selection_page_assets_scatter',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"backup_album_selection_page_albums_device"
|
||||
.tr(args: [albumCount.toString()]),
|
||||
style: context.textTheme.titleSmall,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_albums_tap",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
trailing: IconButton(
|
||||
splashRadius: 16,
|
||||
icon: Icon(
|
||||
Icons.info,
|
||||
size: 20,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
onPressed: showBackupSelectionInfoDialog,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumSearchBar extends StatelessWidget {
|
||||
final Function(String) onSearch;
|
||||
|
||||
const _AlbumSearchBar({required this.onSearch});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
|
||||
child: TextFormField(
|
||||
onChanged: onSearch,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(8.0),
|
||||
hintText: "Search",
|
||||
hintStyle: TextStyle(
|
||||
color: context.isDarkTheme ? Colors.white : Colors.grey,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Colors.grey,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: context.isDarkTheme ? Colors.white30 : Colors.grey[200],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer-sliver-prefix
|
||||
class _SilverLocalAlbumSelectionList extends StatelessWidget {
|
||||
final AsyncValue<List<LocalAlbum>> albums;
|
||||
|
||||
const _SilverLocalAlbumSelectionList({required this.albums});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (albums.isLoading) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (albums.hasError) {
|
||||
return SliverToBoxAdapter(child: Text("Error occured: ${albums.error}"));
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: context.isMobile
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
((context, index) {
|
||||
return BackupAlbumInfoListItem(
|
||||
album: albums.requireValue.elementAt(index),
|
||||
);
|
||||
}),
|
||||
childCount: albums.requireValue.length,
|
||||
),
|
||||
)
|
||||
: SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 300,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
itemCount: albums.requireValue.length,
|
||||
itemBuilder: ((context, index) {
|
||||
return BackupAlbumInfoListItem(
|
||||
album: albums.requireValue.elementAt(index),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -7,7 +6,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/extensions/object_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
@@ -15,7 +17,6 @@ import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.da
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -24,61 +25,71 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
|
||||
final backupState = ref.watch(backupProvider);
|
||||
final backupAlbums = ref.watch(backupAlbumsProvider);
|
||||
final deviceAssetState = ref.watch(deviceAssetsProvider);
|
||||
final hasAnyAlbum =
|
||||
backupAlbums.valueOrNull?.selectedBackupAlbums.isNotEmpty ?? false;
|
||||
|
||||
bool hasExclusiveAccess =
|
||||
backupState.backupProgress != BackUpProgressEnum.inBackground;
|
||||
bool shouldBackup = backupState.allUniqueAssets.length -
|
||||
backupState.selectedAlbumsBackupAssetsIds.length ==
|
||||
0 ||
|
||||
bool shouldBackup = deviceAssetState.valueOrNull?.assetsRemaining == 0 ||
|
||||
!hasExclusiveAccess
|
||||
? false
|
||||
: true;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress &&
|
||||
backupState.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||
}
|
||||
|
||||
// Update the background settings information just to make sure we
|
||||
// have the latest, since the platform channel will not update
|
||||
// automatically
|
||||
if (Platform.isIOS) {
|
||||
ref.watch(iOSBackgroundSettingsProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
ref
|
||||
.watch(websocketProvider.notifier)
|
||||
.stopListenToEvent('on_upload_success');
|
||||
ref.read(backupAlbumsProvider.notifier).refreshAlbumAssetsState();
|
||||
ref.invalidate(deviceAssetsProvider);
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Widget buildSelectedAlbumName() {
|
||||
Future<String> getSelectedAlbumNames() async {
|
||||
var text = "backup_controller_page_backup_selected".tr();
|
||||
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||
|
||||
if (albums.isNotEmpty) {
|
||||
for (var album in albums) {
|
||||
if (album.name == "Recent" || album.name == "Recents") {
|
||||
text += "${album.name} (${'backup_all'.tr()}), ";
|
||||
} else {
|
||||
text += "${album.name}, ";
|
||||
}
|
||||
final selectedAlbums =
|
||||
backupAlbums.valueOrNull?.selectedBackupAlbums ?? [];
|
||||
for (final selected in selectedAlbums) {
|
||||
await selected.album.load();
|
||||
final album = selected.album.value;
|
||||
if (album == null) {
|
||||
continue;
|
||||
}
|
||||
if (album.name == "Recent" || album.name == "Recents") {
|
||||
text += "${album.name} (${'backup_all'.tr()}), ";
|
||||
} else {
|
||||
text += "${album.name}, ";
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
text.trim().substring(0, text.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
Widget buildSelectedAlbumName() {
|
||||
final selectedAlbums =
|
||||
backupAlbums.valueOrNull?.selectedBackupAlbums ?? [];
|
||||
|
||||
if (selectedAlbums.isNotEmpty) {
|
||||
return FutureBuilder(
|
||||
future: getSelectedAlbumNames(),
|
||||
builder: (_, data) => data.hasData
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
data.data!.trim().substring(0, data.data!.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
@@ -93,23 +104,39 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildExcludedAlbumName() {
|
||||
Future<String> getExcludedAlbumNames() async {
|
||||
var text = "backup_controller_page_excluded".tr();
|
||||
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||
|
||||
if (albums.isNotEmpty) {
|
||||
for (var album in albums) {
|
||||
text += "${album.name}, ";
|
||||
final excludedAlbums =
|
||||
backupAlbums.valueOrNull?.excludedBackupAlbums ?? [];
|
||||
for (final excluded in excludedAlbums) {
|
||||
excluded.album.loadSync();
|
||||
final album = excluded.album.value;
|
||||
if (album == null) {
|
||||
continue;
|
||||
}
|
||||
text += "${album.name}, ";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
text.trim().substring(0, text.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
Widget buildExcludedAlbumName() {
|
||||
final excludedAlbums =
|
||||
backupAlbums.valueOrNull?.excludedBackupAlbums ?? [];
|
||||
|
||||
if (excludedAlbums.isNotEmpty) {
|
||||
return FutureBuilder(
|
||||
future: getExcludedAlbumNames(),
|
||||
builder: (_, data) => data.hasData
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
data.data!.trim().substring(0, data.data!.length - 2),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
@@ -121,7 +148,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 56, 56, 56)
|
||||
@@ -152,15 +179,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await context.pushRoute(const BackupAlbumSelectionRoute());
|
||||
// waited until returning from selection
|
||||
await ref
|
||||
.read(backupProvider.notifier)
|
||||
.backupAlbumSelectionDone();
|
||||
// waited until backup albums are stored in DB
|
||||
ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||
},
|
||||
onPressed: () =>
|
||||
context.pushRoute(const BackupAlbumSelectionRoute()),
|
||||
child: const Text(
|
||||
"backup_controller_page_select",
|
||||
style: TextStyle(
|
||||
@@ -242,10 +262,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
"backup_controller_page_backup",
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||
context.popRoute(true);
|
||||
},
|
||||
onPressed: () => context.popRoute(true),
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_rounded,
|
||||
@@ -274,23 +291,32 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
BackupInfoCard(
|
||||
title: "backup_controller_page_total".tr(),
|
||||
subtitle: "backup_controller_page_total_sub".tr(),
|
||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
||||
info: ref
|
||||
.watch(localAlbumsProvider)
|
||||
.valueOrNull
|
||||
.isNullOrEmpty
|
||||
? "..."
|
||||
: "${backupState.allUniqueAssets.length}",
|
||||
: "${deviceAssetState.valueOrNull?.uniqueAssetsToBackup ?? 0}",
|
||||
),
|
||||
BackupInfoCard(
|
||||
title: "backup_controller_page_backup".tr(),
|
||||
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
||||
info: ref
|
||||
.watch(localAlbumsProvider)
|
||||
.valueOrNull
|
||||
.isNullOrEmpty
|
||||
? "..."
|
||||
: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||
: "${deviceAssetState.valueOrNull?.backedUpAssets ?? 0}",
|
||||
),
|
||||
BackupInfoCard(
|
||||
title: "backup_controller_page_remainder".tr(),
|
||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||
info: ref.watch(backupProvider).availableAlbums.isEmpty
|
||||
info: ref
|
||||
.watch(localAlbumsProvider)
|
||||
.valueOrNull
|
||||
.isNullOrEmpty
|
||||
? "..."
|
||||
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
|
||||
: "${deviceAssetState.valueOrNull?.assetsRemaining ?? 0}",
|
||||
),
|
||||
const Divider(),
|
||||
const CurrentUploadingAssetInfoBox(),
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
|
||||
@@ -28,7 +28,8 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
const BackupOptionsPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final deviceAssetState = ref.watch(deviceAssetsProvider);
|
||||
final backupSettings = ref.watch(backupSettingsProvider);
|
||||
final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
|
||||
final settingsService = ref.watch(appSettingsServiceProvider);
|
||||
final showBackupFix = Platform.isAndroid &&
|
||||
@@ -64,8 +65,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
void performBackupCheck() async {
|
||||
try {
|
||||
checkInProgress.value = true;
|
||||
if (backupState.allUniqueAssets.length >
|
||||
backupState.selectedAlbumsBackupAssetsIds.length) {
|
||||
if (deviceAssetState.valueOrNull?.assetsRemaining != 0) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "Backup all assets before starting this check!",
|
||||
@@ -194,9 +194,9 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Widget buildBackgroundBackupController() {
|
||||
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
||||
final bool isWifiRequired = backupState.backupRequireWifi;
|
||||
final bool isChargingRequired = backupState.backupRequireCharging;
|
||||
final bool isBackgroundEnabled = backupSettings.backgroundBackup;
|
||||
final bool isWifiRequired = backupSettings.backupRequireWifi;
|
||||
final bool isChargingRequired = backupSettings.backupRequireCharging;
|
||||
final Color activeColor = context.primaryColor;
|
||||
|
||||
String formatBackupDelaySliderValue(double v) {
|
||||
@@ -236,7 +236,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
final triggerDelay =
|
||||
useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
|
||||
useState(backupDelayToSliderValue(backupSettings.backupTriggerDelay));
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -276,7 +276,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
activeColor: activeColor,
|
||||
value: isWifiRequired,
|
||||
onChanged: (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.read(backupSettingsProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireWifi: isChecked,
|
||||
onError: showErrorToUser,
|
||||
@@ -296,7 +296,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
activeColor: activeColor,
|
||||
value: isChargingRequired,
|
||||
onChanged: (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.read(backupSettingsProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireCharging: isChecked,
|
||||
onError: showErrorToUser,
|
||||
@@ -319,7 +319,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
value: triggerDelay.value,
|
||||
onChanged: (double v) => triggerDelay.value = v,
|
||||
onChangeEnd: (double v) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.read(backupSettingsProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
triggerDelay: backupDelayToMilliseconds(v),
|
||||
onError: showErrorToUser,
|
||||
@@ -333,7 +333,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref
|
||||
.read(backupProvider.notifier)
|
||||
.read(backupSettingsProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
enabled: !isBackgroundEnabled,
|
||||
onError: showErrorToUser,
|
||||
@@ -411,7 +411,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
ListTile buildAutoBackupController() {
|
||||
final isAutoBackup = backupState.autoBackup;
|
||||
final isAutoBackup = backupSettings.autoBackup;
|
||||
final backUpOption = isAutoBackup
|
||||
? "backup_controller_page_status_on".tr()
|
||||
: "backup_controller_page_status_off".tr();
|
||||
@@ -443,7 +443,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => ref
|
||||
.read(backupProvider.notifier)
|
||||
.read(backupSettingsProvider.notifier)
|
||||
.setAutoBackup(!isAutoBackup),
|
||||
child: Text(
|
||||
backupBtnText,
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
|
||||
import 'package:immich_mobile/modules/home/models/selection_state.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
||||
class ControlBottomAppBar extends ConsumerWidget {
|
||||
final void Function(bool shareLocal) onShare;
|
||||
@@ -19,7 +19,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
final void Function([bool force])? onDelete;
|
||||
final void Function([bool force])? onDeleteServer;
|
||||
final void Function(bool onlyBackedUp)? onDeleteLocal;
|
||||
final Function(Album album) onAddToAlbum;
|
||||
final Function(RemoteAlbum album) onAddToAlbum;
|
||||
final void Function() onCreateNewAlbum;
|
||||
final void Function() onUpload;
|
||||
final void Function()? onStack;
|
||||
@@ -61,7 +61,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
||||
selectionAssetState.hasLocal || selectionAssetState.hasMerged;
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final albums = ref.watch(remoteAlbumsProvider).valueOrNull ?? [];
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
const bottomPadding = 0.20;
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_lane.dart';
|
||||
@@ -34,8 +35,9 @@ class HomePage extends HookConsumerWidget {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(localAlbumsProvider.notifier).getDeviceAlbums();
|
||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
@@ -116,7 +116,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
Store.delete(StoreKey.currentUser),
|
||||
Store.delete(StoreKey.accessToken),
|
||||
]);
|
||||
_ref.invalidate(albumProvider);
|
||||
_ref.invalidate(remoteAlbumsProvider);
|
||||
_ref.invalidate(sharedAlbumProvider);
|
||||
|
||||
state = state.copyWith(
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$mapStateNotifierHash() => r'3b509b57b7400b09817e9caee9debf899172cd52';
|
||||
String _$mapStateNotifierHash() => r'6408d616ec9fc0d1ff26e25692417c43504ff754';
|
||||
|
||||
/// See also [MapStateNotifier].
|
||||
@ProviderFor(MapStateNotifier)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState;
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
|
||||
class LocalStorageSettings extends HookConsumerWidget {
|
||||
const LocalStorageSettings({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isarDb = ref.watch(dbProvider);
|
||||
final cacheItemCount = useState(0);
|
||||
useEffect(
|
||||
() {
|
||||
cacheItemCount.value = isarDb.duplicatedAssets.countSync();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
void clearCache() {
|
||||
isarDb.writeTxnSync(() => isarDb.duplicatedAssets.clearSync());
|
||||
cacheItemCount.value = isarDb.duplicatedAssets.countSync();
|
||||
}
|
||||
|
||||
return ExpansionTile(
|
||||
textColor: context.primaryColor,
|
||||
title: Text(
|
||||
"cache_settings_tile_title",
|
||||
style: context.textTheme.titleMedium,
|
||||
).tr(),
|
||||
subtitle: const Text(
|
||||
"cache_settings_tile_subtitle",
|
||||
).tr(),
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
"cache_settings_duplicated_assets_title",
|
||||
style: context.textTheme.titleSmall,
|
||||
).tr(args: ["${cacheItemCount.value}"]),
|
||||
subtitle: const Text(
|
||||
"cache_settings_duplicated_assets_subtitle",
|
||||
).tr(),
|
||||
trailing: TextButton(
|
||||
onPressed: cacheItemCount.value > 0 ? clearCache : null,
|
||||
child: Text(
|
||||
"cache_settings_duplicated_assets_clear_button",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: cacheItemCount.value > 0 ? Colors.red : Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
||||
@@ -40,7 +39,6 @@ class SettingsPage extends HookConsumerWidget {
|
||||
const AssetListSettings(),
|
||||
const NotificationSetting(),
|
||||
// const ExperimentalSettings(),
|
||||
const LocalStorageSettings(),
|
||||
const AdvancedSettings(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,9 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/activities/views/activities_page.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/modules/album/views/album_options_part.dart';
|
||||
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/album_options_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/remote_album_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
||||
@@ -21,7 +22,7 @@ import 'package:immich_mobile/modules/album/views/sharing_page.dart';
|
||||
import 'package:immich_mobile/modules/archive/views/archive_page.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||
import 'package:immich_mobile/modules/album/views/local_album_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
||||
@@ -49,7 +50,6 @@ import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
@@ -60,7 +60,6 @@ import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
||||
@@ -157,7 +156,7 @@ class AppRouter extends _$AppRouter {
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
),
|
||||
AutoRoute(
|
||||
page: AlbumViewerRoute.page,
|
||||
page: RemoteAlbumViewerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
CustomRoute(
|
||||
@@ -170,7 +169,7 @@ class AppRouter extends _$AppRouter {
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: AlbumPreviewRoute.page,
|
||||
page: LocalAlbumViewerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
CustomRoute(
|
||||
|
||||
+108
-102
@@ -31,26 +31,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumPreviewRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumPreviewRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: AlbumPreviewPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: AlbumViewerPage(
|
||||
key: args.key,
|
||||
albumId: args.albumId,
|
||||
),
|
||||
);
|
||||
},
|
||||
AllMotionPhotosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -182,6 +162,17 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const LibraryPage(),
|
||||
);
|
||||
},
|
||||
LocalAlbumViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<LocalAlbumViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: LocalAlbumViewerPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
selectEnabled: args.selectEnabled,
|
||||
),
|
||||
);
|
||||
},
|
||||
LoginRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -255,6 +246,16 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const RecentlyAddedPage(),
|
||||
);
|
||||
},
|
||||
RemoteAlbumViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<RemoteAlbumViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: RemoteAlbumViewerPage(
|
||||
key: args.key,
|
||||
albumId: args.albumId,
|
||||
),
|
||||
);
|
||||
},
|
||||
SearchRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SearchRouteArgs>(
|
||||
orElse: () => const SearchRouteArgs());
|
||||
@@ -382,7 +383,7 @@ class ActivitiesRoute extends PageRouteInfo<void> {
|
||||
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
||||
AlbumOptionsRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
required RemoteAlbum album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumOptionsRoute.name,
|
||||
@@ -407,7 +408,7 @@ class AlbumOptionsRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -415,82 +416,6 @@ class AlbumOptionsRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumPreviewPage]
|
||||
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
|
||||
AlbumPreviewRoute({
|
||||
Key? key,
|
||||
required AssetPathEntity album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumPreviewRoute.name,
|
||||
args: AlbumPreviewRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumPreviewRoute';
|
||||
|
||||
static const PageInfo<AlbumPreviewRouteArgs> page =
|
||||
PageInfo<AlbumPreviewRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumPreviewRouteArgs {
|
||||
const AlbumPreviewRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final AssetPathEntity album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumPreviewRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumViewerPage]
|
||||
class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
|
||||
AlbumViewerRoute({
|
||||
Key? key,
|
||||
required int albumId,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumViewerRoute.name,
|
||||
args: AlbumViewerRouteArgs(
|
||||
key: key,
|
||||
albumId: albumId,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumViewerRoute';
|
||||
|
||||
static const PageInfo<AlbumViewerRouteArgs> page =
|
||||
PageInfo<AlbumViewerRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumViewerRouteArgs {
|
||||
const AlbumViewerRouteArgs({
|
||||
this.key,
|
||||
required this.albumId,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final int albumId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumViewerRouteArgs{key: $key, albumId: $albumId}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AllMotionPhotosPage]
|
||||
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
||||
@@ -874,6 +799,49 @@ class LibraryRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LocalAlbumViewerPage]
|
||||
class LocalAlbumViewerRoute extends PageRouteInfo<LocalAlbumViewerRouteArgs> {
|
||||
LocalAlbumViewerRoute({
|
||||
Key? key,
|
||||
required LocalAlbum album,
|
||||
bool selectEnabled = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
LocalAlbumViewerRoute.name,
|
||||
args: LocalAlbumViewerRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
selectEnabled: selectEnabled,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LocalAlbumViewerRoute';
|
||||
|
||||
static const PageInfo<LocalAlbumViewerRouteArgs> page =
|
||||
PageInfo<LocalAlbumViewerRouteArgs>(name);
|
||||
}
|
||||
|
||||
class LocalAlbumViewerRouteArgs {
|
||||
const LocalAlbumViewerRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
this.selectEnabled = true,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LocalAlbum album;
|
||||
|
||||
final bool selectEnabled;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocalAlbumViewerRouteArgs{key: $key, album: $album, selectEnabled: $selectEnabled}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LoginPage]
|
||||
class LoginRoute extends PageRouteInfo<void> {
|
||||
@@ -1105,6 +1073,44 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RemoteAlbumViewerPage]
|
||||
class RemoteAlbumViewerRoute extends PageRouteInfo<RemoteAlbumViewerRouteArgs> {
|
||||
RemoteAlbumViewerRoute({
|
||||
Key? key,
|
||||
required int albumId,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
RemoteAlbumViewerRoute.name,
|
||||
args: RemoteAlbumViewerRouteArgs(
|
||||
key: key,
|
||||
albumId: albumId,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'RemoteAlbumViewerRoute';
|
||||
|
||||
static const PageInfo<RemoteAlbumViewerRouteArgs> page =
|
||||
PageInfo<RemoteAlbumViewerRouteArgs>(name);
|
||||
}
|
||||
|
||||
class RemoteAlbumViewerRouteArgs {
|
||||
const RemoteAlbumViewerRouteArgs({
|
||||
this.key,
|
||||
required this.albumId,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final int albumId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RemoteAlbumViewerRouteArgs{key: $key, albumId: $albumId}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchPage]
|
||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
||||
@@ -1177,7 +1183,7 @@ class SelectAdditionalUserForSharingRoute
|
||||
extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> {
|
||||
SelectAdditionalUserForSharingRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
required RemoteAlbum album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SelectAdditionalUserForSharingRoute.name,
|
||||
@@ -1202,7 +1208,7 @@ class SelectAdditionalUserForSharingRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
final RemoteAlbum album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -1393,7 +1399,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||
void Function()? onPaused,
|
||||
Widget? placeholder,
|
||||
bool showControls = true,
|
||||
Duration hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||
bool showDownloadingIndicator = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
@@ -1429,7 +1435,7 @@ class VideoViewerRouteArgs {
|
||||
this.onPaused,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/modules/search/providers/people.provider.dart';
|
||||
|
||||
@@ -47,7 +48,8 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||
}
|
||||
|
||||
if (route.name == 'LibraryRoute') {
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
ref.read(localAlbumsProvider.notifier).getDeviceAlbums();
|
||||
}
|
||||
|
||||
if (route.name == 'HomeRoute') {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
part 'album.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class Album {
|
||||
@protected
|
||||
Album({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.modifiedAt,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.lastModifiedAssetTimestamp,
|
||||
required this.shared,
|
||||
required this.activityEnabled,
|
||||
});
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? localId;
|
||||
String name;
|
||||
DateTime createdAt;
|
||||
DateTime modifiedAt;
|
||||
DateTime? startDate;
|
||||
DateTime? endDate;
|
||||
DateTime? lastModifiedAssetTimestamp;
|
||||
bool shared;
|
||||
bool activityEnabled;
|
||||
final IsarLink<User> owner = IsarLink<User>();
|
||||
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
|
||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||
|
||||
@ignore
|
||||
bool get isRemote => remoteId != null;
|
||||
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
|
||||
@ignore
|
||||
int get assetCount => assets.length;
|
||||
|
||||
@ignore
|
||||
String? get ownerId => owner.value?.id;
|
||||
|
||||
@ignore
|
||||
String? get ownerName {
|
||||
// Guard null owner
|
||||
if (owner.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final name = <String>[];
|
||||
if (owner.value?.name != null) {
|
||||
name.add(owner.value!.name);
|
||||
}
|
||||
|
||||
return name.join(' ');
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Album) return false;
|
||||
|
||||
final lastModifiedAssetTimestampIsSetAndEqual =
|
||||
lastModifiedAssetTimestamp != null &&
|
||||
other.lastModifiedAssetTimestamp != null
|
||||
? lastModifiedAssetTimestamp!
|
||||
.isAtSameMomentAs(other.lastModifiedAssetTimestamp!)
|
||||
: true;
|
||||
|
||||
return id == other.id &&
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
name == other.name &&
|
||||
createdAt.isAtSameMomentAs(other.createdAt) &&
|
||||
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
||||
lastModifiedAssetTimestampIsSetAndEqual &&
|
||||
shared == other.shared &&
|
||||
activityEnabled == other.activityEnabled &&
|
||||
owner.value == other.owner.value &&
|
||||
thumbnail.value == other.thumbnail.value &&
|
||||
sharedUsers.length == other.sharedUsers.length &&
|
||||
assets.length == other.assets.length;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
localId.hashCode ^
|
||||
name.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
modifiedAt.hashCode ^
|
||||
lastModifiedAssetTimestamp.hashCode ^
|
||||
shared.hashCode ^
|
||||
activityEnabled.hashCode ^
|
||||
owner.value.hashCode ^
|
||||
thumbnail.value.hashCode ^
|
||||
sharedUsers.length.hashCode ^
|
||||
assets.length.hashCode;
|
||||
|
||||
static Album local(AssetPathEntity ape) {
|
||||
final Album a = Album(
|
||||
name: ape.name,
|
||||
createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||
modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||
shared: false,
|
||||
activityEnabled: false,
|
||||
);
|
||||
a.owner.value = Store.get(StoreKey.currentUser);
|
||||
a.localId = ape.id;
|
||||
return a;
|
||||
}
|
||||
|
||||
static Future<Album> remote(AlbumResponseDto dto) async {
|
||||
final Isar db = Isar.getInstance()!;
|
||||
final Album a = Album(
|
||||
remoteId: dto.id,
|
||||
name: dto.albumName,
|
||||
createdAt: dto.createdAt,
|
||||
modifiedAt: dto.updatedAt,
|
||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||
shared: dto.shared,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
activityEnabled: dto.isActivityEnabled,
|
||||
);
|
||||
a.owner.value = await db.users.getById(dto.ownerId);
|
||||
if (dto.albumThumbnailAssetId != null) {
|
||||
a.thumbnail.value = await db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
if (dto.sharedUsers.isNotEmpty) {
|
||||
final users = await db.users
|
||||
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
|
||||
a.sharedUsers.addAll(users.cast());
|
||||
}
|
||||
if (dto.assets.isNotEmpty) {
|
||||
final assets =
|
||||
await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id));
|
||||
a.assets.addAll(assets);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
||||
extension AssetsHelper on IsarCollection<Album> {
|
||||
Future<void> store(Album a) async {
|
||||
await put(a);
|
||||
await a.owner.save();
|
||||
await a.thumbnail.save();
|
||||
await a.sharedUsers.save();
|
||||
await a.assets.save();
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
String get eTagKeyAssetCount => "device-album-$id-asset-count";
|
||||
}
|
||||
Generated
-1796
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'android_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class AndroidDeviceAsset extends DeviceAsset {
|
||||
AndroidDeviceAsset({required this.id, required super.hash});
|
||||
Id id;
|
||||
}
|
||||
-493
@@ -1,493 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'android_device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetAndroidDeviceAssetCollection on Isar {
|
||||
IsarCollection<AndroidDeviceAsset> get androidDeviceAssets =>
|
||||
this.collection();
|
||||
}
|
||||
|
||||
const AndroidDeviceAssetSchema = CollectionSchema(
|
||||
name: r'AndroidDeviceAsset',
|
||||
id: -6758387181232899335,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
)
|
||||
},
|
||||
estimateSize: _androidDeviceAssetEstimateSize,
|
||||
serialize: _androidDeviceAssetSerialize,
|
||||
deserialize: _androidDeviceAssetDeserialize,
|
||||
deserializeProp: _androidDeviceAssetDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _androidDeviceAssetGetId,
|
||||
getLinks: _androidDeviceAssetGetLinks,
|
||||
attach: _androidDeviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _androidDeviceAssetEstimateSize(
|
||||
AndroidDeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _androidDeviceAssetSerialize(
|
||||
AndroidDeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
}
|
||||
|
||||
AndroidDeviceAsset _androidDeviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = AndroidDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: id,
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _androidDeviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _androidDeviceAssetGetId(AndroidDeviceAsset object) {
|
||||
return object.id;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _androidDeviceAssetGetLinks(
|
||||
AndroidDeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _androidDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, AndroidDeviceAsset object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhere> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhere
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idGreaterThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idLessThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryFilter
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryObject
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQueryLinks
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortBy> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct>
|
||||
distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryProperty
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<AndroidDeviceAsset, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,44 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class DeviceAsset {
|
||||
DeviceAsset({required this.hash});
|
||||
DeviceAsset({
|
||||
required this.id,
|
||||
required this.hash,
|
||||
this.backupSelection = BackupSelection.none,
|
||||
});
|
||||
|
||||
Id get isarId => Isar.autoIncrement;
|
||||
@Index(replace: true, unique: true, type: IndexType.hash)
|
||||
String id;
|
||||
|
||||
@Index(unique: false, type: IndexType.hash)
|
||||
List<byte> hash;
|
||||
|
||||
@enumerated
|
||||
BackupSelection backupSelection;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceAsset(id: $id, hash: $hash, backupSelection: $backupSelection)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DeviceAsset other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
listEquals(other.hash, hash) &&
|
||||
other.backupSelection == backupSelection;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode {
|
||||
return id.hashCode ^ hash.hashCode ^ backupSelection.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+233
-120
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ios_device_asset.dart';
|
||||
part of 'device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
@@ -9,29 +9,35 @@ part of 'ios_device_asset.dart';
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetIOSDeviceAssetCollection on Isar {
|
||||
IsarCollection<IOSDeviceAsset> get iOSDeviceAssets => this.collection();
|
||||
extension GetDeviceAssetCollection on Isar {
|
||||
IsarCollection<DeviceAsset> get deviceAssets => this.collection();
|
||||
}
|
||||
|
||||
const IOSDeviceAssetSchema = CollectionSchema(
|
||||
name: r'IOSDeviceAsset',
|
||||
id: -1671546753821948030,
|
||||
const DeviceAssetSchema = CollectionSchema(
|
||||
name: r'DeviceAsset',
|
||||
id: -2122558759826746878,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
r'backupSelection': PropertySchema(
|
||||
id: 0,
|
||||
name: r'backupSelection',
|
||||
type: IsarType.byte,
|
||||
enumMap: _DeviceAssetbackupSelectionEnumValueMap,
|
||||
),
|
||||
r'hash': PropertySchema(
|
||||
id: 1,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 1,
|
||||
id: 2,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _iOSDeviceAssetEstimateSize,
|
||||
serialize: _iOSDeviceAssetSerialize,
|
||||
deserialize: _iOSDeviceAssetDeserialize,
|
||||
deserializeProp: _iOSDeviceAssetDeserializeProp,
|
||||
estimateSize: _deviceAssetEstimateSize,
|
||||
serialize: _deviceAssetSerialize,
|
||||
deserialize: _deviceAssetDeserialize,
|
||||
deserializeProp: _deviceAssetDeserializeProp,
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
@@ -63,14 +69,14 @@ const IOSDeviceAssetSchema = CollectionSchema(
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _iOSDeviceAssetGetId,
|
||||
getLinks: _iOSDeviceAssetGetLinks,
|
||||
attach: _iOSDeviceAssetAttach,
|
||||
getId: _deviceAssetGetId,
|
||||
getLinks: _deviceAssetGetLinks,
|
||||
attach: _deviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _iOSDeviceAssetEstimateSize(
|
||||
IOSDeviceAsset object,
|
||||
int _deviceAssetEstimateSize(
|
||||
DeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
@@ -80,30 +86,34 @@ int _iOSDeviceAssetEstimateSize(
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetSerialize(
|
||||
IOSDeviceAsset object,
|
||||
void _deviceAssetSerialize(
|
||||
DeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
writer.writeString(offsets[1], object.id);
|
||||
writer.writeByte(offsets[0], object.backupSelection.index);
|
||||
writer.writeByteList(offsets[1], object.hash);
|
||||
writer.writeString(offsets[2], object.id);
|
||||
}
|
||||
|
||||
IOSDeviceAsset _iOSDeviceAssetDeserialize(
|
||||
DeviceAsset _deviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = IOSDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: reader.readString(offsets[1]),
|
||||
final object = DeviceAsset(
|
||||
backupSelection: _DeviceAssetbackupSelectionValueEnumMap[
|
||||
reader.readByteOrNull(offsets[0])] ??
|
||||
BackupSelection.none,
|
||||
hash: reader.readByteList(offsets[1]) ?? [],
|
||||
id: reader.readString(offsets[2]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _iOSDeviceAssetDeserializeProp<P>(
|
||||
P _deviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
@@ -111,31 +121,46 @@ P _iOSDeviceAssetDeserializeProp<P>(
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
return (_DeviceAssetbackupSelectionValueEnumMap[
|
||||
reader.readByteOrNull(offset)] ??
|
||||
BackupSelection.none) as P;
|
||||
case 1:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
case 2:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _iOSDeviceAssetGetId(IOSDeviceAsset object) {
|
||||
const _DeviceAssetbackupSelectionEnumValueMap = {
|
||||
'none': 0,
|
||||
'select': 1,
|
||||
'exclude': 2,
|
||||
};
|
||||
const _DeviceAssetbackupSelectionValueEnumMap = {
|
||||
0: BackupSelection.none,
|
||||
1: BackupSelection.select,
|
||||
2: BackupSelection.exclude,
|
||||
};
|
||||
|
||||
Id _deviceAssetGetId(DeviceAsset object) {
|
||||
return object.isarId;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) {
|
||||
List<IsarLinkBase<dynamic>> _deviceAssetGetLinks(DeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, IOSDeviceAsset object) {}
|
||||
void _deviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, DeviceAsset object) {}
|
||||
|
||||
extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
|
||||
Future<IOSDeviceAsset?> getById(String id) {
|
||||
extension DeviceAssetByIndex on IsarCollection<DeviceAsset> {
|
||||
Future<DeviceAsset?> getById(String id) {
|
||||
return getByIndex(r'id', [id]);
|
||||
}
|
||||
|
||||
IOSDeviceAsset? getByIdSync(String id) {
|
||||
DeviceAsset? getByIdSync(String id) {
|
||||
return getByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
@@ -147,12 +172,12 @@ extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
|
||||
return deleteByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
Future<List<IOSDeviceAsset?>> getAllById(List<String> idValues) {
|
||||
Future<List<DeviceAsset?>> getAllById(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndex(r'id', values);
|
||||
}
|
||||
|
||||
List<IOSDeviceAsset?> getAllByIdSync(List<String> idValues) {
|
||||
List<DeviceAsset?> getAllByIdSync(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndexSync(r'id', values);
|
||||
}
|
||||
@@ -167,36 +192,35 @@ extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
|
||||
return deleteAllByIndexSync(r'id', values);
|
||||
}
|
||||
|
||||
Future<Id> putById(IOSDeviceAsset object) {
|
||||
Future<Id> putById(DeviceAsset object) {
|
||||
return putByIndex(r'id', object);
|
||||
}
|
||||
|
||||
Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) {
|
||||
Id putByIdSync(DeviceAsset object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'id', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllById(List<IOSDeviceAsset> objects) {
|
||||
Future<List<Id>> putAllById(List<DeviceAsset> objects) {
|
||||
return putAllByIndex(r'id', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByIdSync(List<IOSDeviceAsset> objects,
|
||||
{bool saveLinks = true}) {
|
||||
List<Id> putAllByIdSync(List<DeviceAsset> objects, {bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhere> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhere> anyIsarId() {
|
||||
extension DeviceAssetQueryWhereSort
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QWhere> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhere> anyIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhere
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdEqualTo(
|
||||
extension DeviceAssetQueryWhere
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QWhereClause> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> isarIdEqualTo(
|
||||
Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
@@ -206,8 +230,8 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdNotEqualTo(Id isarId) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> isarIdNotEqualTo(
|
||||
Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
@@ -229,8 +253,9 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdGreaterThan(Id isarId, {bool include = false}) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> isarIdGreaterThan(
|
||||
Id isarId,
|
||||
{bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
|
||||
@@ -238,8 +263,9 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdLessThan(Id isarId, {bool include = false}) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> isarIdLessThan(
|
||||
Id isarId,
|
||||
{bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
|
||||
@@ -247,7 +273,7 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdBetween(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> isarIdBetween(
|
||||
Id lowerIsarId,
|
||||
Id upperIsarId, {
|
||||
bool includeLower = true,
|
||||
@@ -263,7 +289,7 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idEqualTo(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> idEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
@@ -273,7 +299,7 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idNotEqualTo(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> idNotEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
@@ -308,7 +334,7 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> hashEqualTo(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> hashEqualTo(
|
||||
List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
@@ -318,8 +344,8 @@ extension IOSDeviceAssetQueryWhere
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterWhereClause> hashNotEqualTo(
|
||||
List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
@@ -354,9 +380,65 @@ extension IOSDeviceAssetQueryWhere
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryFilter
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
extension DeviceAssetQueryFilter
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
backupSelectionEqualTo(BackupSelection value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'backupSelection',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
backupSelectionGreaterThan(
|
||||
BackupSelection value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'backupSelection',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
backupSelectionLessThan(
|
||||
BackupSelection value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'backupSelection',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
backupSelectionBetween(
|
||||
BackupSelection lower,
|
||||
BackupSelection upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'backupSelection',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
@@ -366,7 +448,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
@@ -380,7 +462,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
@@ -394,7 +476,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
@@ -412,7 +494,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
@@ -425,8 +507,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
@@ -438,7 +519,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
@@ -451,7 +532,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
@@ -467,7 +548,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
@@ -483,7 +564,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
@@ -501,7 +582,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idEqualTo(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
@@ -514,8 +595,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
@@ -530,8 +610,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
@@ -546,7 +625,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idBetween(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
@@ -565,8 +644,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idStartsWith(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
@@ -579,8 +657,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idEndsWith(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
@@ -593,8 +670,9 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idContains(String value, {bool caseSensitive = true}) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'id',
|
||||
@@ -604,7 +682,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idMatches(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -616,8 +694,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsEmpty() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
@@ -626,8 +703,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsNotEmpty() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> idIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'id',
|
||||
@@ -636,8 +712,8 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdEqualTo(Id value) {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> isarIdEqualTo(
|
||||
Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isarId',
|
||||
@@ -646,7 +722,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition>
|
||||
isarIdGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
@@ -660,8 +736,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdLessThan(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> isarIdLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
@@ -674,8 +749,7 @@ extension IOSDeviceAssetQueryFilter
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdBetween(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterFilterCondition> isarIdBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
@@ -693,64 +767,96 @@ extension IOSDeviceAssetQueryFilter
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryObject
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
extension DeviceAssetQueryObject
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQueryLinks
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
extension DeviceAssetQueryLinks
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQuerySortBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortById() {
|
||||
extension DeviceAssetQuerySortBy
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QSortBy> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> sortByBackupSelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'backupSelection', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy>
|
||||
sortByBackupSelectionDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'backupSelection', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortByIdDesc() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> sortByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenById() {
|
||||
extension DeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> thenByBackupSelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'backupSelection', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy>
|
||||
thenByBackupSelectionDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'backupSelection', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIdDesc() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIsarId() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> thenByIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy>
|
||||
thenByIsarIdDesc() {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QAfterSortBy> thenByIsarIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctByHash() {
|
||||
extension DeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QDistinct> {
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QDistinct>
|
||||
distinctByBackupSelection() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'backupSelection');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QDistinct> distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctById(
|
||||
QueryBuilder<DeviceAsset, DeviceAsset, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
|
||||
@@ -758,21 +864,28 @@ extension IOSDeviceAssetQueryWhereDistinct
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryProperty
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<IOSDeviceAsset, int, QQueryOperations> isarIdProperty() {
|
||||
extension DeviceAssetQueryProperty
|
||||
on QueryBuilder<DeviceAsset, DeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<DeviceAsset, int, QQueryOperations> isarIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isarId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
QueryBuilder<DeviceAsset, BackupSelection, QQueryOperations>
|
||||
backupSelectionProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'backupSelection');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, String, QQueryOperations> idProperty() {
|
||||
QueryBuilder<DeviceAsset, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'ios_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class IOSDeviceAsset extends DeviceAsset {
|
||||
IOSDeviceAsset({required this.id, required super.hash});
|
||||
|
||||
@Index(replace: true, unique: true, type: IndexType.hash)
|
||||
String id;
|
||||
Id get isarId => fastHash(id);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -92,9 +92,9 @@ class User {
|
||||
|
||||
bool get hasQuota => quotaSizeInBytes > 0;
|
||||
@Backlink(to: 'owner')
|
||||
final IsarLinks<Album> albums = IsarLinks<Album>();
|
||||
final IsarLinks<RemoteAlbum> albums = IsarLinks<RemoteAlbum>();
|
||||
@Backlink(to: 'sharedUsers')
|
||||
final IsarLinks<Album> sharedAlbums = IsarLinks<Album>();
|
||||
final IsarLinks<RemoteAlbum> sharedAlbums = IsarLinks<RemoteAlbum>();
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
|
||||
Generated
+9
-8
@@ -111,16 +111,16 @@ const UserSchema = CollectionSchema(
|
||||
},
|
||||
links: {
|
||||
r'albums': LinkSchema(
|
||||
id: -8764917375410137318,
|
||||
id: 745273729987915243,
|
||||
name: r'albums',
|
||||
target: r'Album',
|
||||
target: r'RemoteAlbum',
|
||||
single: false,
|
||||
linkName: r'owner',
|
||||
),
|
||||
r'sharedAlbums': LinkSchema(
|
||||
id: -7037628715076287024,
|
||||
id: 8917117772714542143,
|
||||
name: r'sharedAlbums',
|
||||
target: r'Album',
|
||||
target: r'RemoteAlbum',
|
||||
single: false,
|
||||
linkName: r'sharedUsers',
|
||||
)
|
||||
@@ -268,9 +268,9 @@ List<IsarLinkBase<dynamic>> _userGetLinks(User object) {
|
||||
}
|
||||
|
||||
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {
|
||||
object.albums.attach(col, col.isar.collection<Album>(), r'albums', id);
|
||||
object.albums.attach(col, col.isar.collection<RemoteAlbum>(), r'albums', id);
|
||||
object.sharedAlbums
|
||||
.attach(col, col.isar.collection<Album>(), r'sharedAlbums', id);
|
||||
.attach(col, col.isar.collection<RemoteAlbum>(), r'sharedAlbums', id);
|
||||
}
|
||||
|
||||
extension UserByIndex on IsarCollection<User> {
|
||||
@@ -1286,7 +1286,8 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
extension UserQueryObject on QueryBuilder<User, User, QFilterCondition> {}
|
||||
|
||||
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albums(FilterQuery<Album> q) {
|
||||
QueryBuilder<User, User, QAfterFilterCondition> albums(
|
||||
FilterQuery<RemoteAlbum> q) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.link(q, r'albums');
|
||||
});
|
||||
@@ -1342,7 +1343,7 @@ extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbums(
|
||||
FilterQuery<Album> q) {
|
||||
FilterQuery<RemoteAlbum> q) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.link(q, r'sharedAlbums');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
@@ -63,7 +64,8 @@ class AppStateNotiifer extends StateNotifier<AppStateEnum> {
|
||||
_ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
case TabEnum.library:
|
||||
_ref.read(albumProvider.notifier).getAllAlbums();
|
||||
_ref.read(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
_ref.read(localAlbumsProvider.notifier).getDeviceAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/local_album_service.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/local_album.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
@@ -16,10 +19,13 @@ import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'asset.provider.g.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
final AlbumService _albumService;
|
||||
final LocalAlbumService _albumService;
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final Isar _db;
|
||||
@@ -47,13 +53,14 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
state = true;
|
||||
if (clear) {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
await _userService.refreshUsers();
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
log.fine("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
state = false;
|
||||
@@ -75,7 +82,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
} else {
|
||||
await _assetService.refreshRemoteAssets(partner);
|
||||
}
|
||||
log.info("Load partner assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
log.fine("Load partner assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getPartnerAssetsInProgress = false;
|
||||
}
|
||||
@@ -317,7 +324,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(localAlbumServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
@@ -339,24 +346,57 @@ final assetWatcher =
|
||||
return db.assets.watchObject(asset.id, fireImmediately: true);
|
||||
});
|
||||
|
||||
final assetsProvider = StreamProvider.family<RenderList, int?>((ref, userId) {
|
||||
Stream<RenderList> _assetsProvider(
|
||||
AutoDisposeStreamProviderRef<RenderList> ref,
|
||||
int? userId,
|
||||
) {
|
||||
if (userId == null) return const Stream.empty();
|
||||
final query = _commonFilterAndSort(
|
||||
ref,
|
||||
_assets(ref).where().ownerIdEqualToAnyChecksum(userId),
|
||||
);
|
||||
return renderListGenerator(query, ref);
|
||||
});
|
||||
return renderListGeneratorAutoDispose(query, ref);
|
||||
}
|
||||
|
||||
final multiUserAssetsProvider =
|
||||
StreamProvider.family<RenderList, List<int>>((ref, userIds) {
|
||||
@riverpod
|
||||
Stream<RenderList> assets(AssetsRef ref, int? userId) {
|
||||
final listener = ref
|
||||
.read(dbProvider)
|
||||
.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.watchLazy()
|
||||
.listen((_) => ref.invalidateSelf());
|
||||
ref.onDispose(listener.cancel);
|
||||
return _assetsProvider(ref, userId);
|
||||
}
|
||||
|
||||
Stream<RenderList> _multiUserAssets(
|
||||
AutoDisposeStreamProviderRef<RenderList> ref,
|
||||
List<int> userIds,
|
||||
) {
|
||||
if (userIds.isEmpty) return const Stream.empty();
|
||||
final query = _commonFilterAndSort(
|
||||
ref,
|
||||
_assets(ref)
|
||||
.where()
|
||||
.anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)),
|
||||
);
|
||||
return renderListGenerator(query, ref);
|
||||
});
|
||||
return renderListGeneratorAutoDispose(query, ref);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<RenderList> multiUserAssets(MultiUserAssetsRef ref, List<int> userIds) {
|
||||
final listener = ref
|
||||
.read(dbProvider)
|
||||
.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.watchLazy()
|
||||
.listen((_) => ref.invalidateSelf());
|
||||
ref.onDispose(listener.cancel);
|
||||
return _multiUserAssets(ref, userIds);
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
|
||||
final userId = ref.watch(currentUserProvider)?.isarId;
|
||||
@@ -375,16 +415,31 @@ QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
|
||||
.sortByFileCreatedAtDesc();
|
||||
}
|
||||
|
||||
IsarCollection<Asset> _assets(StreamProviderRef<RenderList> ref) =>
|
||||
IsarCollection<Asset> _assets(AutoDisposeStreamProviderRef<RenderList> ref) =>
|
||||
ref.watch(dbProvider).assets;
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> _commonFilterAndSort(
|
||||
AutoDisposeStreamProviderRef<RenderList> ref,
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> query,
|
||||
) {
|
||||
final localIds = ref
|
||||
.read(dbProvider)
|
||||
.deviceAssets
|
||||
.filter()
|
||||
.backupSelectionEqualTo(BackupSelection.select)
|
||||
.idProperty()
|
||||
.findAllSync();
|
||||
|
||||
return query
|
||||
.filter()
|
||||
.isArchivedEqualTo(false)
|
||||
.isTrashedEqualTo(false)
|
||||
.stackParentIdIsNull()
|
||||
.group(
|
||||
(q) => q
|
||||
.remoteIdIsNotNull()
|
||||
.or()
|
||||
.anyOf(localIds, (q, id) => q.localIdEqualTo(id)),
|
||||
)
|
||||
.sortByFileCreatedAtDesc();
|
||||
}
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$assetsHash() => r'89d151eead499747703a2361efd178ba89e87be8';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [assets].
|
||||
@ProviderFor(assets)
|
||||
const assetsProvider = AssetsFamily();
|
||||
|
||||
/// See also [assets].
|
||||
class AssetsFamily extends Family<AsyncValue<RenderList>> {
|
||||
/// See also [assets].
|
||||
const AssetsFamily();
|
||||
|
||||
/// See also [assets].
|
||||
AssetsProvider call(
|
||||
int? userId,
|
||||
) {
|
||||
return AssetsProvider(
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AssetsProvider getProviderOverride(
|
||||
covariant AssetsProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.userId,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'assetsProvider';
|
||||
}
|
||||
|
||||
/// See also [assets].
|
||||
class AssetsProvider extends AutoDisposeStreamProvider<RenderList> {
|
||||
/// See also [assets].
|
||||
AssetsProvider(
|
||||
int? userId,
|
||||
) : this._internal(
|
||||
(ref) => assets(
|
||||
ref as AssetsRef,
|
||||
userId,
|
||||
),
|
||||
from: assetsProvider,
|
||||
name: r'assetsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$assetsHash,
|
||||
dependencies: AssetsFamily._dependencies,
|
||||
allTransitiveDependencies: AssetsFamily._allTransitiveDependencies,
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
AssetsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.userId,
|
||||
}) : super.internal();
|
||||
|
||||
final int? userId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
Stream<RenderList> Function(AssetsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: AssetsProvider._internal(
|
||||
(ref) => create(ref as AssetsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
userId: userId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamProviderElement<RenderList> createElement() {
|
||||
return _AssetsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AssetsProvider && other.userId == userId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, userId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin AssetsRef on AutoDisposeStreamProviderRef<RenderList> {
|
||||
/// The parameter `userId` of this provider.
|
||||
int? get userId;
|
||||
}
|
||||
|
||||
class _AssetsProviderElement
|
||||
extends AutoDisposeStreamProviderElement<RenderList> with AssetsRef {
|
||||
_AssetsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
int? get userId => (origin as AssetsProvider).userId;
|
||||
}
|
||||
|
||||
String _$multiUserAssetsHash() => r'63a9506e21d34e66af2af93946930016d51b0b43';
|
||||
|
||||
/// See also [multiUserAssets].
|
||||
@ProviderFor(multiUserAssets)
|
||||
const multiUserAssetsProvider = MultiUserAssetsFamily();
|
||||
|
||||
/// See also [multiUserAssets].
|
||||
class MultiUserAssetsFamily extends Family<AsyncValue<RenderList>> {
|
||||
/// See also [multiUserAssets].
|
||||
const MultiUserAssetsFamily();
|
||||
|
||||
/// See also [multiUserAssets].
|
||||
MultiUserAssetsProvider call(
|
||||
List<int> userIds,
|
||||
) {
|
||||
return MultiUserAssetsProvider(
|
||||
userIds,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MultiUserAssetsProvider getProviderOverride(
|
||||
covariant MultiUserAssetsProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.userIds,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'multiUserAssetsProvider';
|
||||
}
|
||||
|
||||
/// See also [multiUserAssets].
|
||||
class MultiUserAssetsProvider extends AutoDisposeStreamProvider<RenderList> {
|
||||
/// See also [multiUserAssets].
|
||||
MultiUserAssetsProvider(
|
||||
List<int> userIds,
|
||||
) : this._internal(
|
||||
(ref) => multiUserAssets(
|
||||
ref as MultiUserAssetsRef,
|
||||
userIds,
|
||||
),
|
||||
from: multiUserAssetsProvider,
|
||||
name: r'multiUserAssetsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$multiUserAssetsHash,
|
||||
dependencies: MultiUserAssetsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
MultiUserAssetsFamily._allTransitiveDependencies,
|
||||
userIds: userIds,
|
||||
);
|
||||
|
||||
MultiUserAssetsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.userIds,
|
||||
}) : super.internal();
|
||||
|
||||
final List<int> userIds;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
Stream<RenderList> Function(MultiUserAssetsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: MultiUserAssetsProvider._internal(
|
||||
(ref) => create(ref as MultiUserAssetsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
userIds: userIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamProviderElement<RenderList> createElement() {
|
||||
return _MultiUserAssetsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MultiUserAssetsProvider && other.userIds == userIds;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, userIds.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin MultiUserAssetsRef on AutoDisposeStreamProviderRef<RenderList> {
|
||||
/// The parameter `userIds` of this provider.
|
||||
List<int> get userIds;
|
||||
}
|
||||
|
||||
class _MultiUserAssetsProviderElement
|
||||
extends AutoDisposeStreamProviderElement<RenderList>
|
||||
with MultiUserAssetsRef {
|
||||
_MultiUserAssetsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
List<int> get userIds => (origin as MultiUserAssetsProvider).userIds;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -52,6 +52,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
await getServerVersion();
|
||||
await getServerFeatures();
|
||||
await getServerConfig();
|
||||
await getServerDiskInfo();
|
||||
}
|
||||
|
||||
getServerVersion() async {
|
||||
@@ -152,6 +153,14 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
state = state.copyWith(serverConfig: serverConfig);
|
||||
}
|
||||
|
||||
getServerDiskInfo() async {
|
||||
final serverInfo = await _serverInfoService.getServerInfo();
|
||||
if (serverInfo == null) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(serverDiskInfo: serverInfo);
|
||||
}
|
||||
|
||||
Map<String, int> _getDetailVersion(String version) {
|
||||
List<String> detail = version.split(".");
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/device_assets.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/server_info/server_version.model.dart';
|
||||
@@ -185,16 +186,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
}
|
||||
}
|
||||
|
||||
void stopListenToEvent(String eventName) {
|
||||
debugPrint("Stop listening to event $eventName");
|
||||
state.socket?.off(eventName);
|
||||
}
|
||||
|
||||
void listenUploadEvent() {
|
||||
debugPrint("Start listening to event on_upload_success");
|
||||
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||
}
|
||||
|
||||
void addPendingChange(PendingAction action, dynamic value) {
|
||||
final now = DateTime.now();
|
||||
state = state.copyWith(
|
||||
@@ -241,6 +232,9 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
.whereNot((c) => uploadedChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
// Reload backup state
|
||||
_ref.invalidate(deviceAssetsProvider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
@@ -41,10 +36,8 @@ class HashService {
|
||||
const int batchFileCount = 128;
|
||||
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
final ids = assetEntities
|
||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||
final ids = assetEntities.map((a) => a.id).toList();
|
||||
final List<DeviceAsset?> hashes = await _db.deviceAssets.getAllById(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
final List<String> toHash = [];
|
||||
|
||||
@@ -63,9 +56,7 @@ class HashService {
|
||||
}
|
||||
bytes += await file.length();
|
||||
toHash.add(file.path);
|
||||
final deviceAsset = Platform.isAndroid
|
||||
? AndroidDeviceAsset(id: ids[i] as int, hash: const [])
|
||||
: IOSDeviceAsset(id: ids[i] as String, hash: const []);
|
||||
final deviceAsset = DeviceAsset(id: ids[i], hash: const []);
|
||||
toAdd.add(deviceAsset);
|
||||
hashes[i] = deviceAsset;
|
||||
if (toHash.length == batchFileCount || bytes >= batchDataSize) {
|
||||
@@ -81,12 +72,6 @@ class HashService {
|
||||
return _mapAllHashedAssets(assetEntities, hashes);
|
||||
}
|
||||
|
||||
/// Lookup hashes of assets by their local ID
|
||||
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
/// Processes a batch of files and saves any successfully hashed
|
||||
/// values to the DB table.
|
||||
Future<void> _processBatch(
|
||||
@@ -106,11 +91,7 @@ class HashService {
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
await _db.writeTxn(
|
||||
() => Platform.isAndroid
|
||||
? _db.androidDeviceAssets.putAll(validHashes.cast())
|
||||
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
|
||||
);
|
||||
await _db.writeTxn(() => _db.deviceAssets.putAll(validHashes));
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,40 +2,38 @@ import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/extensions/album_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
|
||||
(ref) => SyncService(ref.watch(dbProvider)),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
static final AsyncMutex lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(this._db, this._hashService);
|
||||
SyncService(this._db);
|
||||
|
||||
// public methods:
|
||||
|
||||
/// Syncs users from the server to the local database
|
||||
/// Returns `true`if there were any changes
|
||||
Future<bool> syncUsersFromServer(List<User> users) =>
|
||||
_lock.run(() => _syncUsersFromServer(users));
|
||||
lock.run(() => _syncUsersFromServer(users));
|
||||
|
||||
/// Syncs remote assets owned by the logged-in user to the DB
|
||||
/// Returns `true` if there were any changes
|
||||
@@ -47,7 +45,7 @@ class SyncService {
|
||||
) getChangedAssets,
|
||||
FutureOr<List<Asset>?> Function(User user) loadAssets,
|
||||
) =>
|
||||
_lock.run(
|
||||
lock.run(
|
||||
() async =>
|
||||
await _syncRemoteAssetChanges(user, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(user, loadAssets),
|
||||
@@ -60,15 +58,7 @@ class SyncService {
|
||||
required bool isShared,
|
||||
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
}) =>
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncLocalAlbumAssetsToDb(
|
||||
List<AssetPathEntity> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) =>
|
||||
_lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
|
||||
lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
||||
|
||||
/// returns all Asset IDs that are not contained in the existing list
|
||||
List<int> sharedAssetsToRemove(
|
||||
@@ -80,7 +70,7 @@ class SyncService {
|
||||
}
|
||||
deleteCandidates.sort(Asset.compareById);
|
||||
existing.sort(Asset.compareById);
|
||||
return _diffAssets(existing, deleteCandidates, compare: Asset.compareById)
|
||||
return diffAssets(existing, deleteCandidates, compare: Asset.compareById)
|
||||
.$3
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
@@ -88,10 +78,7 @@ class SyncService {
|
||||
|
||||
/// Syncs a new asset to the db. Returns `true` if successful
|
||||
Future<bool> syncNewAssetToDb(Asset newAsset) =>
|
||||
_lock.run(() => _syncNewAssetToDb(newAsset));
|
||||
|
||||
Future<bool> removeAllLocalAlbumsAndAssets() =>
|
||||
_lock.run(_removeAllLocalAlbumsAndAssets);
|
||||
lock.run(() => _syncNewAssetToDb(newAsset));
|
||||
|
||||
// private methods:
|
||||
|
||||
@@ -222,7 +209,7 @@ class SyncService {
|
||||
// filter our duplicates that might be introduced by the chunked retrieval
|
||||
remote.uniqueConsecutive(compare: Asset.compareByChecksum);
|
||||
|
||||
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
|
||||
final (toAdd, toUpdate, toRemove) = diffAssets(remote, inDb, remote: true);
|
||||
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
|
||||
await _updateUserAssetsETag(user, now);
|
||||
return false;
|
||||
@@ -250,16 +237,16 @@ class SyncService {
|
||||
) async {
|
||||
remote.sortBy((e) => e.id);
|
||||
|
||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||
final baseQuery = _db.remoteAlbums.where().filter();
|
||||
final QueryBuilder<RemoteAlbum, RemoteAlbum, QAfterFilterCondition> query;
|
||||
if (isShared) {
|
||||
query = baseQuery.sharedEqualTo(true);
|
||||
} else {
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId));
|
||||
}
|
||||
final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
|
||||
assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
|
||||
final List<RemoteAlbum> dbAlbums = await query.sortById().findAll();
|
||||
assert(dbAlbums.isSortedBy((e) => e.id), "dbAlbums not sorted!");
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
final List<Asset> existing = [];
|
||||
@@ -267,12 +254,12 @@ class SyncService {
|
||||
final bool changes = await diffSortedLists(
|
||||
remote,
|
||||
dbAlbums,
|
||||
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
|
||||
both: (AlbumResponseDto a, Album b) =>
|
||||
compare: (AlbumResponseDto a, RemoteAlbum b) => a.id.compareTo(b.id),
|
||||
both: (AlbumResponseDto a, RemoteAlbum b) =>
|
||||
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
|
||||
onlyFirst: (AlbumResponseDto a) =>
|
||||
_addAlbumFromServer(a, existing, loadDetails),
|
||||
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
|
||||
onlySecond: (RemoteAlbum a) => _removeAlbumFromDb(a, toDelete),
|
||||
);
|
||||
|
||||
if (isShared && toDelete.isNotEmpty) {
|
||||
@@ -294,7 +281,7 @@ class SyncService {
|
||||
/// accumulates
|
||||
Future<bool> _syncRemoteAlbum(
|
||||
AlbumResponseDto dto,
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
@@ -314,7 +301,7 @@ class SyncService {
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
final (toAdd, toUpdate, toUnlink) = diffAssets(
|
||||
assetsOnRemote,
|
||||
assetsInDb,
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
@@ -345,8 +332,8 @@ class SyncService {
|
||||
album.shared = dto.shared;
|
||||
album.modifiedAt = dto.updatedAt;
|
||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
|
||||
album.thumbnail.value = await _db.assets
|
||||
if (album.thumbnail?.remoteId != dto.albumThumbnailAssetId) {
|
||||
album.thumb.value = await _db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.findFirst();
|
||||
@@ -356,11 +343,11 @@ class SyncService {
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.thumbnail.save();
|
||||
await album.thumb.save();
|
||||
await album.sharedUsers
|
||||
.update(link: usersToLink, unlink: usersToUnlink);
|
||||
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
|
||||
await _db.albums.put(album);
|
||||
await _db.remoteAlbums.put(album);
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
@@ -399,8 +386,8 @@ class SyncService {
|
||||
existing.addAll(existingInDb);
|
||||
await upsertAssetsWithExif(updated);
|
||||
|
||||
final Album a = await Album.remote(dto);
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
final RemoteAlbum a = await RemoteAlbum.fromDto(dto, _db);
|
||||
await _db.writeTxn(() => _db.remoteAlbums.store(a));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
||||
@@ -411,16 +398,10 @@ class SyncService {
|
||||
/// Accumulates all suitable album assets to the `deleteCandidates` and
|
||||
/// removes the album from the database.
|
||||
Future<void> _removeAlbumFromDb(
|
||||
Album album,
|
||||
RemoteAlbum album,
|
||||
List<Asset> deleteCandidates,
|
||||
) async {
|
||||
if (album.isLocal) {
|
||||
_log.info("Removing local album $album from DB");
|
||||
// delete assets in DB unless they are remote or part of some other album
|
||||
deleteCandidates.addAll(
|
||||
await album.assets.filter().remoteIdIsNull().findAll(),
|
||||
);
|
||||
} else if (album.shared) {
|
||||
if (album.shared) {
|
||||
final User user = Store.get(StoreKey.currentUser);
|
||||
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
|
||||
final userIds = await _db.users
|
||||
@@ -437,7 +418,8 @@ class SyncService {
|
||||
deleteCandidates.addAll(orphanedAssets);
|
||||
}
|
||||
try {
|
||||
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
final bool ok =
|
||||
await _db.writeTxn(() => _db.remoteAlbums.delete(album.isarId));
|
||||
assert(ok);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e) {
|
||||
@@ -445,221 +427,6 @@ class SyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> _syncLocalAlbumAssetsToDb(
|
||||
List<AssetPathEntity> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final inDb =
|
||||
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
|
||||
final bool anyChanges = await diffSortedLists(
|
||||
onDevice,
|
||||
inDb,
|
||||
compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!),
|
||||
both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice(
|
||||
ape,
|
||||
album,
|
||||
deleteCandidates,
|
||||
existing,
|
||||
excludedAssets,
|
||||
),
|
||||
onlyFirst: (AssetPathEntity ape) =>
|
||||
_addAlbumFromDevice(ape, existing, excludedAssets),
|
||||
onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
|
||||
);
|
||||
_log.fine(
|
||||
"Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
|
||||
);
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(deleteCandidates, existing, remote: false);
|
||||
_log.fine(
|
||||
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
|
||||
);
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.exifInfos.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
|
||||
);
|
||||
}
|
||||
return anyChanges;
|
||||
}
|
||||
|
||||
/// Syncs the device album to the album in the database
|
||||
/// returns `true` if there were any changes
|
||||
/// Accumulates asset candidates to delete and those already existing in DB
|
||||
Future<bool> _syncAlbumInDbAndOnDevice(
|
||||
AssetPathEntity ape,
|
||||
Album album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
bool forceRefresh = false,
|
||||
]) async {
|
||||
if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
|
||||
_log.fine("Local album ${ape.name} has not changed. Skipping sync.");
|
||||
return false;
|
||||
}
|
||||
if (!forceRefresh &&
|
||||
excludedAssets == null &&
|
||||
await _syncDeviceAlbumFast(ape, album)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
final inDb = await album.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
||||
final List<Asset> onDevice =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(onDevice);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||
if (toAdd.isEmpty &&
|
||||
toUpdate.isEmpty &&
|
||||
toDelete.isEmpty &&
|
||||
album.name == ape.name &&
|
||||
ape.lastModified != null &&
|
||||
album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) {
|
||||
// changes only affeted excluded albums
|
||||
_log.fine(
|
||||
"Only excluded assets in local album ${ape.name} changed. Stopping sync.",
|
||||
);
|
||||
if (assetCountOnDevice !=
|
||||
_db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
|
||||
await _db.writeTxn(
|
||||
() => _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_log.fine(
|
||||
"Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
|
||||
);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||
_log.fine(
|
||||
"Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update",
|
||||
);
|
||||
deleteCandidates.addAll(toDelete);
|
||||
existing.addAll(existingInDb);
|
||||
album.name = ape.name;
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
if (album.thumbnail.value != null &&
|
||||
toDelete.contains(album.thumbnail.value)) {
|
||||
album.thumbnail.value = null;
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.assets
|
||||
.update(link: existingInDb + updated, unlink: toDelete);
|
||||
await _db.albums.put(album);
|
||||
album.thumbnail.value ??= await album.assets.filter().findFirst();
|
||||
await album.thumbnail.save();
|
||||
await _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
|
||||
);
|
||||
});
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB", e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// fast path for common case: only new assets were added to device album
|
||||
/// returns `true` if successfull, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
|
||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await ape.assetCountAsync;
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
|
||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
||||
? await ape.fetchPathProperties(
|
||||
filterOptionGroup: FilterOptionGroup(
|
||||
updateTimeCond: DateTimeCond(
|
||||
min: album.modifiedAt.add(const Duration(seconds: 1)),
|
||||
max: ape.lastModified ?? DateTime.now(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
if (modified == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
||||
|
||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||
return false;
|
||||
}
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await album.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(album);
|
||||
await _db.eTags
|
||||
.put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
|
||||
});
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to fast sync local album ${ape.name} to DB", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Adds a new album from the device to the database and Accumulates all
|
||||
/// assets already existing in the database to the list of `existing` assets
|
||||
Future<void> _addAlbumFromDevice(
|
||||
AssetPathEntity ape,
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||
final Album a = Album.local(ape);
|
||||
final assets =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
|
||||
);
|
||||
await upsertAssetsWithExif(updated);
|
||||
existing.addAll(existingInDb);
|
||||
a.assets.addAll(existingInDb);
|
||||
a.assets.addAll(updated);
|
||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||
a.thumbnail.value = thumb;
|
||||
try {
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
_log.info("Added a new local album to DB: ${ape.name}");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple (existing, updated)
|
||||
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
|
||||
List<Asset> assets,
|
||||
@@ -738,137 +505,73 @@ class SyncService {
|
||||
}
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
assets.uniqueConsecutive(
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
onDuplicate: (a, b) =>
|
||||
_log.info("Ignoring duplicate assets on device:\n$a\n$b"),
|
||||
);
|
||||
final int duplicates = before - assets.length;
|
||||
if (duplicates > 0) {
|
||||
_log.warning("Ignored $duplicates duplicate assets on device");
|
||||
/// Returns a triple(toAdd, toUpdate, toRemove)
|
||||
(List<Asset> toAdd, List<Asset> toUpdate, List<Asset> toRemove) diffAssets(
|
||||
List<Asset> assets,
|
||||
List<Asset> inDb, {
|
||||
bool? remote,
|
||||
int Function(Asset, Asset) compare = Asset.compareByChecksum,
|
||||
}) {
|
||||
// fast paths for trivial cases: reduces memory usage during initial sync etc.
|
||||
if (assets.isEmpty && inDb.isEmpty) {
|
||||
return const ([], [], []);
|
||||
} else if (assets.isEmpty && remote == null) {
|
||||
// remove all from database
|
||||
return (const [], const [], inDb);
|
||||
} else if (inDb.isEmpty) {
|
||||
// add all assets
|
||||
return (assets, const [], const []);
|
||||
}
|
||||
return assets;
|
||||
|
||||
final List<Asset> toAdd = [];
|
||||
final List<Asset> toUpdate = [];
|
||||
final List<Asset> toRemove = [];
|
||||
diffSortedListsSync(
|
||||
inDb,
|
||||
assets,
|
||||
compare: compare,
|
||||
both: (Asset a, Asset b) {
|
||||
if (a.canUpdate(b)) {
|
||||
toUpdate.add(a.updatedCopy(b));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (Asset a) {
|
||||
if (remote == true && a.isLocal) {
|
||||
if (a.remoteId != null) {
|
||||
a.remoteId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else if (remote == false && a.isRemote) {
|
||||
if (a.isLocal) {
|
||||
a.localId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else {
|
||||
toRemove.add(a);
|
||||
}
|
||||
},
|
||||
onlySecond: (Asset b) => toAdd.add(b),
|
||||
);
|
||||
return (toAdd, toUpdate, toRemove);
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync !=
|
||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
|
||||
}
|
||||
|
||||
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
||||
try {
|
||||
final assets = await _db.assets.where().localIdIsNotNull().findAll();
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(assets, [], remote: false);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await _db.albums.where().localIdIsNotNull().deleteAll();
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove all local albums and assets", e);
|
||||
return false;
|
||||
}
|
||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, RemoteAlbum a) {
|
||||
return dto.assetCount != a.assetCount ||
|
||||
dto.albumName != a.name ||
|
||||
dto.albumThumbnailAssetId != a.thumbnail?.remoteId ||
|
||||
dto.shared != a.shared ||
|
||||
dto.sharedUsers.length != a.sharedUsers.length ||
|
||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
||||
(dto.lastModifiedAssetTimestamp == null &&
|
||||
a.lastModifiedAssetTimestamp != null) ||
|
||||
(dto.lastModifiedAssetTimestamp != null &&
|
||||
a.lastModifiedAssetTimestamp == null) ||
|
||||
(dto.lastModifiedAssetTimestamp != null &&
|
||||
a.lastModifiedAssetTimestamp != null &&
|
||||
!dto.lastModifiedAssetTimestamp!
|
||||
.isAtSameMomentAs(a.lastModifiedAssetTimestamp!));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a triple(toAdd, toUpdate, toRemove)
|
||||
(List<Asset> toAdd, List<Asset> toUpdate, List<Asset> toRemove) _diffAssets(
|
||||
List<Asset> assets,
|
||||
List<Asset> inDb, {
|
||||
bool? remote,
|
||||
int Function(Asset, Asset) compare = Asset.compareByChecksum,
|
||||
}) {
|
||||
// fast paths for trivial cases: reduces memory usage during initial sync etc.
|
||||
if (assets.isEmpty && inDb.isEmpty) {
|
||||
return const ([], [], []);
|
||||
} else if (assets.isEmpty && remote == null) {
|
||||
// remove all from database
|
||||
return (const [], const [], inDb);
|
||||
} else if (inDb.isEmpty) {
|
||||
// add all assets
|
||||
return (assets, const [], const []);
|
||||
}
|
||||
|
||||
final List<Asset> toAdd = [];
|
||||
final List<Asset> toUpdate = [];
|
||||
final List<Asset> toRemove = [];
|
||||
diffSortedListsSync(
|
||||
inDb,
|
||||
assets,
|
||||
compare: compare,
|
||||
both: (Asset a, Asset b) {
|
||||
if (a.canUpdate(b)) {
|
||||
toUpdate.add(a.updatedCopy(b));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (Asset a) {
|
||||
if (remote == true && a.isLocal) {
|
||||
if (a.remoteId != null) {
|
||||
a.remoteId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else if (remote == false && a.isRemote) {
|
||||
if (a.isLocal) {
|
||||
a.localId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else {
|
||||
toRemove.add(a);
|
||||
}
|
||||
},
|
||||
onlySecond: (Asset b) => toAdd.add(b),
|
||||
);
|
||||
return (toAdd, toUpdate, toRemove);
|
||||
}
|
||||
|
||||
/// returns a tuple (toDelete toUpdate) when assets are to be deleted
|
||||
(List<int> toDelete, List<Asset> toUpdate) _handleAssetRemoval(
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing, {
|
||||
bool? remote,
|
||||
}) {
|
||||
if (deleteCandidates.isEmpty) {
|
||||
return const ([], []);
|
||||
}
|
||||
deleteCandidates.sort(Asset.compareById);
|
||||
deleteCandidates.uniqueConsecutive(compare: Asset.compareById);
|
||||
existing.sort(Asset.compareById);
|
||||
existing.uniqueConsecutive(compare: Asset.compareById);
|
||||
final (tooAdd, toUpdate, toRemove) = _diffAssets(
|
||||
existing,
|
||||
deleteCandidates,
|
||||
compare: Asset.compareById,
|
||||
remote: remote,
|
||||
);
|
||||
assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval");
|
||||
return (toRemove.map((e) => e.id).toList(), toUpdate);
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
||||
return dto.assetCount != a.assetCount ||
|
||||
dto.albumName != a.name ||
|
||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
||||
dto.shared != a.shared ||
|
||||
dto.sharedUsers.length != a.sharedUsers.length ||
|
||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
||||
(dto.lastModifiedAssetTimestamp == null &&
|
||||
a.lastModifiedAssetTimestamp != null) ||
|
||||
(dto.lastModifiedAssetTimestamp != null &&
|
||||
a.lastModifiedAssetTimestamp == null) ||
|
||||
(dto.lastModifiedAssetTimestamp != null &&
|
||||
a.lastModifiedAssetTimestamp != null &&
|
||||
!dto.lastModifiedAssetTimestamp!
|
||||
.isAtSameMomentAs(a.lastModifiedAssetTimestamp!));
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_profile_info.dart';
|
||||
@@ -23,7 +23,8 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final serverInfo =
|
||||
ref.watch(serverInfoProvider.select((value) => value.serverDiskInfo));
|
||||
final theme = context.themeData;
|
||||
bool isHorizontal = !context.isMobile;
|
||||
final horizontalPadding = isHorizontal ? 100.0 : 20.0;
|
||||
@@ -31,7 +32,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(backupProvider.notifier).updateServerInfo();
|
||||
ref.read(serverInfoProvider.notifier).getServerDiskInfo();
|
||||
ref.read(currentUserProvider.notifier).refresh();
|
||||
return null;
|
||||
},
|
||||
@@ -134,9 +135,9 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Widget buildStorageInformation() {
|
||||
var percentage = backupState.serverInfo.diskUsagePercentage / 100;
|
||||
var usedDiskSpace = backupState.serverInfo.diskUse;
|
||||
var totalDiskSpace = backupState.serverInfo.diskSize;
|
||||
var percentage = serverInfo.diskUsagePercentage / 100;
|
||||
var usedDiskSpace = serverInfo.diskUse;
|
||||
var totalDiskSpace = serverInfo.diskSize;
|
||||
|
||||
if (user != null && user.hasQuota) {
|
||||
usedDiskSpace = formatBytes(user.quotaUsageInBytes);
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
|
||||
@@ -19,7 +20,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
@@ -43,6 +43,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
this.editEnabled = false,
|
||||
this.unarchive = false,
|
||||
this.unfavorite = false,
|
||||
this.selectedEnabled = true,
|
||||
});
|
||||
|
||||
final ProviderListenable<AsyncValue<RenderList>> renderListProvider;
|
||||
@@ -57,6 +58,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
final bool favoriteEnabled;
|
||||
final bool unfavorite;
|
||||
final bool editEnabled;
|
||||
final bool selectedEnabled;
|
||||
|
||||
Widget buildDefaultLoadingIndicator() =>
|
||||
const Center(child: ImmichLoadingIndicator());
|
||||
@@ -94,7 +96,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
bool multiselect,
|
||||
Set<Asset> selectedAssets,
|
||||
) {
|
||||
selectionEnabledHook.value = multiselect;
|
||||
selectionEnabledHook.value = selectedEnabled && multiselect;
|
||||
selection.value = selectedAssets;
|
||||
selectionAssetState.value =
|
||||
SelectionAssetState.fromSelection(selectedAssets);
|
||||
@@ -277,7 +279,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void onAddToAlbum(Album album) async {
|
||||
void onAddToAlbum(RemoteAlbum album) async {
|
||||
processing.value = true;
|
||||
try {
|
||||
final Iterable<Asset> assets = remoteSelection(
|
||||
@@ -337,11 +339,11 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
.createAlbumWithGeneratedName(assets);
|
||||
|
||||
if (result != null) {
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(remoteAlbumsProvider.notifier).getRemoteAlbums();
|
||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
selectionEnabledHook.value = false;
|
||||
|
||||
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
||||
context.pushRoute(RemoteAlbumViewerRoute(albumId: result.isarId));
|
||||
}
|
||||
} finally {
|
||||
processing.value = false;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup_settings.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
@@ -21,9 +22,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final BackUpState backupState = ref.watch(backupProvider);
|
||||
final backupState = ref.watch(backupProvider);
|
||||
final backupSettings = ref.watch(backupSettingsProvider);
|
||||
final bool isEnableAutoBackup =
|
||||
backupState.backgroundBackup || backupState.autoBackup;
|
||||
backupSettings.backgroundBackup || backupSettings.autoBackup;
|
||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
@@ -11,7 +11,8 @@ Future<void> clearAssetsAndAlbums(Isar db) async {
|
||||
await db.writeTxn(() async {
|
||||
await db.assets.clear();
|
||||
await db.exifInfos.clear();
|
||||
await db.albums.clear();
|
||||
await db.localAlbums.clear();
|
||||
await db.remoteAlbums.clear();
|
||||
await db.eTags.clear();
|
||||
await db.users.clear();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/modules/album/models/album.model.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
@@ -33,11 +33,11 @@ String getAlbumThumbnailUrl(
|
||||
final Album album, {
|
||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||
}) {
|
||||
if (album.thumbnail.value?.remoteId == null) {
|
||||
if (album.thumbnail?.remoteId == null) {
|
||||
return '';
|
||||
}
|
||||
return getThumbnailUrlForRemoteId(
|
||||
album.thumbnail.value!.remoteId!,
|
||||
album.thumbnail!.remoteId!,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
@@ -46,11 +46,11 @@ String getAlbumThumbNailCacheKey(
|
||||
final Album album, {
|
||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||
}) {
|
||||
if (album.thumbnail.value?.remoteId == null) {
|
||||
if (album.thumbnail?.remoteId == null) {
|
||||
return '';
|
||||
}
|
||||
return getThumbnailCacheKeyForRemoteId(
|
||||
album.thumbnail.value!.remoteId!,
|
||||
album.thumbnail!.remoteId!,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user