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:
shenlong
2025-07-29 00:34:03 +05:30
committed by GitHub
parent 9b3718120b
commit e52b9d15b5
643 changed files with 32561 additions and 35292 deletions
+15 -61
View File
@@ -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;
}
+3 -16
View File
@@ -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",
);
}
+32 -111
View File
@@ -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);
}
+3 -13
View File
@@ -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);
+12 -57
View File
@@ -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);
+31 -102
View File
@@ -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;
+1 -4
View File
@@ -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);
}
}
+37 -68
View File
@@ -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;
}
+32 -74
View File
@@ -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
+3 -15
View File
@@ -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);
}
}
+9 -40
View File
@@ -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;
+2 -8
View File
@@ -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)),
);
+7 -27
View File
@@ -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 [];
}
}
+13 -34
View File
@@ -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);
}
}
+6 -19
View File
@@ -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) {
+1 -5
View File
@@ -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:
+5 -24
View File
@@ -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;
+1 -4
View File
@@ -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 {
+7 -31
View File
@@ -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),
);
}
}
+3 -13
View File
@@ -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) {
+5 -9
View File
@@ -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 {
+1 -5
View File
@@ -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 {
+1 -5
View File
@@ -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;
+3 -11
View File
@@ -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;
+1 -3
View File
@@ -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;
+4 -15
View File
@@ -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)),
);
+61 -190
View File
@@ -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);
}
+6 -25
View File
@@ -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());
}
}
+4 -16
View File
@@ -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;
}
+10 -48
View File
@@ -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,
+1 -3
View File
@@ -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 {