chore: bump dart sdk to 3.8 (#20355)
* chore: bump dart sdk to 3.8 * chore: make build * make pigeon * chore: format files --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -49,11 +49,7 @@ class ActionService {
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
context.pushRoute(
|
||||
SharedLinkEditRoute(
|
||||
assetsList: remoteIds,
|
||||
),
|
||||
);
|
||||
context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds));
|
||||
}
|
||||
|
||||
Future<void> favorite(List<String> remoteIds) async {
|
||||
@@ -67,39 +63,18 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<void> archive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibilityEnum.archive,
|
||||
);
|
||||
await _remoteAssetRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibility.archive,
|
||||
);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.archive);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive);
|
||||
}
|
||||
|
||||
Future<void> unArchive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibilityEnum.timeline,
|
||||
);
|
||||
await _remoteAssetRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibility.timeline,
|
||||
);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
Future<void> moveToLockFolder(
|
||||
List<String> remoteIds,
|
||||
List<String> localIds,
|
||||
) async {
|
||||
await _assetApiRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibilityEnum.locked,
|
||||
);
|
||||
await _remoteAssetRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibility.locked,
|
||||
);
|
||||
Future<void> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||
|
||||
// Ask user if they want to delete local copies
|
||||
if (localIds.isNotEmpty) {
|
||||
@@ -112,14 +87,8 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<void> removeFromLockFolder(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibilityEnum.timeline,
|
||||
);
|
||||
await _remoteAssetRepository.updateVisibility(
|
||||
remoteIds,
|
||||
AssetVisibility.timeline,
|
||||
);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
Future<void> trash(List<String> remoteIds) async {
|
||||
@@ -145,10 +114,7 @@ class ActionService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteRemoteAndLocal(
|
||||
List<String> remoteIds,
|
||||
List<String> localIds,
|
||||
) async {
|
||||
Future<void> deleteRemoteAndLocal(List<String> remoteIds, List<String> localIds) async {
|
||||
await _assetApiRepository.delete(remoteIds, true);
|
||||
await _remoteAssetRepository.delete(remoteIds);
|
||||
|
||||
@@ -171,10 +137,7 @@ class ActionService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Future<bool> editLocation(
|
||||
List<String> remoteIds,
|
||||
BuildContext context,
|
||||
) async {
|
||||
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
||||
maplibre.LatLng? initialLatLng;
|
||||
if (remoteIds.length == 1) {
|
||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||
@@ -184,23 +147,14 @@ class ActionService {
|
||||
}
|
||||
}
|
||||
|
||||
final location = await showLocationPicker(
|
||||
context: context,
|
||||
initialLatLng: initialLatLng,
|
||||
);
|
||||
final location = await showLocationPicker(context: context, initialLatLng: initialLatLng);
|
||||
|
||||
if (location == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _assetApiRepository.updateLocation(
|
||||
remoteIds,
|
||||
location,
|
||||
);
|
||||
await _remoteAssetRepository.updateLocation(
|
||||
remoteIds,
|
||||
location,
|
||||
);
|
||||
await _assetApiRepository.updateLocation(remoteIds, location);
|
||||
await _remoteAssetRepository.updateLocation(remoteIds, location);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ class ActivityService with ErrorLoggerMixin {
|
||||
|
||||
ActivityService(this._activityApiRepository);
|
||||
|
||||
Future<List<Activity>> getAllActivities(
|
||||
String albumId, {
|
||||
String? assetId,
|
||||
}) async {
|
||||
Future<List<Activity>> getAllActivities(String albumId, {String? assetId}) async {
|
||||
return logError(
|
||||
() => _activityApiRepository.getAll(albumId, assetId: assetId),
|
||||
defaultValue: [],
|
||||
@@ -41,19 +38,9 @@ class ActivityService with ErrorLoggerMixin {
|
||||
);
|
||||
}
|
||||
|
||||
AsyncFuture<Activity> addActivity(
|
||||
String albumId,
|
||||
ActivityType type, {
|
||||
String? assetId,
|
||||
String? comment,
|
||||
}) async {
|
||||
AsyncFuture<Activity> addActivity(String albumId, ActivityType type, {String? assetId, String? comment}) async {
|
||||
return guardError(
|
||||
() => _activityApiRepository.create(
|
||||
albumId,
|
||||
type,
|
||||
assetId: assetId,
|
||||
comment: comment,
|
||||
),
|
||||
() => _activityApiRepository.create(albumId, type, assetId: assetId, comment: comment),
|
||||
errorMessage: "Failed to create $type for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class AlbumService {
|
||||
final (selectedIds, excludedIds, onDevice) = await (
|
||||
_backupAlbumRepository.getIdsBySelection(BackupSelection.select).then((value) => value.toSet()),
|
||||
_backupAlbumRepository.getIdsBySelection(BackupSelection.exclude).then((value) => value.toSet()),
|
||||
_albumMediaRepository.getAll()
|
||||
_albumMediaRepository.getAll(),
|
||||
).wait;
|
||||
_log.info("Found ${onDevice.length} device albums");
|
||||
if (selectedIds.isEmpty) {
|
||||
@@ -102,9 +102,7 @@ class AlbumService {
|
||||
}
|
||||
// remove all excluded albums
|
||||
onDevice.removeWhere((e) => excludedIds.contains(e.localId));
|
||||
_log.info(
|
||||
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
|
||||
);
|
||||
_log.info("Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums");
|
||||
}
|
||||
|
||||
final allAlbum = onDevice.firstWhereOrNull((album) => album.isAll);
|
||||
@@ -130,16 +128,11 @@ class AlbumService {
|
||||
return changes;
|
||||
}
|
||||
|
||||
Future<Set<String>> _loadExcludedAssetIds(
|
||||
List<Album> albums,
|
||||
Set<String> excludedAlbumIds,
|
||||
) async {
|
||||
Future<Set<String>> _loadExcludedAssetIds(List<Album> albums, Set<String> excludedAlbumIds) async {
|
||||
final Set<String> result = HashSet<String>();
|
||||
for (final batchAlbums in albums.where((album) => excludedAlbumIds.contains(album.localId)).slices(5)) {
|
||||
await batchAlbums
|
||||
.map(
|
||||
(album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds)),
|
||||
)
|
||||
.map((album) => _albumMediaRepository.getAssetIds(album.localId!).then((assetIds) => result.addAll(assetIds)))
|
||||
.wait;
|
||||
}
|
||||
return result;
|
||||
@@ -167,13 +160,10 @@ class AlbumService {
|
||||
_albumApiRepository.getAll(shared: true),
|
||||
// Passing null (or nothing) for `shared` returns only albums that
|
||||
// explicitly belong to us
|
||||
_albumApiRepository.getAll(shared: null)
|
||||
_albumApiRepository.getAll(shared: null),
|
||||
).wait;
|
||||
|
||||
final albums = HashSet<Album>(
|
||||
equals: (a, b) => a.remoteId == b.remoteId,
|
||||
hashCode: (a) => a.remoteId.hashCode,
|
||||
);
|
||||
final albums = HashSet<Album>(equals: (a, b) => a.remoteId == b.remoteId, hashCode: (a) => a.remoteId.hashCode);
|
||||
|
||||
albums.addAll(sharedAlbum);
|
||||
albums.addAll(ownedAlbum);
|
||||
@@ -205,7 +195,7 @@ class AlbumService {
|
||||
*/
|
||||
Future<String> _getNextAlbumName() async {
|
||||
const baseName = "Untitled";
|
||||
for (int round = 0;; round++) {
|
||||
for (int round = 0; ; round++) {
|
||||
final proposedName = "$baseName${round == 0 ? "" : " ($round)"}";
|
||||
|
||||
if (null == await _albumRepository.getByName(proposedName, owner: true)) {
|
||||
@@ -214,46 +204,28 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Album?> createAlbumWithGeneratedName(
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
return createAlbum(
|
||||
await _getNextAlbumName(),
|
||||
assets,
|
||||
[],
|
||||
);
|
||||
Future<Album?> createAlbumWithGeneratedName(Iterable<Asset> assets) async {
|
||||
return createAlbum(await _getNextAlbumName(), assets, []);
|
||||
}
|
||||
|
||||
Future<AlbumAddAssetsResponse?> addAssets(
|
||||
Album album,
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
Future<AlbumAddAssetsResponse?> addAssets(Album album, Iterable<Asset> assets) async {
|
||||
try {
|
||||
final result = await _albumApiRepository.addAssets(
|
||||
album.remoteId!,
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
final result = await _albumApiRepository.addAssets(album.remoteId!, assets.map((asset) => asset.remoteId!));
|
||||
|
||||
final List<Asset> addedAssets =
|
||||
result.added.map((id) => assets.firstWhere((asset) => asset.remoteId == id)).toList();
|
||||
final List<Asset> addedAssets = result.added
|
||||
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
|
||||
.toList();
|
||||
|
||||
await _updateAssets(album.id, add: addedAssets);
|
||||
|
||||
return AlbumAddAssetsResponse(
|
||||
alreadyInAlbum: result.duplicates,
|
||||
successfullyAdded: addedAssets.length,
|
||||
);
|
||||
return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length);
|
||||
} catch (e) {
|
||||
debugPrint("Error addAssets ${e.toString()}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _updateAssets(
|
||||
int albumId, {
|
||||
List<Asset> add = const [],
|
||||
List<Asset> remove = const [],
|
||||
}) =>
|
||||
Future<void> _updateAssets(int albumId, {List<Asset> add = const [], List<Asset> remove = const []}) =>
|
||||
_albumRepository.transaction(() async {
|
||||
final album = await _albumRepository.get(albumId);
|
||||
if (album == null) return;
|
||||
@@ -265,10 +237,7 @@ class AlbumService {
|
||||
|
||||
Future<bool> setActivityStatus(Album album, bool enabled) async {
|
||||
try {
|
||||
final updatedAlbum = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
activityEnabled: enabled,
|
||||
);
|
||||
final updatedAlbum = await _albumApiRepository.update(album.remoteId!, activityEnabled: enabled);
|
||||
album.activityEnabled = updatedAlbum.activityEnabled;
|
||||
await _albumRepository.update(album);
|
||||
return true;
|
||||
@@ -291,9 +260,7 @@ class AlbumService {
|
||||
final List<Album> albums = await _albumRepository.getAll(shared: true);
|
||||
final List<Asset> existing = [];
|
||||
for (Album album in albums) {
|
||||
existing.addAll(
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]),
|
||||
);
|
||||
existing.addAll(await _assetRepository.getByAlbum(album, notOwnedBy: [userId]));
|
||||
}
|
||||
final List<int> idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing);
|
||||
if (idsToRemove.isNotEmpty) {
|
||||
@@ -319,15 +286,9 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> removeAsset(
|
||||
Album album,
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
|
||||
try {
|
||||
final result = await _albumApiRepository.removeAssets(
|
||||
album.remoteId!,
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
final result = await _albumApiRepository.removeAssets(album.remoteId!, assets.map((asset) => asset.remoteId!));
|
||||
final toRemove = result.removed.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
|
||||
await _updateAssets(album.id, remove: toRemove.toList());
|
||||
return true;
|
||||
@@ -337,15 +298,9 @@ class AlbumService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> removeUser(
|
||||
Album album,
|
||||
UserDto user,
|
||||
) async {
|
||||
Future<bool> removeUser(Album album, UserDto user) async {
|
||||
try {
|
||||
await _albumApiRepository.removeUser(
|
||||
album.remoteId!,
|
||||
userId: user.id,
|
||||
);
|
||||
await _albumApiRepository.removeUser(album.remoteId!, userId: user.id);
|
||||
|
||||
album.sharedUsers.remove(entity.User.fromDto(user));
|
||||
await _albumRepository.removeUsers(album, [user]);
|
||||
@@ -360,20 +315,14 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addUsers(
|
||||
Album album,
|
||||
List<String> userIds,
|
||||
) async {
|
||||
Future<bool> addUsers(Album album, List<String> userIds) async {
|
||||
try {
|
||||
final updatedAlbum = await _albumApiRepository.addUsers(album.remoteId!, userIds);
|
||||
|
||||
album.sharedUsers.addAll(updatedAlbum.remoteUsers);
|
||||
album.shared = true;
|
||||
|
||||
await _albumRepository.addUsers(
|
||||
album,
|
||||
album.sharedUsers.map((u) => u.toDto()).toList(),
|
||||
);
|
||||
await _albumRepository.addUsers(album, album.sharedUsers.map((u) => u.toDto()).toList());
|
||||
await _albumRepository.update(album);
|
||||
|
||||
return true;
|
||||
@@ -383,15 +332,9 @@ class AlbumService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> changeTitleAlbum(
|
||||
Album album,
|
||||
String newAlbumTitle,
|
||||
) async {
|
||||
Future<bool> changeTitleAlbum(Album album, String newAlbumTitle) async {
|
||||
try {
|
||||
final updatedAlbum = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
name: newAlbumTitle,
|
||||
);
|
||||
final updatedAlbum = await _albumApiRepository.update(album.remoteId!, name: newAlbumTitle);
|
||||
|
||||
album.name = updatedAlbum.name;
|
||||
await _albumRepository.update(album);
|
||||
@@ -402,15 +345,9 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> changeDescriptionAlbum(
|
||||
Album album,
|
||||
String newAlbumDescription,
|
||||
) async {
|
||||
Future<bool> changeDescriptionAlbum(Album album, String newAlbumDescription) async {
|
||||
try {
|
||||
final updatedAlbum = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
description: newAlbumDescription,
|
||||
);
|
||||
final updatedAlbum = await _albumApiRepository.update(album.remoteId!, description: newAlbumDescription);
|
||||
|
||||
album.description = updatedAlbum.description;
|
||||
await _albumRepository.update(album);
|
||||
@@ -421,26 +358,13 @@ class AlbumService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Album?> getAlbumByName(
|
||||
String name, {
|
||||
bool? remote,
|
||||
bool? shared,
|
||||
bool? owner,
|
||||
}) =>
|
||||
_albumRepository.getByName(
|
||||
name,
|
||||
remote: remote,
|
||||
shared: shared,
|
||||
owner: owner,
|
||||
);
|
||||
Future<Album?> getAlbumByName(String name, {bool? remote, bool? shared, bool? owner}) =>
|
||||
_albumRepository.getByName(name, remote: remote, shared: shared, owner: owner);
|
||||
|
||||
///
|
||||
/// Add the uploaded asset to the selected albums
|
||||
///
|
||||
Future<void> syncUploadAlbums(
|
||||
List<String> albumNames,
|
||||
List<String> assetIds,
|
||||
) async {
|
||||
Future<void> syncUploadAlbums(List<String> albumNames, List<String> assetIds) async {
|
||||
for (final albumName in albumNames) {
|
||||
Album? album = await getAlbumByName(albumName, remote: true, owner: true);
|
||||
album ??= await createAlbum(albumName, []);
|
||||
@@ -479,10 +403,7 @@ class AlbumService {
|
||||
return _albumRepository.watchAlbum(id);
|
||||
}
|
||||
|
||||
Future<List<Album>> search(
|
||||
String searchTerm,
|
||||
QuickFilterMode filterMode,
|
||||
) async {
|
||||
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode) async {
|
||||
return _albumRepository.search(searchTerm, filterMode);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,11 +127,7 @@ class ApiService implements Authentication {
|
||||
} on SocketException catch (_) {
|
||||
return false;
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe(
|
||||
"Error while checking server availability",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
_log.severe("Error while checking server availability", error, stackTrace);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -145,10 +141,7 @@ class ApiService implements Authentication {
|
||||
headers.addAll(getRequestHeaders());
|
||||
|
||||
final res = await client
|
||||
.get(
|
||||
Uri.parse("$baseUrl/.well-known/immich"),
|
||||
headers: headers,
|
||||
)
|
||||
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
@@ -211,10 +204,7 @@ class ApiService implements Authentication {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyToParams(
|
||||
List<QueryParam> queryParams,
|
||||
Map<String, String> headerParams,
|
||||
) {
|
||||
Future<void> applyToParams(List<QueryParam> queryParams, Map<String, String> headerParams) {
|
||||
return Future<void>(() {
|
||||
var headers = ApiService.getRequestHeaders();
|
||||
headerParams.addAll(headers);
|
||||
|
||||
@@ -5,26 +5,10 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
enum AppSettingsEnum<T> {
|
||||
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
|
||||
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
|
||||
themeMode<String>(
|
||||
StoreKey.themeMode,
|
||||
"themeMode",
|
||||
"system",
|
||||
), // "light","dark","system"
|
||||
primaryColor<String>(
|
||||
StoreKey.primaryColor,
|
||||
"primaryColor",
|
||||
defaultColorPresetName,
|
||||
),
|
||||
dynamicTheme<bool>(
|
||||
StoreKey.dynamicTheme,
|
||||
"dynamicTheme",
|
||||
false,
|
||||
),
|
||||
colorfulInterface<bool>(
|
||||
StoreKey.colorfulInterface,
|
||||
"colorfulInterface",
|
||||
true,
|
||||
),
|
||||
themeMode<String>(StoreKey.themeMode, "themeMode", "system"), // "light","dark","system"
|
||||
primaryColor<String>(StoreKey.primaryColor, "primaryColor", defaultColorPresetName),
|
||||
dynamicTheme<bool>(StoreKey.dynamicTheme, "dynamicTheme", false),
|
||||
colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true),
|
||||
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
|
||||
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
|
||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
|
||||
@@ -33,43 +17,23 @@ enum AppSettingsEnum<T> {
|
||||
"uploadErrorNotificationGracePeriod",
|
||||
2,
|
||||
),
|
||||
backgroundBackupTotalProgress<bool>(
|
||||
StoreKey.backgroundBackupTotalProgress,
|
||||
"backgroundBackupTotalProgress",
|
||||
true,
|
||||
),
|
||||
backgroundBackupTotalProgress<bool>(StoreKey.backgroundBackupTotalProgress, "backgroundBackupTotalProgress", true),
|
||||
backgroundBackupSingleProgress<bool>(
|
||||
StoreKey.backgroundBackupSingleProgress,
|
||||
"backgroundBackupSingleProgress",
|
||||
false,
|
||||
),
|
||||
storageIndicator<bool>(StoreKey.storageIndicator, "storageIndicator", true),
|
||||
thumbnailCacheSize<int>(
|
||||
StoreKey.thumbnailCacheSize,
|
||||
"thumbnailCacheSize",
|
||||
10000,
|
||||
),
|
||||
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
||||
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
||||
albumThumbnailCacheSize<int>(
|
||||
StoreKey.albumThumbnailCacheSize,
|
||||
"albumThumbnailCacheSize",
|
||||
200,
|
||||
),
|
||||
selectedAlbumSortOrder<int>(
|
||||
StoreKey.selectedAlbumSortOrder,
|
||||
"selectedAlbumSortOrder",
|
||||
0,
|
||||
),
|
||||
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
||||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 0),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
loadOriginalVideo<bool>(
|
||||
StoreKey.loadOriginalVideo,
|
||||
"loadOriginalVideo",
|
||||
false,
|
||||
),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
|
||||
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
||||
@@ -77,22 +41,13 @@ enum AppSettingsEnum<T> {
|
||||
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
||||
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
|
||||
ignoreIcloudAssets<bool>(StoreKey.ignoreIcloudAssets, null, false),
|
||||
selectedAlbumSortReverse<bool>(
|
||||
StoreKey.selectedAlbumSortReverse,
|
||||
null,
|
||||
false,
|
||||
),
|
||||
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, false),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
|
||||
photoManagerCustomFilter<bool>(
|
||||
StoreKey.photoManagerCustomFilter,
|
||||
null,
|
||||
true,
|
||||
),
|
||||
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
|
||||
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||
;
|
||||
enableBackup<bool>(StoreKey.enableBackup, null, false);
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -78,8 +78,9 @@ class AssetService {
|
||||
/// required. Returns `true` if there were any changes.
|
||||
Future<bool> refreshRemoteAssets() async {
|
||||
final syncedUserIds = await _etagRepository.getAllIds();
|
||||
final List<UserDto> syncedUsers =
|
||||
syncedUserIds.isEmpty ? [] : (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList();
|
||||
final List<UserDto> syncedUsers = syncedUserIds.isEmpty
|
||||
? []
|
||||
: (await _isarUserRepository.getByUserIds(syncedUserIds)).nonNulls.toList();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||
users: syncedUsers,
|
||||
@@ -95,10 +96,7 @@ class AssetService {
|
||||
List<UserDto> users,
|
||||
DateTime since,
|
||||
) async {
|
||||
final dto = AssetDeltaSyncDto(
|
||||
updatedAfter: since,
|
||||
userIds: users.map((e) => e.id).toList(),
|
||||
);
|
||||
final dto = AssetDeltaSyncDto(updatedAfter: since, userIds: users.map((e) => e.id).toList());
|
||||
final changes = await _apiService.syncApi.getDeltaSync(dto);
|
||||
return changes == null || changes.needsFullSync
|
||||
? (null, null)
|
||||
@@ -107,19 +105,13 @@ class AssetService {
|
||||
|
||||
/// Returns the list of people of the given asset id.
|
||||
// If the server is not reachable `null` is returned.
|
||||
Future<List<PersonWithFacesResponseDto>?> getRemotePeopleOfAsset(
|
||||
String remoteId,
|
||||
) async {
|
||||
Future<List<PersonWithFacesResponseDto>?> getRemotePeopleOfAsset(String remoteId) async {
|
||||
try {
|
||||
final AssetResponseDto? dto = await _apiService.assetsApi.getAssetInfo(remoteId);
|
||||
|
||||
return dto?.people;
|
||||
} catch (error, stack) {
|
||||
log.severe(
|
||||
'Error while getting remote asset info: ${error.toString()}',
|
||||
error,
|
||||
stack,
|
||||
);
|
||||
log.severe('Error while getting remote asset info: ${error.toString()}', error, stack);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -133,18 +125,11 @@ class AssetService {
|
||||
String? lastId;
|
||||
// will break on error or once all assets are loaded
|
||||
while (true) {
|
||||
final dto = AssetFullSyncDto(
|
||||
limit: chunkSize,
|
||||
updatedUntil: until,
|
||||
lastId: lastId,
|
||||
userId: user.id,
|
||||
);
|
||||
final dto = AssetFullSyncDto(limit: chunkSize, updatedUntil: until, lastId: lastId, userId: user.id);
|
||||
log.fine("Requesting $chunkSize assets from $lastId");
|
||||
final List<AssetResponseDto>? assets = await _apiService.syncApi.getFullSyncForUser(dto);
|
||||
if (assets == null) return null;
|
||||
log.fine(
|
||||
"Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}",
|
||||
);
|
||||
log.fine("Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}");
|
||||
allAssets.addAll(assets.map(Asset.remote));
|
||||
if (assets.length != chunkSize) break;
|
||||
lastId = assets.last.id;
|
||||
@@ -182,10 +167,7 @@ class AssetService {
|
||||
return a;
|
||||
}
|
||||
|
||||
Future<void> updateAssets(
|
||||
List<Asset> assets,
|
||||
UpdateAssetDto updateAssetDto,
|
||||
) async {
|
||||
Future<void> updateAssets(List<Asset> assets, UpdateAssetDto updateAssetDto) async {
|
||||
return await _apiService.assetsApi.updateAssets(
|
||||
AssetBulkUpdateDto(
|
||||
ids: assets.map((e) => e.remoteId!).toList(),
|
||||
@@ -198,10 +180,7 @@ class AssetService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Asset>> changeFavoriteStatus(
|
||||
List<Asset> assets,
|
||||
bool isFavorite,
|
||||
) async {
|
||||
Future<List<Asset>> changeFavoriteStatus(List<Asset> assets, bool isFavorite) async {
|
||||
try {
|
||||
await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite));
|
||||
|
||||
@@ -218,16 +197,11 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>> changeArchiveStatus(
|
||||
List<Asset> assets,
|
||||
bool isArchived,
|
||||
) async {
|
||||
Future<List<Asset>> changeArchiveStatus(List<Asset> assets, bool isArchived) async {
|
||||
try {
|
||||
await updateAssets(
|
||||
assets,
|
||||
UpdateAssetDto(
|
||||
visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline,
|
||||
),
|
||||
UpdateAssetDto(visibility: isArchived ? AssetVisibility.archive : AssetVisibility.timeline),
|
||||
);
|
||||
|
||||
for (var element in assets) {
|
||||
@@ -244,15 +218,9 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> changeDateTime(
|
||||
List<Asset> assets,
|
||||
String updatedDt,
|
||||
) async {
|
||||
Future<List<Asset>?> changeDateTime(List<Asset> assets, String updatedDt) async {
|
||||
try {
|
||||
await updateAssets(
|
||||
assets,
|
||||
UpdateAssetDto(dateTimeOriginal: updatedDt),
|
||||
);
|
||||
await updateAssets(assets, UpdateAssetDto(dateTimeOriginal: updatedDt));
|
||||
|
||||
for (var element in assets) {
|
||||
element.fileCreatedAt = DateTime.parse(updatedDt);
|
||||
@@ -268,24 +236,12 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> changeLocation(
|
||||
List<Asset> assets,
|
||||
LatLng location,
|
||||
) async {
|
||||
Future<List<Asset>?> changeLocation(List<Asset> assets, LatLng location) async {
|
||||
try {
|
||||
await updateAssets(
|
||||
assets,
|
||||
UpdateAssetDto(
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
),
|
||||
);
|
||||
await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude));
|
||||
|
||||
for (var element in assets) {
|
||||
element.exifInfo = element.exifInfo?.copyWith(
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
);
|
||||
element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude);
|
||||
}
|
||||
|
||||
await _syncService.upsertAssetsWithExif(assets);
|
||||
@@ -310,18 +266,13 @@ class AssetService {
|
||||
|
||||
await refreshRemoteAssets();
|
||||
final owner = _userService.getMyUser();
|
||||
final remoteAssets = await _assetRepository.getAll(
|
||||
ownerId: owner.id,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
final remoteAssets = await _assetRepository.getAll(ownerId: owner.id, state: AssetState.merged);
|
||||
|
||||
/// Map<AlbumName, [AssetId]>
|
||||
Map<String, List<String>> assetToAlbums = {};
|
||||
|
||||
for (BackupCandidate candidate in candidates) {
|
||||
final asset = remoteAssets.firstWhereOrNull(
|
||||
(a) => a.localId == candidate.asset.localId,
|
||||
);
|
||||
final asset = remoteAssets.firstWhereOrNull((a) => a.localId == candidate.asset.localId);
|
||||
|
||||
if (asset != null) {
|
||||
for (final albumName in candidate.albumNames) {
|
||||
@@ -342,10 +293,7 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setDescription(
|
||||
Asset asset,
|
||||
String newDescription,
|
||||
) async {
|
||||
Future<void> setDescription(Asset asset, String newDescription) async {
|
||||
final remoteAssetId = asset.remoteId;
|
||||
final localExifId = asset.exifInfo?.assetId;
|
||||
|
||||
@@ -354,10 +302,7 @@ class AssetService {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _assetApiRepository.update(
|
||||
remoteAssetId,
|
||||
description: newDescription,
|
||||
);
|
||||
final result = await _assetApiRepository.update(remoteAssetId, description: newDescription);
|
||||
|
||||
final description = result.exifInfo?.description;
|
||||
|
||||
@@ -437,10 +382,7 @@ class AssetService {
|
||||
}
|
||||
|
||||
/// Delete assets from the server and unreference from the database
|
||||
Future<void> deleteRemoteAssets(
|
||||
Iterable<Asset> assets, {
|
||||
bool shouldDeletePermanently = false,
|
||||
}) async {
|
||||
Future<void> deleteRemoteAssets(Iterable<Asset> assets, {bool shouldDeletePermanently = false}) async {
|
||||
final candidates = assets.where((a) => a.isRemote);
|
||||
|
||||
if (candidates.isEmpty) {
|
||||
@@ -448,10 +390,7 @@ class AssetService {
|
||||
}
|
||||
|
||||
await _apiService.assetsApi.deleteAssets(
|
||||
AssetBulkDeleteDto(
|
||||
ids: candidates.map((a) => a.remoteId!).toList(),
|
||||
force: shouldDeletePermanently,
|
||||
),
|
||||
AssetBulkDeleteDto(ids: candidates.map((a) => a.remoteId!).toList(), force: shouldDeletePermanently),
|
||||
);
|
||||
|
||||
/// Update asset info bassed on the deletion type.
|
||||
@@ -470,8 +409,10 @@ class AssetService {
|
||||
await _assetRepository.updateAll(payload.toList());
|
||||
|
||||
if (shouldDeletePermanently) {
|
||||
final remoteAssetIds =
|
||||
assets.where((asset) => asset.storage == AssetState.remote).map((asset) => asset.id).toList();
|
||||
final remoteAssetIds = assets
|
||||
.where((asset) => asset.storage == AssetState.remote)
|
||||
.map((asset) => asset.id)
|
||||
.toList();
|
||||
await _assetRepository.deleteByIds(remoteAssetIds);
|
||||
}
|
||||
});
|
||||
@@ -479,10 +420,7 @@ class AssetService {
|
||||
|
||||
/// Delete assets on both local file system and the server.
|
||||
/// Unreference from the database.
|
||||
Future<void> deleteAssets(
|
||||
Iterable<Asset> assets, {
|
||||
bool shouldDeletePermanently = false,
|
||||
}) async {
|
||||
Future<void> deleteAssets(Iterable<Asset> assets, {bool shouldDeletePermanently = false}) async {
|
||||
final hasLocal = assets.any((asset) => asset.isLocal);
|
||||
final hasRemote = assets.any((asset) => asset.isRemote);
|
||||
|
||||
@@ -491,10 +429,7 @@ class AssetService {
|
||||
}
|
||||
|
||||
if (hasRemote) {
|
||||
await deleteRemoteAssets(
|
||||
assets,
|
||||
shouldDeletePermanently: shouldDeletePermanently,
|
||||
);
|
||||
await deleteRemoteAssets(assets, shouldDeletePermanently: shouldDeletePermanently);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,14 +447,8 @@ class AssetService {
|
||||
return _assetRepository.getMotionAssets(me.id);
|
||||
}
|
||||
|
||||
Future<void> setVisibility(
|
||||
List<Asset> assets,
|
||||
AssetVisibilityEnum visibility,
|
||||
) async {
|
||||
await _assetApiRepository.updateVisibility(
|
||||
assets.map((asset) => asset.remoteId!).toList(),
|
||||
visibility,
|
||||
);
|
||||
Future<void> setVisibility(List<Asset> assets, AssetVisibilityEnum visibility) async {
|
||||
await _assetApiRepository.updateVisibility(assets.map((asset) => asset.remoteId!).toList(), visibility);
|
||||
|
||||
final updatedAssets = assets.map((asset) {
|
||||
asset.visibility = visibility;
|
||||
|
||||
@@ -111,10 +111,7 @@ class AuthService {
|
||||
_log.severe("Error clearing local data", error, stackTrace);
|
||||
});
|
||||
|
||||
await _appSettingsService.setSetting(
|
||||
AppSettingsEnum.enableBackup,
|
||||
false,
|
||||
);
|
||||
await _appSettingsService.setSetting(AppSettingsEnum.enableBackup, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,10 @@ class BackgroundService {
|
||||
String _lastPrintedDetailContent = "";
|
||||
String? _lastPrintedDetailTitle;
|
||||
late final ThrottleProgressUpdate _throttledNotifiy = ThrottleProgressUpdate(_updateProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledDetailNotify =
|
||||
ThrottleProgressUpdate(_updateDetailProgress, notifyInterval);
|
||||
late final ThrottleProgressUpdate _throttledDetailNotify = ThrottleProgressUpdate(
|
||||
_updateDetailProgress,
|
||||
notifyInterval,
|
||||
);
|
||||
|
||||
bool get isBackgroundInitialized {
|
||||
return _isBackgroundInitialized;
|
||||
@@ -87,15 +89,12 @@ class BackgroundService {
|
||||
int triggerMaxDelay = 50000,
|
||||
}) async {
|
||||
try {
|
||||
final bool ok = await _foregroundChannel.invokeMethod(
|
||||
'configure',
|
||||
[
|
||||
requireUnmetered,
|
||||
requireCharging,
|
||||
triggerUpdateDelay,
|
||||
triggerMaxDelay,
|
||||
],
|
||||
);
|
||||
final bool ok = await _foregroundChannel.invokeMethod('configure', [
|
||||
requireUnmetered,
|
||||
requireCharging,
|
||||
triggerUpdateDelay,
|
||||
triggerMaxDelay,
|
||||
]);
|
||||
return ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
@@ -140,10 +139,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<List<Uint8List?>?> digestFiles(List<String> paths) {
|
||||
return _foregroundChannel.invokeListMethod<Uint8List?>(
|
||||
"digestFiles",
|
||||
paths,
|
||||
);
|
||||
return _foregroundChannel.invokeListMethod<Uint8List?>("digestFiles", paths);
|
||||
}
|
||||
|
||||
/// Updates the notification shown by the background service
|
||||
@@ -158,10 +154,15 @@ class BackgroundService {
|
||||
}) async {
|
||||
try {
|
||||
if (_isBackgroundInitialized) {
|
||||
return _backgroundChannel.invokeMethod<bool>(
|
||||
'updateNotification',
|
||||
[title, content, progress, max, indeterminate, isDetail, onlyIfFG],
|
||||
);
|
||||
return _backgroundChannel.invokeMethod<bool>('updateNotification', [
|
||||
title,
|
||||
content,
|
||||
progress,
|
||||
max,
|
||||
indeterminate,
|
||||
isDetail,
|
||||
onlyIfFG,
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint("[_updateNotification] failed to communicate with plugin");
|
||||
@@ -170,11 +171,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
/// Shows a new priority notification
|
||||
Future<bool> _showErrorNotification({
|
||||
required String title,
|
||||
String? content,
|
||||
String? individualTag,
|
||||
}) async {
|
||||
Future<bool> _showErrorNotification({required String title, String? content, String? individualTag}) async {
|
||||
try {
|
||||
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
|
||||
return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]);
|
||||
@@ -191,9 +188,7 @@ class BackgroundService {
|
||||
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
"[_clearErrorNotifications] failed to communicate with plugin",
|
||||
);
|
||||
debugPrint("[_clearErrorNotifications] failed to communicate with plugin");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -302,10 +297,7 @@ class BackgroundService {
|
||||
// indefinitely and can run later
|
||||
// Android is fine to wait here until the lock releases
|
||||
final waitForLock = Platform.isIOS
|
||||
? acquireLock().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => false,
|
||||
)
|
||||
? acquireLock().timeout(const Duration(seconds: 5), onTimeout: () => false)
|
||||
: acquireLock();
|
||||
|
||||
final bool hasAccess = await waitForLock;
|
||||
@@ -341,20 +333,13 @@ class BackgroundService {
|
||||
final db = await Bootstrap.initIsar();
|
||||
await Bootstrap.initDomain(db);
|
||||
|
||||
final ref = ProviderContainer(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(db),
|
||||
isarProvider.overrideWithValue(db),
|
||||
],
|
||||
);
|
||||
final ref = ProviderContainer(overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)]);
|
||||
|
||||
HttpSSLOptions.apply();
|
||||
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
|
||||
await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
"[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}",
|
||||
);
|
||||
debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
|
||||
}
|
||||
|
||||
final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select);
|
||||
@@ -418,10 +403,7 @@ class BackgroundService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<BackupCandidate> toUpload = await backupService.buildUploadCandidates(
|
||||
selectedAlbums,
|
||||
excludedAlbums,
|
||||
);
|
||||
Set<BackupCandidate> toUpload = await backupService.buildUploadCandidates(selectedAlbums, excludedAlbums);
|
||||
|
||||
try {
|
||||
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
|
||||
@@ -444,12 +426,7 @@ class BackgroundService {
|
||||
_uploadedAssetsCount = 0;
|
||||
_updateNotification(
|
||||
title: "backup_background_service_in_progress_notification".tr(),
|
||||
content: notifyTotalProgress
|
||||
? formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
_assetsToUploadCount,
|
||||
)
|
||||
: null,
|
||||
content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null,
|
||||
progress: 0,
|
||||
max: notifyTotalProgress ? _assetsToUploadCount : 0,
|
||||
indeterminate: !notifyTotalProgress,
|
||||
@@ -463,9 +440,7 @@ class BackgroundService {
|
||||
toUpload,
|
||||
_cancellationToken!,
|
||||
pmProgressHandler: pmProgressHandler,
|
||||
onSuccess: (result) => _onAssetUploaded(
|
||||
shouldNotify: notifyTotalProgress,
|
||||
),
|
||||
onSuccess: (result) => _onAssetUploaded(shouldNotify: notifyTotalProgress),
|
||||
onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress),
|
||||
onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress),
|
||||
onError: _onBackupError,
|
||||
@@ -482,9 +457,7 @@ class BackgroundService {
|
||||
return ok;
|
||||
}
|
||||
|
||||
void _onAssetUploaded({
|
||||
bool shouldNotify = false,
|
||||
}) async {
|
||||
void _onAssetUploaded({bool shouldNotify = false}) async {
|
||||
if (!shouldNotify) {
|
||||
return;
|
||||
}
|
||||
@@ -522,31 +495,27 @@ class BackgroundService {
|
||||
progress: _uploadedAssetsCount,
|
||||
max: _assetsToUploadCount,
|
||||
title: title,
|
||||
content: formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
_assetsToUploadCount,
|
||||
),
|
||||
content: formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount),
|
||||
);
|
||||
}
|
||||
|
||||
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||
_showErrorNotification(
|
||||
title:
|
||||
"backup_background_service_upload_failure_notification".tr(namedArgs: {'filename': errorAssetInfo.fileName}),
|
||||
title: "backup_background_service_upload_failure_notification".tr(
|
||||
namedArgs: {'filename': errorAssetInfo.fileName},
|
||||
),
|
||||
individualTag: errorAssetInfo.id,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSetCurrentBackupAsset(
|
||||
CurrentUploadAsset currentUploadAsset, {
|
||||
bool shouldNotify = false,
|
||||
}) {
|
||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset, {bool shouldNotify = false}) {
|
||||
if (!shouldNotify) {
|
||||
return;
|
||||
}
|
||||
|
||||
_throttledDetailNotify.title = "backup_background_service_current_upload_notification"
|
||||
.tr(namedArgs: {'filename': currentUploadAsset.fileName});
|
||||
_throttledDetailNotify.title = "backup_background_service_current_upload_notification".tr(
|
||||
namedArgs: {'filename': currentUploadAsset.fileName},
|
||||
);
|
||||
_throttledDetailNotify.progress = 0;
|
||||
_throttledDetailNotify.total = 0;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,8 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) => _assetRepository.transaction(
|
||||
() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds),
|
||||
);
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) =>
|
||||
_assetRepository.transaction(() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds));
|
||||
|
||||
/// Get duplicated asset id from database
|
||||
Future<Set<String>> getDuplicatedAssetIds() async {
|
||||
@@ -135,8 +134,8 @@ class BackupService {
|
||||
backupAlbum.id,
|
||||
modifiedFrom: useTimeFilter
|
||||
?
|
||||
// subtract 2 seconds to prevent missing assets due to rounding issues
|
||||
backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
|
||||
// subtract 2 seconds to prevent missing assets due to rounding issues
|
||||
backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
|
||||
: null,
|
||||
modifiedUntil: useTimeFilter ? now : null,
|
||||
);
|
||||
@@ -149,9 +148,7 @@ class BackupService {
|
||||
for (final asset in assets) {
|
||||
List<String> albumNames = [localAlbum.name];
|
||||
|
||||
final existingAsset = candidates.firstWhereOrNull(
|
||||
(candidate) => candidate.asset.localId == asset.localId,
|
||||
);
|
||||
final existingAsset = candidates.firstWhereOrNull((candidate) => candidate.asset.localId == asset.localId);
|
||||
|
||||
if (existingAsset != null) {
|
||||
albumNames.addAll(existingAsset.albumNames);
|
||||
@@ -168,17 +165,13 @@ class BackupService {
|
||||
}
|
||||
|
||||
/// Returns a new list of assets not yet uploaded
|
||||
Future<Set<BackupCandidate>> removeAlreadyUploadedAssets(
|
||||
Set<BackupCandidate> candidates,
|
||||
) async {
|
||||
Future<Set<BackupCandidate>> removeAlreadyUploadedAssets(Set<BackupCandidate> candidates) async {
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
|
||||
candidates.removeWhere(
|
||||
(candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
|
||||
);
|
||||
candidates.removeWhere((candidate) => duplicatedAssetIds.contains(candidate.asset.localId));
|
||||
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
@@ -188,10 +181,7 @@ class BackupService {
|
||||
try {
|
||||
final String deviceId = Store.get(StoreKey.deviceId);
|
||||
final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets(
|
||||
CheckExistingAssetsDto(
|
||||
deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
|
||||
deviceId: deviceId,
|
||||
),
|
||||
CheckExistingAssetsDto(deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(), deviceId: deviceId),
|
||||
);
|
||||
if (duplicates != null) {
|
||||
existing.addAll(duplicates.existingIds);
|
||||
@@ -215,8 +205,10 @@ class BackupService {
|
||||
if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) {
|
||||
// double check that permission is granted here, to guard against
|
||||
// uploading corrupt assets without EXIF information
|
||||
_log.warning("Media location permission is not granted. "
|
||||
"Cannot access original assets for backup.");
|
||||
_log.warning(
|
||||
"Media location permission is not granted. "
|
||||
"Cannot access original assets for backup.",
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -232,13 +224,11 @@ class BackupService {
|
||||
/// Upload images before video assets for background tasks
|
||||
/// these are further sorted by using their creation date
|
||||
List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
|
||||
return candidates.sorted(
|
||||
(a, b) {
|
||||
final cmp = a.asset.type.index - b.asset.type.index;
|
||||
if (cmp != 0) return cmp;
|
||||
return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
|
||||
},
|
||||
);
|
||||
return candidates.sorted((a, b) {
|
||||
final cmp = a.asset.type.index - b.asset.type.index;
|
||||
if (cmp != 0) return cmp;
|
||||
return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> backupAsset(
|
||||
@@ -295,10 +285,7 @@ class BackupService {
|
||||
|
||||
file = await asset.local!.loadFile(progressHandler: pmProgressHandler);
|
||||
if (asset.local!.isLivePhoto) {
|
||||
livePhotoFile = await asset.local!.loadFile(
|
||||
withSubtype: true,
|
||||
progressHandler: pmProgressHandler,
|
||||
);
|
||||
livePhotoFile = await asset.local!.loadFile(withSubtype: true, progressHandler: pmProgressHandler);
|
||||
}
|
||||
} else {
|
||||
file = await asset.local!.originFile.timeout(const Duration(seconds: 5));
|
||||
@@ -314,9 +301,7 @@ class BackupService {
|
||||
|
||||
if (asset.local!.isLivePhoto) {
|
||||
if (livePhotoFile == null) {
|
||||
_log.warning(
|
||||
"Failed to obtain motion part of the livePhoto - $originalFileName",
|
||||
);
|
||||
_log.warning("Failed to obtain motion part of the livePhoto - $originalFileName");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,22 +341,14 @@ class BackupService {
|
||||
|
||||
String? livePhotoVideoId;
|
||||
if (asset.local!.isLivePhoto && livePhotoFile != null) {
|
||||
livePhotoVideoId = await uploadLivePhotoVideo(
|
||||
originalFileName,
|
||||
livePhotoFile,
|
||||
baseRequest,
|
||||
cancelToken,
|
||||
);
|
||||
livePhotoVideoId = await uploadLivePhotoVideo(originalFileName, livePhotoFile, baseRequest, cancelToken);
|
||||
}
|
||||
|
||||
if (livePhotoVideoId != null) {
|
||||
baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId;
|
||||
}
|
||||
|
||||
final response = await httpClient.send(
|
||||
baseRequest,
|
||||
cancellationToken: cancelToken,
|
||||
);
|
||||
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
|
||||
|
||||
final responseBody = jsonDecode(await response.stream.bytesToString());
|
||||
|
||||
@@ -417,10 +394,7 @@ class BackupService {
|
||||
);
|
||||
|
||||
if (shouldSyncAlbums) {
|
||||
await _albumService.syncUploadAlbums(
|
||||
candidate.albumNames,
|
||||
[responseBody['id'] as String],
|
||||
);
|
||||
await _albumService.syncUploadAlbums(candidate.albumNames, [responseBody['id'] as String]);
|
||||
}
|
||||
}
|
||||
} on http.CancelledException {
|
||||
@@ -459,10 +433,7 @@ class BackupService {
|
||||
if (livePhotoVideoFile == null) {
|
||||
return null;
|
||||
}
|
||||
final livePhotoTitle = p.setExtension(
|
||||
originalFileName,
|
||||
p.extension(livePhotoVideoFile.path),
|
||||
);
|
||||
final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoVideoFile.path));
|
||||
final fileStream = livePhotoVideoFile.openRead();
|
||||
final livePhotoRawUploadData = http.MultipartFile(
|
||||
"assetData",
|
||||
@@ -470,49 +441,36 @@ class BackupService {
|
||||
livePhotoVideoFile.lengthSync(),
|
||||
filename: livePhotoTitle,
|
||||
);
|
||||
final livePhotoReq = MultipartRequest(
|
||||
baseRequest.method,
|
||||
baseRequest.url,
|
||||
onProgress: baseRequest.onProgress,
|
||||
)
|
||||
final livePhotoReq = MultipartRequest(baseRequest.method, baseRequest.url, onProgress: baseRequest.onProgress)
|
||||
..headers.addAll(baseRequest.headers)
|
||||
..fields.addAll(baseRequest.fields);
|
||||
|
||||
livePhotoReq.files.add(livePhotoRawUploadData);
|
||||
|
||||
var response = await httpClient.send(
|
||||
livePhotoReq,
|
||||
cancellationToken: cancelToken,
|
||||
);
|
||||
var response = await httpClient.send(livePhotoReq, cancellationToken: cancelToken);
|
||||
|
||||
var responseBody = jsonDecode(await response.stream.bytesToString());
|
||||
|
||||
if (![200, 201].contains(response.statusCode)) {
|
||||
var error = responseBody;
|
||||
|
||||
debugPrint(
|
||||
"Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}",
|
||||
);
|
||||
debugPrint("Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}");
|
||||
}
|
||||
|
||||
return responseBody.containsKey('id') ? responseBody['id'] : null;
|
||||
}
|
||||
|
||||
String _getAssetType(AssetType assetType) => switch (assetType) {
|
||||
AssetType.audio => "AUDIO",
|
||||
AssetType.image => "IMAGE",
|
||||
AssetType.video => "VIDEO",
|
||||
AssetType.other => "OTHER",
|
||||
};
|
||||
AssetType.audio => "AUDIO",
|
||||
AssetType.image => "IMAGE",
|
||||
AssetType.video => "VIDEO",
|
||||
AssetType.other => "OTHER",
|
||||
};
|
||||
}
|
||||
|
||||
class MultipartRequest extends http.MultipartRequest {
|
||||
/// Creates a new [MultipartRequest].
|
||||
MultipartRequest(
|
||||
super.method,
|
||||
super.url, {
|
||||
required this.onProgress,
|
||||
});
|
||||
MultipartRequest(super.method, super.url, {required this.onProgress});
|
||||
|
||||
final void Function(int bytes, int totalBytes) onProgress;
|
||||
|
||||
|
||||
@@ -37,11 +37,7 @@ class BackupVerificationService {
|
||||
/// Returns at most [limit] assets that were backed up without exif
|
||||
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
||||
final owner = _userService.getMyUser().id;
|
||||
final List<Asset> onlyLocal = await _assetRepository.getAll(
|
||||
ownerId: owner,
|
||||
state: AssetState.local,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> onlyLocal = await _assetRepository.getAll(ownerId: owner, state: AssetState.local, limit: limit);
|
||||
final List<Asset> remoteMatches = await _assetRepository.getMatches(
|
||||
assets: onlyLocal,
|
||||
ownerId: owner,
|
||||
@@ -75,41 +71,32 @@ class BackupVerificationService {
|
||||
if (deleteCandidates.length > 10) {
|
||||
// performs 2 checks in parallel for a nice speedup
|
||||
final half = deleteCandidates.length ~/ 2;
|
||||
final lower = compute(
|
||||
_computeSaveToDelete,
|
||||
(
|
||||
deleteCandidates: deleteCandidates.slice(0, half),
|
||||
originals: originals.slice(0, half),
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
final upper = compute(
|
||||
_computeSaveToDelete,
|
||||
(
|
||||
deleteCandidates: deleteCandidates.slice(half),
|
||||
originals: originals.slice(half),
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
final lower = compute(_computeSaveToDelete, (
|
||||
deleteCandidates: deleteCandidates.slice(0, half),
|
||||
originals: originals.slice(0, half),
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
));
|
||||
final upper = compute(_computeSaveToDelete, (
|
||||
deleteCandidates: deleteCandidates.slice(half),
|
||||
originals: originals.slice(half),
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
));
|
||||
toDelete = await lower + await upper;
|
||||
} else {
|
||||
toDelete = await compute(
|
||||
_computeSaveToDelete,
|
||||
(
|
||||
deleteCandidates: deleteCandidates,
|
||||
originals: originals,
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
),
|
||||
);
|
||||
toDelete = await compute(_computeSaveToDelete, (
|
||||
deleteCandidates: deleteCandidates,
|
||||
originals: originals,
|
||||
auth: Store.get(StoreKey.accessToken),
|
||||
endpoint: Store.get(StoreKey.serverEndpoint),
|
||||
rootIsolateToken: isolateToken,
|
||||
fileMediaRepository: _fileMediaRepository,
|
||||
));
|
||||
}
|
||||
return toDelete;
|
||||
}
|
||||
@@ -122,7 +109,8 @@ class BackupVerificationService {
|
||||
String endpoint,
|
||||
RootIsolateToken rootIsolateToken,
|
||||
FileMediaRepository fileMediaRepository,
|
||||
}) tuple,
|
||||
})
|
||||
tuple,
|
||||
) async {
|
||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||
final List<Asset> result = [];
|
||||
@@ -134,22 +122,14 @@ class BackupVerificationService {
|
||||
apiService.setEndpoint(tuple.endpoint);
|
||||
apiService.setAccessToken(tuple.auth);
|
||||
for (int i = 0; i < tuple.deleteCandidates.length; i++) {
|
||||
if (await _compareAssets(
|
||||
tuple.deleteCandidates[i],
|
||||
tuple.originals[i],
|
||||
apiService,
|
||||
)) {
|
||||
if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) {
|
||||
result.add(tuple.deleteCandidates[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<bool> _compareAssets(
|
||||
Asset remote,
|
||||
Asset local,
|
||||
ApiService apiService,
|
||||
) async {
|
||||
static Future<bool> _compareAssets(Asset remote, Asset local, ApiService apiService) async {
|
||||
if (remote.checksum == local.checksum) return false;
|
||||
ExifInfo? exif = remote.exifInfo;
|
||||
if (exif != null && exif.latitude != null) return false;
|
||||
@@ -169,10 +149,7 @@ class BackupVerificationService {
|
||||
latLng.latitude != null &&
|
||||
(remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) ||
|
||||
remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) ||
|
||||
_sameExceptTimeZone(
|
||||
remote.fileCreatedAt,
|
||||
local.fileCreatedAt,
|
||||
))) {
|
||||
_sameExceptTimeZone(remote.fileCreatedAt, local.fileCreatedAt))) {
|
||||
if (remote.type == AssetType.video) {
|
||||
// it's very unlikely that a video of same length, filesize, name
|
||||
// and date is wrong match. Cannot easily compare videos anyway
|
||||
|
||||
@@ -66,7 +66,6 @@ class DeepLinkService {
|
||||
return DeepLink([
|
||||
// we need something to segue back to if the app was cold started
|
||||
// TODO: use MainTimelineRoute this when beta is default
|
||||
|
||||
if (isColdStart) (Store.isBetaTimelineEnabled) ? const MainTimelineRoute() : const PhotosRoute(),
|
||||
route,
|
||||
]);
|
||||
@@ -96,10 +95,7 @@ class DeepLinkService {
|
||||
return _handleColdStart(deepLinkRoute, isColdStart);
|
||||
}
|
||||
|
||||
Future<DeepLink> handleMyImmichApp(
|
||||
PlatformDeepLink link,
|
||||
bool isColdStart,
|
||||
) async {
|
||||
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, bool isColdStart) async {
|
||||
final path = link.uri.path;
|
||||
|
||||
const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
|
||||
@@ -152,10 +148,7 @@ class DeepLinkService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AssetViewerRoute(
|
||||
initialIndex: 0,
|
||||
timelineService: _betaTimelineFactory.fromAssets([asset]),
|
||||
);
|
||||
return AssetViewerRoute(initialIndex: 0, timelineService: _betaTimelineFactory.fromAssets([asset]));
|
||||
} else {
|
||||
// TODO: Remove this when beta is default
|
||||
final asset = await _assetService.getAssetByRemoteId(assetId);
|
||||
@@ -166,12 +159,7 @@ class DeepLinkService {
|
||||
_currentAsset.set(asset);
|
||||
final renderList = await RenderList.fromAssets([asset], GroupAssetsBy.auto);
|
||||
|
||||
return GalleryViewerRoute(
|
||||
renderList: renderList,
|
||||
initialIndex: 0,
|
||||
heroOffset: 0,
|
||||
showStack: true,
|
||||
);
|
||||
return GalleryViewerRoute(renderList: renderList, initialIndex: 0, heroOffset: 0, showStack: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final downloadServiceProvider = Provider(
|
||||
(ref) => DownloadService(
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(downloadRepositoryProvider),
|
||||
),
|
||||
(ref) => DownloadService(ref.watch(fileMediaRepositoryProvider), ref.watch(downloadRepositoryProvider)),
|
||||
);
|
||||
|
||||
class DownloadService {
|
||||
@@ -29,10 +26,7 @@ class DownloadService {
|
||||
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
|
||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||
|
||||
DownloadService(
|
||||
this._fileMediaRepository,
|
||||
this._downloadRepository,
|
||||
) {
|
||||
DownloadService(this._fileMediaRepository, this._downloadRepository) {
|
||||
_downloadRepository.onImageDownloadStatus = _onImageDownloadCallback;
|
||||
_downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback;
|
||||
_downloadRepository.onLivePhotoDownloadStatus = _onLivePhotoDownloadCallback;
|
||||
@@ -82,11 +76,7 @@ class DownloadService {
|
||||
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||
final file = File(filePath);
|
||||
try {
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveVideo(
|
||||
file,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
final Asset? resultAsset = await _fileMediaRepository.saveVideo(file, title: title, relativePath: relativePath);
|
||||
return resultAsset != null;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving video", error, stack);
|
||||
@@ -98,10 +88,7 @@ class DownloadService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveLivePhotos(
|
||||
Task task,
|
||||
String livePhotosId,
|
||||
) async {
|
||||
Future<bool> saveLivePhotos(Task task, String livePhotosId) async {
|
||||
final records = await _downloadRepository.getLiveVideoTasks();
|
||||
if (records.length < 2) {
|
||||
return false;
|
||||
@@ -142,10 +129,7 @@ class DownloadService {
|
||||
await videoFile.delete();
|
||||
}
|
||||
|
||||
await _downloadRepository.deleteRecordsWithIds([
|
||||
imageRecord.task.taskId,
|
||||
videoRecord.task.taskId,
|
||||
]);
|
||||
await _downloadRepository.deleteRecordsWithIds([imageRecord.task.taskId, videoRecord.task.taskId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,19 +153,13 @@ class DownloadService {
|
||||
asset.remoteId!,
|
||||
asset.fileName,
|
||||
group: kDownloadGroupLivePhoto,
|
||||
metadata: LivePhotosMetadata(
|
||||
part: LivePhotosPart.image,
|
||||
id: asset.remoteId!,
|
||||
).toJson(),
|
||||
metadata: LivePhotosMetadata(part: LivePhotosPart.image, id: asset.remoteId!).toJson(),
|
||||
),
|
||||
_buildDownloadTask(
|
||||
asset.livePhotoVideoId!,
|
||||
asset.fileName.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
|
||||
group: kDownloadGroupLivePhoto,
|
||||
metadata: LivePhotosMetadata(
|
||||
part: LivePhotosPart.video,
|
||||
id: asset.remoteId!,
|
||||
).toJson(),
|
||||
metadata: LivePhotosMetadata(part: LivePhotosPart.video, id: asset.remoteId!).toJson(),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -199,12 +177,7 @@ class DownloadService {
|
||||
];
|
||||
}
|
||||
|
||||
DownloadTask _buildDownloadTask(
|
||||
String id,
|
||||
String filename, {
|
||||
String? group,
|
||||
String? metadata,
|
||||
}) {
|
||||
DownloadTask _buildDownloadTask(String id, String filename, {String? group, String? metadata}) {
|
||||
final path = r'/assets/{id}/original'.replaceAll('{id}', id);
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
@@ -221,11 +194,7 @@ class DownloadService {
|
||||
}
|
||||
}
|
||||
|
||||
TaskRecord _findTaskRecord(
|
||||
List<TaskRecord> records,
|
||||
String livePhotosId,
|
||||
LivePhotosPart part,
|
||||
) {
|
||||
TaskRecord _findTaskRecord(List<TaskRecord> records, String livePhotosId, LivePhotosPart part) {
|
||||
return records.firstWhere((record) {
|
||||
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
|
||||
return metadata.id == livePhotosId && metadata.part == part;
|
||||
|
||||
@@ -8,10 +8,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
class EntityService {
|
||||
final AssetRepository _assetRepository;
|
||||
final IsarUserRepository _isarUserRepository;
|
||||
const EntityService(
|
||||
this._assetRepository,
|
||||
this._isarUserRepository,
|
||||
);
|
||||
const EntityService(this._assetRepository, this._isarUserRepository);
|
||||
|
||||
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
|
||||
final ownerId = album.ownerId;
|
||||
@@ -43,8 +40,5 @@ class EntityService {
|
||||
}
|
||||
|
||||
final entityServiceProvider = Provider(
|
||||
(ref) => EntityService(
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
),
|
||||
(ref) => EntityService(ref.watch(assetRepositoryProvider), ref.watch(userRepositoryProvider)),
|
||||
);
|
||||
|
||||
@@ -6,9 +6,7 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
import 'package:immich_mobile/repositories/folder_api.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final folderServiceProvider = Provider(
|
||||
(ref) => FolderService(ref.watch(folderApiRepositoryProvider)),
|
||||
);
|
||||
final folderServiceProvider = Provider((ref) => FolderService(ref.watch(folderApiRepositoryProvider)));
|
||||
|
||||
class FolderService {
|
||||
final FolderApiRepository _folderApiRepository;
|
||||
@@ -44,11 +42,7 @@ class FolderService {
|
||||
|
||||
if (!folderMap[parentPath]!.any((f) => f.name == segments[i])) {
|
||||
folderMap[parentPath]!.add(
|
||||
RecursiveFolder(
|
||||
path: parentPath == '_root_' ? '' : parentPath,
|
||||
name: segments[i],
|
||||
subfolders: [],
|
||||
),
|
||||
RecursiveFolder(path: parentPath == '_root_' ? '' : parentPath, name: segments[i], subfolders: []),
|
||||
);
|
||||
// Sort folders based on order parameter
|
||||
folderMap[parentPath]!.sort(
|
||||
@@ -64,9 +58,7 @@ class FolderService {
|
||||
if (folderMap.containsKey(fullPath)) {
|
||||
folder.subfolders.addAll(folderMap[fullPath]!);
|
||||
// Sort subfolders based on order parameter
|
||||
folder.subfolders.sort(
|
||||
(a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name),
|
||||
);
|
||||
folder.subfolders.sort((a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name));
|
||||
for (var subfolder in folder.subfolders) {
|
||||
attachSubfolders(subfolder);
|
||||
}
|
||||
@@ -75,24 +67,16 @@ class FolderService {
|
||||
|
||||
List<RecursiveFolder> rootSubfolders = folderMap['_root_'] ?? [];
|
||||
// Sort root subfolders based on order parameter
|
||||
rootSubfolders.sort(
|
||||
(a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name),
|
||||
);
|
||||
rootSubfolders.sort((a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name));
|
||||
|
||||
for (var folder in rootSubfolders) {
|
||||
attachSubfolders(folder);
|
||||
}
|
||||
|
||||
return RootFolder(
|
||||
subfolders: rootSubfolders,
|
||||
path: '/',
|
||||
);
|
||||
return RootFolder(subfolders: rootSubfolders, path: '/');
|
||||
}
|
||||
|
||||
Future<List<Asset>> getFolderAssets(
|
||||
RootFolder folder,
|
||||
SortOrder order,
|
||||
) async {
|
||||
Future<List<Asset>> getFolderAssets(RootFolder folder, SortOrder order) async {
|
||||
try {
|
||||
if (folder is RecursiveFolder) {
|
||||
String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}';
|
||||
@@ -110,11 +94,7 @@ class FolderService {
|
||||
final result = await _folderApiRepository.getAssetsForPath('/');
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_log.severe(
|
||||
"Failed to fetch assets for folder ${folder is RecursiveFolder ? folder.name : "root"}",
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
_log.severe("Failed to fetch assets for folder ${folder is RecursiveFolder ? folder.name : "root"}", e, stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,7 @@ class GCastService {
|
||||
|
||||
void Function(CastState)? onCastState;
|
||||
|
||||
GCastService(
|
||||
this._gCastRepository,
|
||||
this._sessionsApiService,
|
||||
this._assetApiRepository,
|
||||
) {
|
||||
GCastService(this._gCastRepository, this._sessionsApiService, this._assetApiRepository) {
|
||||
_gCastRepository.onCastStatus = _onCastStatusCallback;
|
||||
_gCastRepository.onCastMessage = _onCastMessageCallback;
|
||||
}
|
||||
@@ -100,9 +96,7 @@ class GCastService {
|
||||
}
|
||||
|
||||
if (status["media"] != null && status["media"]["duration"] != null) {
|
||||
final duration = Duration(
|
||||
milliseconds: (status["media"]["duration"] * 1000 ?? 0).toInt(),
|
||||
);
|
||||
final duration = Duration(milliseconds: (status["media"]["duration"] * 1000 ?? 0).toInt());
|
||||
onDuration?.call(duration);
|
||||
}
|
||||
|
||||
@@ -170,13 +164,8 @@ class GCastService {
|
||||
}
|
||||
|
||||
final unauthenticatedUrl = asset.isVideo
|
||||
? getPlaybackUrlForRemoteId(
|
||||
asset.id,
|
||||
)
|
||||
: getThumbnailUrlForRemoteId(
|
||||
asset.id,
|
||||
type: AssetMediaSize.fullsize,
|
||||
);
|
||||
? getPlaybackUrlForRemoteId(asset.id)
|
||||
: getThumbnailUrlForRemoteId(asset.id, type: AssetMediaSize.fullsize);
|
||||
|
||||
final authenticatedURL = "$unauthenticatedUrl&sessionKey=${sessionKey?.token}";
|
||||
|
||||
@@ -220,17 +209,11 @@ class GCastService {
|
||||
}
|
||||
|
||||
void play() {
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "PLAY",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {"type": "PLAY", "mediaSessionId": _sessionId});
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "PAUSE",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {"type": "PAUSE", "mediaSessionId": _sessionId});
|
||||
}
|
||||
|
||||
void seekTo(Duration position) {
|
||||
@@ -242,10 +225,7 @@ class GCastService {
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {
|
||||
"type": "STOP",
|
||||
"mediaSessionId": _sessionId,
|
||||
});
|
||||
_gCastRepository.sendMessage(CastSession.kNamespaceMedia, {"type": "STOP", "mediaSessionId": _sessionId});
|
||||
_mediaStatusPollingTimer?.cancel();
|
||||
|
||||
currentAssetId = null;
|
||||
@@ -258,14 +238,13 @@ class GCastService {
|
||||
final dests = await _gCastRepository.listDestinations();
|
||||
|
||||
return dests
|
||||
.map(
|
||||
(device) => (device.extras["fn"] ?? "Google Cast", CastDestinationType.googleCast, device),
|
||||
)
|
||||
.map((device) => (device.extras["fn"] ?? "Google Cast", CastDestinationType.googleCast, device))
|
||||
.where((device) {
|
||||
final caString = device.$3.extras["ca"];
|
||||
final caNumber = int.tryParse(caString ?? "0") ?? 0;
|
||||
final caString = device.$3.extras["ca"];
|
||||
final caNumber = int.tryParse(caString ?? "0") ?? 0;
|
||||
|
||||
return isDisplay(caNumber);
|
||||
}).toList(growable: false);
|
||||
return isDisplay(caNumber);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ class HashService {
|
||||
required BackgroundService backgroundService,
|
||||
this.batchSizeLimit = kBatchHashSizeLimit,
|
||||
this.batchFileLimit = kBatchHashFileLimit,
|
||||
}) : _deviceAssetRepository = deviceAssetRepository,
|
||||
_backgroundService = backgroundService;
|
||||
}) : _deviceAssetRepository = deviceAssetRepository,
|
||||
_backgroundService = backgroundService;
|
||||
|
||||
final IsarDeviceAssetRepository _deviceAssetRepository;
|
||||
final BackgroundService _backgroundService;
|
||||
@@ -33,9 +33,7 @@ class HashService {
|
||||
assets.sort(Asset.compareByLocalId);
|
||||
|
||||
// Get and sort DB entries - guaranteed to be a subset of assets
|
||||
final hashesInDB = await _deviceAssetRepository.getByIds(
|
||||
assets.map((a) => a.localId!).toList(),
|
||||
);
|
||||
final hashesInDB = await _deviceAssetRepository.getByIds(assets.map((a) => a.localId!).toList());
|
||||
hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||
|
||||
int dbIndex = 0;
|
||||
@@ -60,9 +58,7 @@ class HashService {
|
||||
matchingDbEntry.hash.isNotEmpty &&
|
||||
matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) {
|
||||
// Reuse the existing hash
|
||||
hashedAssets.add(
|
||||
asset.copyWith(checksum: base64.encode(matchingDbEntry.hash)),
|
||||
);
|
||||
hashedAssets.add(asset.copyWith(checksum: base64.encode(matchingDbEntry.hash)));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -125,10 +121,7 @@ class HashService {
|
||||
|
||||
/// Processes a batch of files and returns a list of successfully hashed assets after saving
|
||||
/// them in [DeviceAssetToHash] for future retrieval
|
||||
Future<List<Asset>> _processBatch(
|
||||
List<_AssetPath> toBeHashed,
|
||||
List<String> toBeDeleted,
|
||||
) async {
|
||||
Future<List<Asset>> _processBatch(List<_AssetPath> toBeHashed, List<String> toBeDeleted) async {
|
||||
_log.info("Hashing ${toBeHashed.length} files");
|
||||
final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList());
|
||||
assert(
|
||||
@@ -143,13 +136,7 @@ class HashService {
|
||||
final asset = toBeHashed.elementAtOrNull(index)?.asset;
|
||||
if (asset != null && hash?.length == 20) {
|
||||
hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||
toBeAdded.add(
|
||||
DeviceAsset(
|
||||
assetId: asset.localId!,
|
||||
hash: hash,
|
||||
modifiedTime: asset.fileModifiedAt,
|
||||
),
|
||||
);
|
||||
toBeAdded.add(DeviceAsset(assetId: asset.localId!, hash: hash, modifiedTime: asset.fileModifiedAt));
|
||||
} else {
|
||||
_log.warning("Failed to hash file ${asset?.localId ?? '<null>'}");
|
||||
if (asset != null) {
|
||||
|
||||
@@ -2,11 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||
import 'package:immich_mobile/repositories/biometric.repository.dart';
|
||||
|
||||
final localAuthServiceProvider = Provider(
|
||||
(ref) => LocalAuthService(
|
||||
ref.watch(biometricRepositoryProvider),
|
||||
),
|
||||
);
|
||||
final localAuthServiceProvider = Provider((ref) => LocalAuthService(ref.watch(biometricRepositoryProvider)));
|
||||
|
||||
class LocalAuthService {
|
||||
final BiometricRepository _biometricRepository;
|
||||
|
||||
@@ -2,9 +2,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final localFileManagerServiceProvider = Provider<LocalFilesManagerService>(
|
||||
(ref) => const LocalFilesManagerService(),
|
||||
);
|
||||
final localFileManagerServiceProvider = Provider<LocalFilesManagerService>((ref) => const LocalFilesManagerService());
|
||||
|
||||
class LocalFilesManagerService {
|
||||
const LocalFilesManagerService();
|
||||
@@ -22,10 +20,7 @@ class LocalFilesManagerService {
|
||||
|
||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||
try {
|
||||
return await _channel.invokeMethod(
|
||||
'restoreFromTrash',
|
||||
{'fileName': fileName, 'type': type},
|
||||
);
|
||||
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
|
||||
} catch (e, s) {
|
||||
_logger.warning('Error restore file from trash', e, s);
|
||||
return false;
|
||||
|
||||
@@ -6,10 +6,7 @@ import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
final localNotificationService = Provider(
|
||||
(ref) => LocalNotificationService(
|
||||
ref.watch(notificationPermissionProvider),
|
||||
ref,
|
||||
),
|
||||
(ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref),
|
||||
);
|
||||
|
||||
class LocalNotificationService {
|
||||
@@ -46,10 +43,7 @@ class LocalNotificationService {
|
||||
AndroidNotificationDetails androidNotificationDetails,
|
||||
DarwinNotificationDetails iosNotificationDetails,
|
||||
) async {
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
iOS: iosNotificationDetails,
|
||||
);
|
||||
final notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails);
|
||||
|
||||
if (_permissionStatus == PermissionStatus.granted) {
|
||||
await _localNotificationPlugin.show(id, title, body, notificationDetails);
|
||||
@@ -95,20 +89,12 @@ class LocalNotificationService {
|
||||
ongoing: true,
|
||||
actions: (showActions ?? false)
|
||||
? <AndroidNotificationAction>[
|
||||
const AndroidNotificationAction(
|
||||
cancelUploadActionID,
|
||||
'Cancel',
|
||||
showsUserInterface: true,
|
||||
),
|
||||
const AndroidNotificationAction(cancelUploadActionID, 'Cancel', showsUserInterface: true),
|
||||
]
|
||||
: null,
|
||||
)
|
||||
// Non-progress notification
|
||||
: AndroidNotificationDetails(
|
||||
androidChannelID,
|
||||
androidChannelName,
|
||||
playSound: false,
|
||||
);
|
||||
: AndroidNotificationDetails(androidChannelID, androidChannelName, playSound: false);
|
||||
|
||||
final iosNotificationDetails = DarwinNotificationDetails(
|
||||
presentBadge: true,
|
||||
@@ -116,18 +102,10 @@ class LocalNotificationService {
|
||||
presentBanner: presentBanner,
|
||||
);
|
||||
|
||||
return _showOrUpdateNotification(
|
||||
notificationlId,
|
||||
title,
|
||||
body,
|
||||
androidNotificationDetails,
|
||||
iosNotificationDetails,
|
||||
);
|
||||
return _showOrUpdateNotification(notificationlId, title, body, androidNotificationDetails, iosNotificationDetails);
|
||||
}
|
||||
|
||||
void _onDidReceiveForegroundNotificationResponse(
|
||||
NotificationResponse notificationResponse,
|
||||
) {
|
||||
void _onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) {
|
||||
// Handle notification actions
|
||||
switch (notificationResponse.actionId) {
|
||||
case cancelUploadActionID:
|
||||
|
||||
@@ -7,10 +7,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
||||
return MemoryService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
);
|
||||
return MemoryService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider));
|
||||
});
|
||||
|
||||
class MemoryService {
|
||||
@@ -38,17 +35,8 @@ class MemoryService {
|
||||
final dbAssets = await _assetRepository.getAllByRemoteId(memory.assets.map((e) => e.id));
|
||||
final yearsAgo = now.year - memory.data.year;
|
||||
if (dbAssets.isNotEmpty) {
|
||||
final String title = 'years_ago'.t(
|
||||
args: {
|
||||
'years': yearsAgo.toString(),
|
||||
},
|
||||
);
|
||||
memories.add(
|
||||
Memory(
|
||||
title: title,
|
||||
assets: dbAssets,
|
||||
),
|
||||
);
|
||||
final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()});
|
||||
memories.add(Memory(title: title, assets: dbAssets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,16 +60,9 @@ class MemoryService {
|
||||
return null;
|
||||
}
|
||||
final yearsAgo = DateTime.now().year - memoryResponse.data.year;
|
||||
final String title = 'years_ago'.t(
|
||||
args: {
|
||||
'years': yearsAgo.toString(),
|
||||
},
|
||||
);
|
||||
final String title = 'years_ago'.t(args: {'years': yearsAgo.toString()});
|
||||
|
||||
return Memory(
|
||||
title: title,
|
||||
assets: dbAssets,
|
||||
);
|
||||
return Memory(title: title, assets: dbAssets);
|
||||
} catch (error, stack) {
|
||||
log.severe("Cannot get memory with ID: $id", error, stack);
|
||||
return null;
|
||||
|
||||
@@ -3,10 +3,7 @@ import 'package:immich_mobile/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
|
||||
final networkServiceProvider = Provider((ref) {
|
||||
return NetworkService(
|
||||
ref.watch(networkRepositoryProvider),
|
||||
ref.watch(permissionRepositoryProvider),
|
||||
);
|
||||
return NetworkService(ref.watch(networkRepositoryProvider), ref.watch(permissionRepositoryProvider));
|
||||
});
|
||||
|
||||
class NetworkService {
|
||||
|
||||
@@ -11,24 +11,14 @@ class OAuthService {
|
||||
final log = Logger('OAuthService');
|
||||
OAuthService(this._apiService);
|
||||
|
||||
Future<String?> getOAuthServerUrl(
|
||||
String serverUrl,
|
||||
String state,
|
||||
String codeChallenge,
|
||||
) async {
|
||||
Future<String?> getOAuthServerUrl(String serverUrl, String state, String codeChallenge) async {
|
||||
// Resolve API server endpoint from user provided serverUrl
|
||||
await _apiService.resolveAndSetEndpoint(serverUrl);
|
||||
final redirectUri = '$callbackUrlScheme:///oauth-callback';
|
||||
log.info(
|
||||
"Starting OAuth flow with redirect URI: $redirectUri",
|
||||
);
|
||||
log.info("Starting OAuth flow with redirect URI: $redirectUri");
|
||||
|
||||
final dto = await _apiService.oAuthApi.startOAuth(
|
||||
OAuthConfigDto(
|
||||
redirectUri: redirectUri,
|
||||
state: state,
|
||||
codeChallenge: codeChallenge,
|
||||
),
|
||||
OAuthConfigDto(redirectUri: redirectUri, state: state, codeChallenge: codeChallenge),
|
||||
);
|
||||
|
||||
final authUrl = dto?.url;
|
||||
@@ -37,31 +27,17 @@ class OAuthService {
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
Future<LoginResponseDto?> oAuthLogin(
|
||||
String oauthUrl,
|
||||
String state,
|
||||
String codeVerifier,
|
||||
) async {
|
||||
String result = await FlutterWebAuth2.authenticate(
|
||||
url: oauthUrl,
|
||||
callbackUrlScheme: callbackUrlScheme,
|
||||
);
|
||||
Future<LoginResponseDto?> oAuthLogin(String oauthUrl, String state, String codeVerifier) async {
|
||||
String result = await FlutterWebAuth2.authenticate(url: oauthUrl, callbackUrlScheme: callbackUrlScheme);
|
||||
|
||||
log.info('Received OAuth callback: $result');
|
||||
|
||||
if (result.startsWith('app.immich:/oauth-callback')) {
|
||||
result = result.replaceAll(
|
||||
'app.immich:/oauth-callback',
|
||||
'app.immich:///oauth-callback',
|
||||
);
|
||||
result = result.replaceAll('app.immich:/oauth-callback', 'app.immich:///oauth-callback');
|
||||
}
|
||||
|
||||
return await _apiService.oAuthApi.finishOAuth(
|
||||
OAuthCallbackDto(
|
||||
url: result,
|
||||
state: state,
|
||||
codeVerifier: codeVerifier,
|
||||
),
|
||||
OAuthCallbackDto(url: result, state: state, codeVerifier: codeVerifier),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,7 @@ class PartnerService {
|
||||
final IsarUserRepository _isarUserRepository;
|
||||
final Logger _log = Logger("PartnerService");
|
||||
|
||||
PartnerService(
|
||||
this._partnerApiRepository,
|
||||
this._isarUserRepository,
|
||||
this._partnerRepository,
|
||||
);
|
||||
PartnerService(this._partnerApiRepository, this._isarUserRepository, this._partnerRepository);
|
||||
|
||||
Future<List<UserDto>> getSharedWith() async {
|
||||
return _partnerRepository.getSharedWith();
|
||||
@@ -64,15 +60,9 @@ class PartnerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> updatePartner(
|
||||
UserDto partner, {
|
||||
required bool inTimeline,
|
||||
}) async {
|
||||
Future<bool> updatePartner(UserDto partner, {required bool inTimeline}) async {
|
||||
try {
|
||||
final dto = await _partnerApiRepository.update(
|
||||
partner.id,
|
||||
inTimeline: inTimeline,
|
||||
);
|
||||
final dto = await _partnerApiRepository.update(partner.id, inTimeline: inTimeline);
|
||||
await _isarUserRepository.update(partner.copyWith(inTimeline: dto.inTimeline));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -11,10 +11,10 @@ part 'person.service.g.dart';
|
||||
|
||||
@riverpod
|
||||
PersonService personService(Ref ref) => PersonService(
|
||||
ref.watch(personApiRepositoryProvider),
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.read(assetRepositoryProvider),
|
||||
);
|
||||
ref.watch(personApiRepositoryProvider),
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.read(assetRepositoryProvider),
|
||||
);
|
||||
|
||||
class PersonService {
|
||||
final Logger _log = Logger("PersonService");
|
||||
@@ -22,11 +22,7 @@ class PersonService {
|
||||
final AssetApiRepository _assetApiRepository;
|
||||
final AssetRepository _assetRepository;
|
||||
|
||||
PersonService(
|
||||
this._personApiRepository,
|
||||
this._assetApiRepository,
|
||||
this._assetRepository,
|
||||
);
|
||||
PersonService(this._personApiRepository, this._assetApiRepository, this._assetRepository);
|
||||
|
||||
Future<List<PersonDto>> getAllPeople() async {
|
||||
try {
|
||||
|
||||
@@ -25,11 +25,7 @@ class SearchService {
|
||||
final SearchApiRepository _searchApiRepository;
|
||||
|
||||
final _log = Logger("SearchService");
|
||||
SearchService(
|
||||
this._apiService,
|
||||
this._assetRepository,
|
||||
this._searchApiRepository,
|
||||
);
|
||||
SearchService(this._apiService, this._assetRepository, this._searchApiRepository);
|
||||
|
||||
Future<List<String>?> getSearchSuggestions(
|
||||
SearchSuggestionType type, {
|
||||
|
||||
@@ -2,9 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/repositories/secure_storage.repository.dart';
|
||||
|
||||
final secureStorageServiceProvider = Provider(
|
||||
(ref) => SecureStorageService(
|
||||
ref.watch(secureStorageRepositoryProvider),
|
||||
),
|
||||
(ref) => SecureStorageService(ref.watch(secureStorageRepositoryProvider)),
|
||||
);
|
||||
|
||||
class SecureStorageService {
|
||||
|
||||
@@ -7,11 +7,7 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
|
||||
final serverInfoServiceProvider = Provider(
|
||||
(ref) => ServerInfoService(
|
||||
ref.watch(apiServiceProvider),
|
||||
),
|
||||
);
|
||||
final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider)));
|
||||
|
||||
class ServerInfoService {
|
||||
final ApiService _apiService;
|
||||
|
||||
@@ -39,10 +39,7 @@ class ShareService {
|
||||
final res = await _apiService.assetsApi.downloadAssetWithHttpInfo(asset.remoteId!);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe(
|
||||
"Asset download for ${asset.fileName} failed",
|
||||
res.toLoggerString(),
|
||||
);
|
||||
_log.severe("Asset download for ${asset.fileName} failed", res.toLoggerString());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -57,18 +54,13 @@ class ShareService {
|
||||
}
|
||||
|
||||
if (downloadedXFiles.length != assets.length) {
|
||||
_log.warning(
|
||||
"Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}",
|
||||
);
|
||||
_log.warning("Partial share - Requested: ${assets.length}, Sharing: ${downloadedXFiles.length}");
|
||||
}
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
Share.shareXFiles(
|
||||
downloadedXFiles,
|
||||
sharePositionOrigin: Rect.fromPoints(
|
||||
Offset.zero,
|
||||
Offset(size.width / 3, size.height),
|
||||
),
|
||||
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,19 +2,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:immich_mobile/repositories/share_handler.repository.dart';
|
||||
|
||||
final shareIntentServiceProvider = Provider(
|
||||
(ref) => ShareIntentService(
|
||||
ref.watch(shareHandlerRepositoryProvider),
|
||||
),
|
||||
);
|
||||
final shareIntentServiceProvider = Provider((ref) => ShareIntentService(ref.watch(shareHandlerRepositoryProvider)));
|
||||
|
||||
class ShareIntentService {
|
||||
final ShareHandlerRepository shareHandlerRepository;
|
||||
void Function(List<ShareIntentAttachment> attachments)? onSharedMedia;
|
||||
|
||||
ShareIntentService(
|
||||
this.shareHandlerRepository,
|
||||
);
|
||||
ShareIntentService(this.shareHandlerRepository);
|
||||
|
||||
void init() {
|
||||
shareHandlerRepository.onSharedMedia = onSharedMedia;
|
||||
|
||||
@@ -5,9 +5,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final sharedLinkServiceProvider = Provider(
|
||||
(ref) => SharedLinkService(ref.watch(apiServiceProvider)),
|
||||
);
|
||||
final sharedLinkServiceProvider = Provider((ref) => SharedLinkService(ref.watch(apiServiceProvider)));
|
||||
|
||||
class SharedLinkService {
|
||||
final ApiService _apiService;
|
||||
|
||||
@@ -23,24 +23,16 @@ class StackService {
|
||||
|
||||
Future<StackResponseDto?> createStack(List<String> assetIds) async {
|
||||
try {
|
||||
return _api.stacksApi.createStack(
|
||||
StackCreateDto(assetIds: assetIds),
|
||||
);
|
||||
return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds));
|
||||
} catch (error) {
|
||||
debugPrint("Error while creating stack: $error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<StackResponseDto?> updateStack(
|
||||
String stackId,
|
||||
String primaryAssetId,
|
||||
) async {
|
||||
Future<StackResponseDto?> updateStack(String stackId, String primaryAssetId) async {
|
||||
try {
|
||||
return await _api.stacksApi.updateStack(
|
||||
stackId,
|
||||
StackUpdateDto(primaryAssetId: primaryAssetId),
|
||||
);
|
||||
return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId));
|
||||
} catch (error) {
|
||||
debugPrint("Error while updating stack children: $error");
|
||||
}
|
||||
@@ -68,8 +60,5 @@ class StackService {
|
||||
}
|
||||
|
||||
final stackServiceProvider = Provider(
|
||||
(ref) => StackService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
),
|
||||
(ref) => StackService(ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider)),
|
||||
);
|
||||
|
||||
@@ -100,38 +100,26 @@ class SyncService {
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAssetsToDb({
|
||||
required List<UserDto> users,
|
||||
required Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
List<UserDto> users,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
required Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(List<UserDto> users, DateTime since)
|
||||
getChangedAssets,
|
||||
required FutureOr<List<Asset>?> Function(UserDto user, DateTime until) loadAssets,
|
||||
}) =>
|
||||
_lock.run(
|
||||
() async =>
|
||||
await _syncRemoteAssetChanges(users, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(getUsersFromServer, loadAssets),
|
||||
);
|
||||
}) => _lock.run(
|
||||
() async =>
|
||||
await _syncRemoteAssetChanges(users, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(getUsersFromServer, loadAssets),
|
||||
);
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAlbumsToDb(
|
||||
List<Album> remote,
|
||||
) =>
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote));
|
||||
Future<bool> syncRemoteAlbumsToDb(List<Album> remote) => _lock.run(() => _syncRemoteAlbumsToDb(remote));
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncLocalAlbumAssetsToDb(
|
||||
List<Album> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) =>
|
||||
Future<bool> syncLocalAlbumAssetsToDb(List<Album> onDevice, [Set<String>? excludedAssets]) =>
|
||||
_lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
|
||||
|
||||
/// returns all Asset IDs that are not contained in the existing list
|
||||
List<int> sharedAssetsToRemove(
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing,
|
||||
) {
|
||||
List<int> sharedAssetsToRemove(List<Asset> deleteCandidates, List<Asset> existing) {
|
||||
if (deleteCandidates.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
@@ -200,10 +188,8 @@ class SyncService {
|
||||
/// Efficiently syncs assets via changes. Returns `null` when a full sync is required.
|
||||
Future<bool?> _syncRemoteAssetChanges(
|
||||
List<UserDto> users,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
List<UserDto> users,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(List<UserDto> users, DateTime since)
|
||||
getChangedAssets,
|
||||
) async {
|
||||
final currentUser = _userService.getMyUser();
|
||||
final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc();
|
||||
@@ -237,9 +223,7 @@ class SyncService {
|
||||
final List<Asset> localAssets = await _assetRepository.getAllLocal();
|
||||
final List<Asset> matchedAssets = localAssets.where((asset) => idsToDelete.contains(asset.remoteId)).toList();
|
||||
|
||||
final mediaUrls = await Future.wait(
|
||||
matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)),
|
||||
);
|
||||
final mediaUrls = await Future.wait(matchedAssets.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)));
|
||||
|
||||
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
}
|
||||
@@ -247,18 +231,9 @@ class SyncService {
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) async {
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.remote,
|
||||
);
|
||||
final merged = await _assetRepository.getAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
await _assetRepository.deleteAllByRemoteId(idsToDelete, state: AssetState.remote);
|
||||
final merged = await _assetRepository.getAllByRemoteId(idsToDelete, state: AssetState.merged);
|
||||
if (Platform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) {
|
||||
await _moveToTrashMatchedAssets(idsToDelete);
|
||||
}
|
||||
if (merged.isEmpty) return;
|
||||
@@ -304,10 +279,7 @@ class SyncService {
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> inDb = await _assetRepository.getAll(
|
||||
ownerId: user.id,
|
||||
sortBy: AssetSort.checksum,
|
||||
);
|
||||
final List<Asset> inDb = await _assetRepository.getAll(ownerId: user.id, sortBy: AssetSort.checksum);
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
|
||||
remote.sort(Asset.compareByChecksum);
|
||||
@@ -343,15 +315,10 @@ class SyncService {
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> _syncRemoteAlbumsToDb(
|
||||
List<Album> remoteAlbums,
|
||||
) async {
|
||||
Future<bool> _syncRemoteAlbumsToDb(List<Album> remoteAlbums) async {
|
||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||
|
||||
final List<Album> dbAlbums = await _albumRepository.getAll(
|
||||
remote: true,
|
||||
sortBy: AlbumSort.remoteId,
|
||||
);
|
||||
final List<Album> dbAlbums = await _albumRepository.getAll(remote: true, sortBy: AlbumSort.remoteId);
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
final List<Asset> existing = [];
|
||||
@@ -379,12 +346,7 @@ class SyncService {
|
||||
/// syncs albums from the server to the local database (does not support
|
||||
/// syncing changes from local back to server)
|
||||
/// accumulates
|
||||
Future<bool> _syncRemoteAlbum(
|
||||
Album dto,
|
||||
Album album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing,
|
||||
) async {
|
||||
Future<bool> _syncRemoteAlbum(Album dto, Album album, List<Asset> deleteCandidates, List<Asset> existing) async {
|
||||
if (!_hasRemoteAlbumChanged(dto, album)) {
|
||||
return false;
|
||||
}
|
||||
@@ -393,18 +355,11 @@ class SyncService {
|
||||
final originalDto = dto;
|
||||
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||
|
||||
final assetsInDb = await _assetRepository.getByAlbum(
|
||||
album,
|
||||
sortBy: AssetSort.ownerIdChecksum,
|
||||
);
|
||||
final assetsInDb = await _assetRepository.getByAlbum(album, sortBy: AssetSort.ownerIdChecksum);
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
assetsOnRemote,
|
||||
assetsInDb,
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb, compare: Asset.compareByOwnerChecksum);
|
||||
|
||||
// update shared users
|
||||
final List<UserDto> sharedUsers = album.sharedUsers.map((u) => u.toDto()).toList(growable: false);
|
||||
@@ -476,10 +431,7 @@ class SyncService {
|
||||
/// Adds a remote album to the database while making sure to add any foreign
|
||||
/// (shared) assets to the database beforehand
|
||||
/// accumulates assets already existing in the database
|
||||
Future<void> _addAlbumFromServer(
|
||||
Album album,
|
||||
List<Asset> existing,
|
||||
) async {
|
||||
Future<void> _addAlbumFromServer(Album album, List<Asset> existing) async {
|
||||
if (album.remoteAssetCount != album.remoteAssets.length) {
|
||||
album = await _albumApiRepository.get(album.remoteId!);
|
||||
}
|
||||
@@ -493,23 +445,20 @@ class SyncService {
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _albumRepository.create(album);
|
||||
} else {
|
||||
_log.warning("Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
"asset array length ${album.remoteAssets.length} for album ${album.name}");
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
"asset array length ${album.remoteAssets.length} for album ${album.name}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulates all suitable album assets to the `deleteCandidates` and
|
||||
/// removes the album from the database.
|
||||
Future<void> _removeAlbumFromDb(
|
||||
Album album,
|
||||
List<Asset> deleteCandidates,
|
||||
) async {
|
||||
Future<void> _removeAlbumFromDb(Album 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 _assetRepository.getByAlbum(album, state: AssetState.local),
|
||||
);
|
||||
deleteCandidates.addAll(await _assetRepository.getByAlbum(album, state: AssetState.local));
|
||||
} else if (album.shared) {
|
||||
// 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 _getAllAccessibleUsers()).map((user) => user.id);
|
||||
@@ -526,10 +475,7 @@ class SyncService {
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> _syncLocalAlbumAssetsToDb(
|
||||
List<Album> onDevice, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
Future<bool> _syncLocalAlbumAssetsToDb(List<Album> onDevice, [Set<String>? excludedAssets]) async {
|
||||
onDevice.sort((a, b) => a.localId!.compareTo(b.localId!));
|
||||
final inDb = await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId);
|
||||
final List<Asset> deleteCandidates = [];
|
||||
@@ -538,31 +484,19 @@ class SyncService {
|
||||
onDevice,
|
||||
inDb,
|
||||
compare: (Album a, Album b) => a.localId!.compareTo(b.localId!),
|
||||
both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(
|
||||
a,
|
||||
b,
|
||||
deleteCandidates,
|
||||
existing,
|
||||
excludedAssets,
|
||||
),
|
||||
both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(a, b, deleteCandidates, existing, excludedAssets),
|
||||
onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets),
|
||||
onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
|
||||
);
|
||||
_log.fine(
|
||||
"Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
|
||||
);
|
||||
_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",
|
||||
);
|
||||
_log.fine("${toDelete.length} assets to delete, ${toUpdate.length} to update");
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteByIds(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
|
||||
);
|
||||
_log.info("Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB");
|
||||
}
|
||||
return anyChanges;
|
||||
}
|
||||
@@ -580,9 +514,7 @@ class SyncService {
|
||||
]) async {
|
||||
_log.info("Syncing a local album to DB: ${deviceAlbum.name}");
|
||||
if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||
);
|
||||
_log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync.");
|
||||
return false;
|
||||
}
|
||||
_log.info("Local album ${deviceAlbum.name} has changed. Syncing...");
|
||||
@@ -599,10 +531,7 @@ class SyncService {
|
||||
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final List<Asset> onDevice = await _getHashedAssets(
|
||||
deviceAlbum,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
final List<Asset> onDevice = await _getHashedAssets(deviceAlbum, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(onDevice);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||
@@ -613,16 +542,9 @@ class SyncService {
|
||||
dbAlbum.description == deviceAlbum.description &&
|
||||
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||
// changes only affeted excluded albums
|
||||
_log.info(
|
||||
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
|
||||
);
|
||||
_log.info("Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.");
|
||||
if (assetCountOnDevice != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount) {
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
]);
|
||||
await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -648,12 +570,7 @@ class SyncService {
|
||||
await _albumRepository.removeAssets(dbAlbum, toDelete);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
]);
|
||||
await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: assetCountOnDevice)]);
|
||||
});
|
||||
_log.info("Synced changes of local album ${deviceAlbum.name} to DB");
|
||||
} catch (e) {
|
||||
@@ -667,17 +584,13 @@ class SyncService {
|
||||
/// returns `true` if successful, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
|
||||
if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||
);
|
||||
_log.info("Local album ${deviceAlbum.name} has not changed. Skipping sync.");
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final int lastKnownTotal = (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? 0;
|
||||
if (totalOnDevice <= lastKnownTotal) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync.",
|
||||
);
|
||||
_log.info("Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync.");
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await _getHashedAssets(
|
||||
@@ -701,16 +614,11 @@ class SyncService {
|
||||
await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll(
|
||||
[ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)],
|
||||
);
|
||||
await _eTagRepository.upsertAll([ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)]);
|
||||
});
|
||||
_log.info("Fast synced local album ${deviceAlbum.name} to DB");
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
"Failed to fast sync local album ${deviceAlbum.name} to DB",
|
||||
e,
|
||||
);
|
||||
_log.severe("Failed to fast sync local album ${deviceAlbum.name} to DB", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -719,21 +627,12 @@ class SyncService {
|
||||
|
||||
/// 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(
|
||||
Album album,
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
Future<void> _addAlbumFromDevice(Album album, List<Asset> existing, [Set<String>? excludedAssets]) async {
|
||||
_log.info("Adding a new local album to DB: ${album.name}");
|
||||
final assets = await _getHashedAssets(
|
||||
album,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
final assets = await _getHashedAssets(album, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
|
||||
);
|
||||
_log.info("${existingInDb.length} assets already existed in DB, to upsert ${updated.length}");
|
||||
await upsertAssetsWithExif(updated);
|
||||
existing.addAll(existingInDb);
|
||||
album.assets.addAll(existingInDb);
|
||||
@@ -743,9 +642,7 @@ class SyncService {
|
||||
try {
|
||||
await _albumRepository.create(album);
|
||||
final int assetCount = await _albumMediaRepository.getAssetCount(album.localId!);
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(id: album.eTagKeyAssetCount, assetCount: assetCount),
|
||||
]);
|
||||
await _eTagRepository.upsertAll([ETag(id: album.eTagKeyAssetCount, assetCount: assetCount)]);
|
||||
_log.info("Added a new local album to DB: ${album.name}");
|
||||
} catch (e) {
|
||||
_log.severe("Failed to add new local album ${album.name} to DB", e);
|
||||
@@ -753,9 +650,7 @@ class SyncService {
|
||||
}
|
||||
|
||||
/// Returns a tuple (existing, updated)
|
||||
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
|
||||
List<Asset> assets,
|
||||
) async {
|
||||
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 _assetRepository.getAllByOwnerIdChecksum(
|
||||
@@ -789,17 +684,12 @@ class SyncService {
|
||||
if (asset.isTrashed) {
|
||||
final mediaUrl = await asset.local?.getMediaUrl();
|
||||
if (mediaUrl == null) {
|
||||
_log.warning(
|
||||
"Failed to get media URL for asset ${asset.name} while moving to trash",
|
||||
);
|
||||
_log.warning("Failed to get media URL for asset ${asset.name} while moving to trash");
|
||||
continue;
|
||||
}
|
||||
trashMediaUrls.add(mediaUrl);
|
||||
} else {
|
||||
await _localFilesManager.restoreFromTrash(
|
||||
asset.fileName,
|
||||
asset.type.index,
|
||||
);
|
||||
await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,10 +702,7 @@ class SyncService {
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) return;
|
||||
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
if (Platform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) {
|
||||
_toggleTrashStatusForAssets(assets);
|
||||
}
|
||||
|
||||
@@ -842,21 +729,15 @@ class SyncService {
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
if (!a.isInDb) {
|
||||
_log.warning(
|
||||
"Trying to update an asset that does not exist in DB:\n$a",
|
||||
);
|
||||
_log.warning("Trying to update an asset that does not exist in DB:\n$a");
|
||||
}
|
||||
} else if (a.id != b.id) {
|
||||
_log.warning(
|
||||
"Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a",
|
||||
);
|
||||
_log.warning("Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a");
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < assets.length; i++) {
|
||||
if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) {
|
||||
_log.warning(
|
||||
"Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}",
|
||||
);
|
||||
_log.warning("Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -878,18 +759,16 @@ class SyncService {
|
||||
modifiedFrom: modifiedFrom,
|
||||
modifiedUntil: modifiedUntil,
|
||||
);
|
||||
final filtered =
|
||||
excludedAssets == null ? entities : entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||
final filtered = excludedAssets == null
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||
return _hashService.hashAssets(filtered);
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
assets.uniqueConsecutive(
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
onDuplicate: (a, b) => {},
|
||||
);
|
||||
assets.uniqueConsecutive(compare: Asset.compareByOwnerChecksum, onDuplicate: (a, b) => {});
|
||||
final int duplicates = before - assets.length;
|
||||
if (duplicates > 0) {
|
||||
_log.warning("Ignored $duplicates duplicate assets on device");
|
||||
@@ -898,10 +777,7 @@ class SyncService {
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAlbumChangeOnDevice(
|
||||
Album deviceAlbum,
|
||||
Album dbAlbum,
|
||||
) async {
|
||||
Future<bool> _hasAlbumChangeOnDevice(Album deviceAlbum, Album dbAlbum) async {
|
||||
return deviceAlbum.name != dbAlbum.name ||
|
||||
deviceAlbum.description != dbAlbum.description ||
|
||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
@@ -966,9 +842,7 @@ class SyncService {
|
||||
sharedWith,
|
||||
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
|
||||
both: (UserDto a, UserDto b) {
|
||||
updatedSharedWith.add(
|
||||
a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true),
|
||||
);
|
||||
updatedSharedWith.add(a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true));
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (UserDto a) => updatedSharedWith.add(a),
|
||||
@@ -1065,8 +939,5 @@ bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
|
||||
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
|
||||
!isAtSameMomentAs(
|
||||
remoteAlbum.lastModifiedAssetTimestamp,
|
||||
dbAlbum.lastModifiedAssetTimestamp,
|
||||
);
|
||||
!isAtSameMomentAs(remoteAlbum.lastModifiedAssetTimestamp, dbAlbum.lastModifiedAssetTimestamp);
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ class TimelineService {
|
||||
final AppSettingsService _appSettingsService;
|
||||
final UserService _userService;
|
||||
|
||||
const TimelineService(
|
||||
this._timelineRepository,
|
||||
this._appSettingsService,
|
||||
this._userService,
|
||||
);
|
||||
const TimelineService(this._timelineRepository, this._appSettingsService, this._userService);
|
||||
|
||||
Future<List<String>> getTimelineUserIds() async {
|
||||
final me = _userService.getMyUser();
|
||||
@@ -42,10 +38,7 @@ class TimelineService {
|
||||
}
|
||||
|
||||
Stream<RenderList> watchMultiUsersTimeline(List<String> userIds) {
|
||||
return _timelineRepository.watchMultiUsersTimeline(
|
||||
userIds,
|
||||
_getGroupByOption(),
|
||||
);
|
||||
return _timelineRepository.watchMultiUsersTimeline(userIds, _getGroupByOption());
|
||||
}
|
||||
|
||||
Stream<RenderList> watchArchiveTimeline() async* {
|
||||
@@ -61,10 +54,7 @@ class TimelineService {
|
||||
}
|
||||
|
||||
Stream<RenderList> watchAlbumTimeline(Album album) async* {
|
||||
yield* _timelineRepository.watchAlbumTimeline(
|
||||
album,
|
||||
_getGroupByOption(),
|
||||
);
|
||||
yield* _timelineRepository.watchAlbumTimeline(album, _getGroupByOption());
|
||||
}
|
||||
|
||||
Stream<RenderList> watchTrashTimeline() async* {
|
||||
@@ -79,10 +69,7 @@ class TimelineService {
|
||||
return _timelineRepository.watchAllVideosTimeline(user.id);
|
||||
}
|
||||
|
||||
Future<RenderList> getTimelineFromAssets(
|
||||
List<Asset> assets,
|
||||
GroupAssetsBy? groupBy,
|
||||
) {
|
||||
Future<RenderList> getTimelineFromAssets(List<Asset> assets, GroupAssetsBy? groupBy) {
|
||||
GroupAssetsBy groupOption = GroupAssetsBy.none;
|
||||
if (groupBy == null) {
|
||||
groupOption = _getGroupByOption();
|
||||
@@ -90,10 +77,7 @@ class TimelineService {
|
||||
groupOption = groupBy;
|
||||
}
|
||||
|
||||
return _timelineRepository.getTimelineFromAssets(
|
||||
assets,
|
||||
groupOption,
|
||||
);
|
||||
return _timelineRepository.getTimelineFromAssets(assets, groupOption);
|
||||
}
|
||||
|
||||
Stream<RenderList> watchAssetSelectionTimeline() async* {
|
||||
@@ -109,9 +93,6 @@ class TimelineService {
|
||||
Stream<RenderList> watchLockedTimelineProvider() async* {
|
||||
final user = _userService.getMyUser();
|
||||
|
||||
yield* _timelineRepository.watchLockedTimeline(
|
||||
user.id,
|
||||
_getGroupByOption(),
|
||||
);
|
||||
yield* _timelineRepository.watchLockedTimeline(user.id, _getGroupByOption());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,11 @@ class TrashService {
|
||||
final AssetRepository _assetRepository;
|
||||
final UserService _userService;
|
||||
|
||||
const TrashService(
|
||||
this._apiService,
|
||||
this._assetRepository,
|
||||
this._userService,
|
||||
);
|
||||
const TrashService(this._apiService, this._assetRepository, this._userService);
|
||||
|
||||
Future<void> restoreAssets(Iterable<Asset> assetList) async {
|
||||
final remoteAssets = assetList.where((a) => a.isRemote);
|
||||
await _apiService.trashApi.restoreAssets(
|
||||
BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList()),
|
||||
);
|
||||
await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList()));
|
||||
|
||||
final updatedAssets = remoteAssets.map((asset) {
|
||||
asset.isTrashed = false;
|
||||
@@ -49,15 +43,9 @@ class TrashService {
|
||||
final ids = trashedAssets.map((e) => e.remoteId!).toList();
|
||||
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
ids,
|
||||
state: AssetState.remote,
|
||||
);
|
||||
await _assetRepository.deleteAllByRemoteId(ids, state: AssetState.remote);
|
||||
|
||||
final merged = await _assetRepository.getAllByRemoteId(
|
||||
ids,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
final merged = await _assetRepository.getAllByRemoteId(ids, state: AssetState.merged);
|
||||
if (merged.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,12 +32,7 @@ final uploadServiceProvider = Provider((ref) {
|
||||
});
|
||||
|
||||
class UploadService {
|
||||
UploadService(
|
||||
this._uploadRepository,
|
||||
this._backupRepository,
|
||||
this._storageRepository,
|
||||
this._localAssetRepository,
|
||||
) {
|
||||
UploadService(this._uploadRepository, this._backupRepository, this._storageRepository, this._localAssetRepository) {
|
||||
_uploadRepository.onUploadStatus = _onUploadCallback;
|
||||
_uploadRepository.onTaskProgress = _onTaskProgressCallback;
|
||||
}
|
||||
@@ -114,10 +109,7 @@ class UploadService {
|
||||
/// Find backup candidates
|
||||
/// Build the upload tasks
|
||||
/// Enqueue the tasks
|
||||
Future<void> startBackup(
|
||||
String userId,
|
||||
void Function(EnqueueStatus status) onEnqueueTasks,
|
||||
) async {
|
||||
Future<void> startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async {
|
||||
shouldAbortQueuingTasks = false;
|
||||
|
||||
final candidates = await _backupRepository.getCandidates(userId);
|
||||
@@ -146,12 +138,7 @@ class UploadService {
|
||||
count += tasks.length;
|
||||
enqueueTasks(tasks);
|
||||
|
||||
onEnqueueTasks(
|
||||
EnqueueStatus(
|
||||
enqueueCount: count,
|
||||
totalCount: candidates.length,
|
||||
),
|
||||
);
|
||||
onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,10 +192,7 @@ class UploadService {
|
||||
return;
|
||||
}
|
||||
|
||||
final uploadTask = await _getLivePhotoUploadTask(
|
||||
localAsset,
|
||||
response['id'] as String,
|
||||
);
|
||||
final uploadTask = await _getLivePhotoUploadTask(localAsset, response['id'] as String);
|
||||
|
||||
if (uploadTask == null) {
|
||||
return;
|
||||
@@ -220,11 +204,7 @@ class UploadService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<UploadTask?> _getUploadTask(
|
||||
LocalAsset asset, {
|
||||
String group = kBackupGroup,
|
||||
int? priority,
|
||||
}) async {
|
||||
Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
@@ -252,12 +232,7 @@ class UploadService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final originalFileName = entity.isLivePhoto
|
||||
? p.setExtension(
|
||||
asset.name,
|
||||
p.extension(file.path),
|
||||
)
|
||||
: asset.name;
|
||||
final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
|
||||
|
||||
String metadata = UploadTaskMetadata(
|
||||
localAssetId: asset.id,
|
||||
@@ -275,10 +250,7 @@ class UploadService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<UploadTask?> _getLivePhotoUploadTask(
|
||||
LocalAsset asset,
|
||||
String livePhotoVideoId,
|
||||
) async {
|
||||
Future<UploadTask?> _getLivePhotoUploadTask(LocalAsset asset, String livePhotoVideoId) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
@@ -289,9 +261,7 @@ class UploadService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fields = {
|
||||
'livePhotoVideoId': livePhotoVideoId,
|
||||
};
|
||||
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
||||
|
||||
return buildUploadTask(
|
||||
file,
|
||||
@@ -357,17 +327,9 @@ class UploadTaskMetadata {
|
||||
final bool isLivePhotos;
|
||||
final String livePhotoVideoId;
|
||||
|
||||
const UploadTaskMetadata({
|
||||
required this.localAssetId,
|
||||
required this.isLivePhotos,
|
||||
required this.livePhotoVideoId,
|
||||
});
|
||||
const UploadTaskMetadata({required this.localAssetId, required this.isLivePhotos, required this.livePhotoVideoId});
|
||||
|
||||
UploadTaskMetadata copyWith({
|
||||
String? localAssetId,
|
||||
bool? isLivePhotos,
|
||||
String? livePhotoVideoId,
|
||||
}) {
|
||||
UploadTaskMetadata copyWith({String? localAssetId, bool? isLivePhotos, String? livePhotoVideoId}) {
|
||||
return UploadTaskMetadata(
|
||||
localAssetId: localAssetId ?? this.localAssetId,
|
||||
isLivePhotos: isLivePhotos ?? this.isLivePhotos,
|
||||
|
||||
@@ -3,9 +3,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/repositories/widget.repository.dart';
|
||||
|
||||
final widgetServiceProvider = Provider((ref) {
|
||||
return WidgetService(
|
||||
ref.watch(widgetRepositoryProvider),
|
||||
);
|
||||
return WidgetService(ref.watch(widgetRepositoryProvider));
|
||||
});
|
||||
|
||||
class WidgetService {
|
||||
|
||||
Reference in New Issue
Block a user