mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: album info sync (#21103)
* wip * album creation * fix: album api repository no invalidating after logging out * add linkedRemoteAlbumId column and migration * link/unlink remote album * logic to find and add new assets to album * pr feedback * add toggle option to backup option page * refactor: provider > service * rename * Handle page pop manually * UI feedback for user creation and sync linked album * uncomment migration * remove unused method
This commit is contained in:
parent
538263dc38
commit
bcfb5bee1f
26 changed files with 601 additions and 199 deletions
|
|
@ -1417,6 +1417,8 @@
|
||||||
"open_the_search_filters": "Open the search filters",
|
"open_the_search_filters": "Open the search filters",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
|
"organize_into_albums": "Organize into albums",
|
||||||
|
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
||||||
"organize_your_library": "Organize your library",
|
"organize_your_library": "Organize your library",
|
||||||
"original": "original",
|
"original": "original",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
|
|
|
||||||
BIN
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
BIN
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
Binary file not shown.
|
|
@ -15,6 +15,7 @@ class LocalAlbum {
|
||||||
|
|
||||||
final int assetCount;
|
final int assetCount;
|
||||||
final BackupSelection backupSelection;
|
final BackupSelection backupSelection;
|
||||||
|
final String? linkedRemoteAlbumId;
|
||||||
|
|
||||||
const LocalAlbum({
|
const LocalAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -23,6 +24,7 @@ class LocalAlbum {
|
||||||
this.assetCount = 0,
|
this.assetCount = 0,
|
||||||
this.backupSelection = BackupSelection.none,
|
this.backupSelection = BackupSelection.none,
|
||||||
this.isIosSharedAlbum = false,
|
this.isIosSharedAlbum = false,
|
||||||
|
this.linkedRemoteAlbumId,
|
||||||
});
|
});
|
||||||
|
|
||||||
LocalAlbum copyWith({
|
LocalAlbum copyWith({
|
||||||
|
|
@ -32,6 +34,7 @@ class LocalAlbum {
|
||||||
int? assetCount,
|
int? assetCount,
|
||||||
BackupSelection? backupSelection,
|
BackupSelection? backupSelection,
|
||||||
bool? isIosSharedAlbum,
|
bool? isIosSharedAlbum,
|
||||||
|
String? linkedRemoteAlbumId,
|
||||||
}) {
|
}) {
|
||||||
return LocalAlbum(
|
return LocalAlbum(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -40,6 +43,7 @@ class LocalAlbum {
|
||||||
assetCount: assetCount ?? this.assetCount,
|
assetCount: assetCount ?? this.assetCount,
|
||||||
backupSelection: backupSelection ?? this.backupSelection,
|
backupSelection: backupSelection ?? this.backupSelection,
|
||||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +57,8 @@ class LocalAlbum {
|
||||||
other.updatedAt == updatedAt &&
|
other.updatedAt == updatedAt &&
|
||||||
other.assetCount == assetCount &&
|
other.assetCount == assetCount &&
|
||||||
other.backupSelection == backupSelection &&
|
other.backupSelection == backupSelection &&
|
||||||
other.isIosSharedAlbum == isIosSharedAlbum;
|
other.isIosSharedAlbum == isIosSharedAlbum &&
|
||||||
|
other.linkedRemoteAlbumId == linkedRemoteAlbumId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -63,7 +68,8 @@ class LocalAlbum {
|
||||||
updatedAt.hashCode ^
|
updatedAt.hashCode ^
|
||||||
assetCount.hashCode ^
|
assetCount.hashCode ^
|
||||||
backupSelection.hashCode ^
|
backupSelection.hashCode ^
|
||||||
isIosSharedAlbum.hashCode;
|
isIosSharedAlbum.hashCode ^
|
||||||
|
linkedRemoteAlbumId.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -75,6 +81,7 @@ updatedAt: $updatedAt,
|
||||||
assetCount: $assetCount,
|
assetCount: $assetCount,
|
||||||
backupSelection: $backupSelection,
|
backupSelection: $backupSelection,
|
||||||
isIosSharedAlbum: $isIosSharedAlbum
|
isIosSharedAlbum: $isIosSharedAlbum
|
||||||
|
linkedRemoteAlbumId: $linkedRemoteAlbumId,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,16 @@ class LocalAlbumService {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> unlinkRemoteAlbum(String id) async {
|
||||||
|
return _repository.unlinkRemoteAlbum(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||||
|
return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums() {
|
||||||
|
return _repository.getBackupAlbums();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ class RemoteAlbumService {
|
||||||
return _repository.get(albumId);
|
return _repository.get(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||||
|
return _repository.getByName(albumName, ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> sortAlbums(
|
Future<List<RemoteAlbum>> sortAlbums(
|
||||||
List<RemoteAlbum> albums,
|
List<RemoteAlbum> albums,
|
||||||
RemoteAlbumSortMode sortMode, {
|
RemoteAlbumSortMode sortMode, {
|
||||||
|
|
@ -80,7 +84,6 @@ class RemoteAlbumService {
|
||||||
|
|
||||||
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
||||||
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
||||||
|
|
||||||
await _repository.create(album, assetIds);
|
await _repository.create(album, assetIds);
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
|
|
|
||||||
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
|
||||||
|
final syncLinkedAlbumServiceProvider = Provider(
|
||||||
|
(ref) => SyncLinkedAlbumService(
|
||||||
|
ref.watch(localAlbumRepository),
|
||||||
|
ref.watch(remoteAlbumRepository),
|
||||||
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class SyncLinkedAlbumService {
|
||||||
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||||
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
|
||||||
|
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbums(String userId) async {
|
||||||
|
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||||
|
|
||||||
|
await Future.wait(
|
||||||
|
selectedAlbums.map((localAlbum) async {
|
||||||
|
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
|
||||||
|
if (linkedRemoteAlbumId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
|
||||||
|
if (remoteAlbum == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get assets that are uploaded but not in the remote album
|
||||||
|
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
||||||
|
|
||||||
|
if (assetIds.isNotEmpty) {
|
||||||
|
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
||||||
|
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||||
|
for (final album in localAlbums) {
|
||||||
|
await _processLocalAlbum(album, ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes a single local album to ensure proper linking with remote albums
|
||||||
|
Future<void> _processLocalAlbum(LocalAlbum localAlbum, String ownerId) {
|
||||||
|
final hasLinkedRemoteAlbum = localAlbum.linkedRemoteAlbumId != null;
|
||||||
|
|
||||||
|
if (hasLinkedRemoteAlbum) {
|
||||||
|
return _handleLinkedAlbum(localAlbum);
|
||||||
|
} else {
|
||||||
|
return _handleUnlinkedAlbum(localAlbum, ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles albums that are already linked to a remote album
|
||||||
|
Future<void> _handleLinkedAlbum(LocalAlbum localAlbum) async {
|
||||||
|
final remoteAlbumId = localAlbum.linkedRemoteAlbumId!;
|
||||||
|
final remoteAlbum = await _remoteAlbumRepository.get(remoteAlbumId);
|
||||||
|
|
||||||
|
final remoteAlbumExists = remoteAlbum != null;
|
||||||
|
if (!remoteAlbumExists) {
|
||||||
|
return _localAlbumRepository.unlinkRemoteAlbum(localAlbum.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles albums that are not linked to any remote album
|
||||||
|
Future<void> _handleUnlinkedAlbum(LocalAlbum localAlbum, String ownerId) async {
|
||||||
|
final existingRemoteAlbum = await _remoteAlbumRepository.getByName(localAlbum.name, ownerId);
|
||||||
|
|
||||||
|
if (existingRemoteAlbum != null) {
|
||||||
|
return _linkToExistingRemoteAlbum(localAlbum, existingRemoteAlbum);
|
||||||
|
} else {
|
||||||
|
return _createAndLinkNewRemoteAlbum(localAlbum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Links a local album to an existing remote album
|
||||||
|
Future<void> _linkToExistingRemoteAlbum(LocalAlbum localAlbum, dynamic existingRemoteAlbum) {
|
||||||
|
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, existingRemoteAlbum.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new remote album and links it to the local album
|
||||||
|
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
|
||||||
|
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
|
||||||
|
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
|
||||||
|
await _remoteAlbumRepository.create(newRemoteAlbum, []);
|
||||||
|
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||||
import 'package:immich_mobile/utils/isolate.dart';
|
import 'package:immich_mobile/utils/isolate.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
@ -155,6 +156,11 @@ class BackgroundSyncManager {
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbum() {
|
||||||
|
final task = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
|
||||||
|
return task.future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||||
|
|
|
||||||
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
|
|
@ -11,9 +13,26 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
||||||
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
// // Linked album for putting assets to the remote album after finished uploading
|
||||||
|
TextColumn get linkedRemoteAlbumId =>
|
||||||
|
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.setNull).nullable()();
|
||||||
|
|
||||||
// Used for mark & sweep
|
// Used for mark & sweep
|
||||||
BoolColumn get marker_ => boolean().nullable()();
|
BoolColumn get marker_ => boolean().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
||||||
|
LocalAlbum toDto({int assetCount = 0}) {
|
||||||
|
return LocalAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
assetCount: assetCount,
|
||||||
|
backupSelection: backupSelection,
|
||||||
|
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -20,7 +20,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||||
LocalAsset toDto() => LocalAsset(
|
LocalAsset toDto() => LocalAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import "package:immich_mobile/utils/database.utils.dart";
|
|
||||||
|
|
||||||
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
||||||
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 8;
|
int get schemaVersion => 9;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
|
@ -123,6 +123,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
from7To8: (m, v8) async {
|
from7To8: (m, v8) async {
|
||||||
await m.create(v8.storeEntity);
|
await m.create(v8.storeEntity);
|
||||||
},
|
},
|
||||||
|
from8To9: (m, v9) async {
|
||||||
|
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/utils/database.utils.dart';
|
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||||
|
|
@ -49,6 +50,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums() async {
|
||||||
|
final query = _db.localAlbumEntity.select()
|
||||||
|
..where((row) => row.backupSelection.equalsValue(BackupSelection.selected));
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete(String albumId) => transaction(() async {
|
Future<void> delete(String albumId) => transaction(() async {
|
||||||
// Remove all assets that are only in this particular album
|
// Remove all assets that are only in this particular album
|
||||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||||
|
|
@ -335,4 +343,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.localAlbumEntity.count();
|
return _db.managers.localAlbumEntity.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future unlinkRemoteAlbum(String id) async {
|
||||||
|
return _db.localAlbumEntity.update()
|
||||||
|
..where((row) => row.id.equals(id))
|
||||||
|
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||||
|
return _db.localAlbumEntity.update()
|
||||||
|
..where((row) => row.id.equals(localAlbumId))
|
||||||
|
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||||
|
final query = _db.remoteAlbumEntity.select()
|
||||||
|
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(1);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
final entity = RemoteAlbumEntityCompanion(
|
final entity = RemoteAlbumEntityCompanion(
|
||||||
|
|
@ -321,6 +330,42 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.remoteAlbumEntity.count();
|
return _db.managers.remoteAlbumEntity.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
|
||||||
|
// Find remote asset ids that:
|
||||||
|
// 1. Belong to the provided local album (via local_album_asset_entity)
|
||||||
|
// 2. Have been uploaded (i.e. a matching remote asset exists for the same checksum & owner)
|
||||||
|
// 3. Are NOT already in the remote album (remote_album_asset_entity)
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.remoteAssetEntity.id])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
// Left join remote album assets to exclude those already in the remote album
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id) &
|
||||||
|
_db.remoteAlbumAssetEntity.albumId.equals(remoteAlbumId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
|
_db.localAlbumAssetEntity.albumId.equals(localAlbumId) &
|
||||||
|
_db.remoteAlbumAssetEntity.assetId.isNull(), // only those not yet linked
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
@ -26,10 +27,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
bool _isSearchMode = false;
|
bool _isSearchMode = false;
|
||||||
int _initialTotalAssetCount = 0;
|
int _initialTotalAssetCount = 0;
|
||||||
bool _hasPopped = false;
|
|
||||||
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||||
late TextEditingController _searchController;
|
late TextEditingController _searchController;
|
||||||
late FocusNode _searchFocusNode;
|
late FocusNode _searchFocusNode;
|
||||||
|
Future? _handleLinkedAlbumFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -44,6 +45,36 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePagePopped() async {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
final selectedAlbums = ref
|
||||||
|
.read(backupAlbumProvider)
|
||||||
|
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (enableSyncUploadAlbum && selectedAlbums.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_handleLinkedAlbumFuture = ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedAlbums, user.id);
|
||||||
|
});
|
||||||
|
await _handleLinkedAlbumFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart backup if total count changed and backup is enabled
|
||||||
|
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
|
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||||
|
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
if (totalChanged && isBackupEnabled) {
|
||||||
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
|
await ref.read(driftBackupProvider.notifier).startBackup(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_enableSyncUploadAlbum.dispose();
|
_enableSyncUploadAlbum.dispose();
|
||||||
|
|
@ -65,42 +96,12 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
||||||
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
||||||
|
|
||||||
// handleSyncAlbumToggle(bool isEnable) async {
|
|
||||||
// if (isEnable) {
|
|
||||||
// await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
|
||||||
// for (final album in selectedBackupAlbums) {
|
|
||||||
// await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
canPop: false,
|
||||||
// There is an issue with Flutter where the pop event
|
onPopInvokedWithResult: (didPop, _) async {
|
||||||
// can be triggered multiple times, so we guard it with _hasPopped
|
if (!didPop) {
|
||||||
if (didPop && !_hasPopped) {
|
await _handlePagePopped();
|
||||||
_hasPopped = true;
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final currentUser = ref.read(currentUserProvider);
|
|
||||||
if (currentUser == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
|
||||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
|
||||||
|
|
||||||
if (currentTotalAssetCount != _initialTotalAssetCount) {
|
|
||||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
|
||||||
|
|
||||||
if (!isBackupEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
|
||||||
|
|
||||||
backupNotifier.cancel().then((_) {
|
|
||||||
backupNotifier.startBackup(currentUser.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|
@ -139,103 +140,123 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
],
|
],
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: Stack(
|
||||||
physics: const ClampingScrollPhysics(),
|
children: [
|
||||||
slivers: [
|
CustomScrollView(
|
||||||
SliverToBoxAdapter(
|
physics: const ClampingScrollPhysics(),
|
||||||
child: Column(
|
slivers: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
SliverToBoxAdapter(
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
children: [
|
||||||
child: Text(
|
Padding(
|
||||||
"backup_album_selection_page_selection_info",
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
style: context.textTheme.titleSmall,
|
child: Text(
|
||||||
).t(context: context),
|
"backup_album_selection_page_selection_info",
|
||||||
),
|
style: context.textTheme.titleSmall,
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
|
||||||
// Selected Album Chips
|
// Selected Album Chips
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
||||||
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
// SettingsSwitchListTile(
|
title: Text(
|
||||||
// valueNotifier: _enableSyncUploadAlbum,
|
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||||
// title: "sync_albums".t(context: context),
|
style: context.textTheme.titleSmall,
|
||||||
// subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
),
|
||||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
subtitle: Padding(
|
||||||
// titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
// subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
child: Text(
|
||||||
// onChanged: handleSyncAlbumToggle,
|
"backup_album_selection_page_albums_tap",
|
||||||
// ),
|
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
||||||
ListTile(
|
).t(context: context),
|
||||||
title: Text(
|
),
|
||||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
trailing: IconButton(
|
||||||
style: context.textTheme.titleSmall,
|
splashRadius: 16,
|
||||||
),
|
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
||||||
subtitle: Padding(
|
onPressed: () {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
showDialog(
|
||||||
child: Text(
|
context: context,
|
||||||
"backup_album_selection_page_albums_tap",
|
builder: (BuildContext context) {
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
return AlertDialog(
|
||||||
).t(context: context),
|
shape: const RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
trailing: IconButton(
|
),
|
||||||
splashRadius: 16,
|
elevation: 5,
|
||||||
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
title: Text(
|
||||||
onPressed: () {
|
'backup_album_selection_page_selection_info',
|
||||||
// show the dialog
|
style: TextStyle(
|
||||||
showDialog(
|
fontSize: 16,
|
||||||
context: context,
|
fontWeight: FontWeight.bold,
|
||||||
builder: (BuildContext context) {
|
color: context.primaryColor,
|
||||||
return AlertDialog(
|
),
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
).t(context: context),
|
||||||
elevation: 5,
|
content: SingleChildScrollView(
|
||||||
title: Text(
|
child: ListBody(
|
||||||
'backup_album_selection_page_selection_info',
|
children: [
|
||||||
style: TextStyle(
|
const Text(
|
||||||
fontSize: 16,
|
'backup_album_selection_page_assets_scatter',
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(fontSize: 14),
|
||||||
color: context.primaryColor,
|
).t(context: context),
|
||||||
),
|
],
|
||||||
).t(context: context),
|
),
|
||||||
content: SingleChildScrollView(
|
),
|
||||||
child: ListBody(
|
);
|
||||||
children: [
|
},
|
||||||
const Text(
|
|
||||||
'backup_album_selection_page_assets_scatter',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
).t(context: context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverLayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (constraints.crossAxisExtent > 600) {
|
||||||
|
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||||
|
} else {
|
||||||
|
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_handleLinkedAlbumFuture != null)
|
||||||
|
FutureBuilder(
|
||||||
|
future: _handleLinkedAlbumFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Container(
|
||||||
|
color: context.scaffoldBackgroundColor.withValues(alpha: 0.8),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(strokeWidth: 4),
|
||||||
|
Text("Creating linked albums...", style: context.textTheme.labelLarge),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
SliverLayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
if (constraints.crossAxisExtent > 600) {
|
|
||||||
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
|
||||||
} else {
|
|
||||||
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
]).then((_) async {
|
]).then((_) async {
|
||||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
if (isEnableBackup) {
|
if (isEnableBackup) {
|
||||||
final currentUser = _ref.read(currentUserProvider);
|
final currentUser = _ref.read(currentUserProvider);
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
|
|
@ -113,6 +115,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
|
|
||||||
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAlbumLinkedSyncEnable) {
|
||||||
|
await backgroundManager.syncLinkedAlbum();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
// import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
@ -323,7 +322,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()));
|
unawaited(
|
||||||
|
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
|
||||||
|
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
|
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
|
||||||
|
|
||||||
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
|
||||||
LocalAlbum toDto({int assetCount = 0}) {
|
|
||||||
return LocalAlbum(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
assetCount: assetCount,
|
|
||||||
backupSelection: backupSelection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension LocalAssetEntityDataHelper on LocalAssetEntityData {
|
|
||||||
LocalAsset toDto() {
|
|
||||||
return LocalAsset(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
checksum: checksum,
|
|
||||||
type: type,
|
|
||||||
createdAt: createdAt,
|
|
||||||
updatedAt: updatedAt,
|
|
||||||
durationInSeconds: durationInSeconds,
|
|
||||||
isFavorite: isFavorite,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,8 +23,10 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -268,11 +270,17 @@ Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
|
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
|
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
return Future.wait([
|
return Future.wait([
|
||||||
backgroundManager.syncLocal(full: full).then((_) {
|
backgroundManager.syncLocal(full: full).then((_) {
|
||||||
Logger("runNewSync").fine("Hashing assets after syncLocal");
|
Logger("runNewSync").fine("Hashing assets after syncLocal");
|
||||||
return backgroundManager.hashAssets();
|
return backgroundManager.hashAssets();
|
||||||
}),
|
}),
|
||||||
backgroundManager.syncRemote(),
|
backgroundManager.syncRemote().then((_) {
|
||||||
|
if (isAlbumLinkedSyncEnable) {
|
||||||
|
return backgroundManager.syncLinkedAlbum();
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,153 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
|
|
||||||
class DriftBackupSettings extends StatelessWidget {
|
class DriftBackupSettings extends ConsumerWidget {
|
||||||
const DriftBackupSettings({super.key});
|
const DriftBackupSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const SettingsSubPageScaffold(
|
||||||
|
settings: [
|
||||||
|
_UseWifiForUploadVideosButton(),
|
||||||
|
_UseWifiForUploadPhotosButton(),
|
||||||
|
Divider(indent: 16, endIndent: 16),
|
||||||
|
_AlbumSyncActionButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSyncActionButton extends ConsumerStatefulWidget {
|
||||||
|
const _AlbumSyncActionButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AlbumSyncActionButton> createState() => _AlbumSyncActionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> {
|
||||||
|
bool isAlbumSyncInProgress = false;
|
||||||
|
|
||||||
|
Future<void> _manualSyncAlbums() async {
|
||||||
|
setState(() {
|
||||||
|
isAlbumSyncInProgress = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||||
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
} catch (_) {
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
setState(() {
|
||||||
|
isAlbumSyncInProgress = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _manageLinkedAlbums() async {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final localAlbums = ref.read(backupAlbumProvider);
|
||||||
|
final selectedBackupAlbums = localAlbums
|
||||||
|
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedBackupAlbums, currentUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const SettingsSubPageScaffold(settings: [_UseWifiForUploadVideosButton(), _UseWifiForUploadPhotosButton()]);
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
StreamBuilder(
|
||||||
|
stream: Store.watch(StoreKey.syncAlbums),
|
||||||
|
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final albumSyncEnable = snapshot.data ?? false;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"sync_albums".t(context: context),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"sync_upload_album_setting_subtitle".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: albumSyncEnable,
|
||||||
|
onChanged: (bool newValue) async {
|
||||||
|
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
|
||||||
|
|
||||||
|
if (newValue == true) {
|
||||||
|
await _manageLinkedAlbums();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
opacity: albumSyncEnable ? 1.0 : 0.0,
|
||||||
|
child: albumSyncEnable
|
||||||
|
? ListTile(
|
||||||
|
onTap: _manualSyncAlbums,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||||
|
title: Text(
|
||||||
|
"organize_into_albums".t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"organize_into_albums_description".t(context: context),
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: isAlbumSyncInProgress
|
||||||
|
? const SizedBox(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: _manualSyncAlbums,
|
||||||
|
icon: const Icon(Icons.sync_rounded),
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
|
iconSize: 20,
|
||||||
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,37 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
await ref.read(storageRepositoryProvider).clearCache();
|
await ref.read(storageRepositoryProvider).clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetSqliteDb(BuildContext context, Future<void> Function() resetDatabase) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("reset_sqlite".t(context: context)),
|
||||||
|
content: Text("reset_sqlite_confirmation".t(context: context)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text("cancel".t(context: context)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await resetDatabase();
|
||||||
|
context.pop();
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"confirm".t(context: context),
|
||||||
|
style: TextStyle(color: context.colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return FutureBuilder<List<dynamic>>(
|
return FutureBuilder<List<dynamic>>(
|
||||||
future: loadCounts(),
|
future: loadCounts(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -116,6 +147,33 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"Error occur, reset the local database by tapping the button below",
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"reset_sqlite".t(context: context),
|
||||||
|
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
||||||
|
onTap: () async {
|
||||||
|
await resetSqliteDb(context, resetDatabase);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final assetCounts = snapshot.data![0]! as (int, int);
|
final assetCounts = snapshot.data![0]! as (int, int);
|
||||||
final localAssetCount = assetCounts.$1;
|
final localAssetCount = assetCounts.$1;
|
||||||
final remoteAssetCount = assetCounts.$2;
|
final remoteAssetCount = assetCounts.$2;
|
||||||
|
|
@ -270,34 +328,7 @@ class BetaSyncSettings extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
showDialog(
|
await resetSqliteDb(context, resetDatabase);
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text("reset_sqlite".t(context: context)),
|
|
||||||
content: Text("reset_sqlite_confirmation".t(context: context)),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text("cancel".t(context: context)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await resetDatabase();
|
|
||||||
context.pop();
|
|
||||||
context.scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"confirm".t(context: context),
|
|
||||||
style: TextStyle(color: context.colorScheme.error),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
BIN
mobile/test/drift/main/generated/schema.dart
generated
BIN
mobile/test/drift/main/generated/schema.dart
generated
Binary file not shown.
BIN
mobile/test/drift/main/generated/schema_v9.dart
generated
Normal file
BIN
mobile/test/drift/main/generated/schema_v9.dart
generated
Normal file
Binary file not shown.
Loading…
Reference in a new issue