mirror of
https://github.com/immich-app/immich.git
synced 2025-01-17 01:06:46 +01:00
refactor(mobile): add AssetState and proper asset updating (#2270)
* refactor(mobile): add AssetState and proper asset updating * generate files --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
b970a40b4e
commit
e80d37bf8f
14 changed files with 254 additions and 96 deletions
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/album/providers/asset_selection.provider.d
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
|
@ -85,11 +86,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
asset.isRemote
|
storageIcon(asset),
|
||||||
? (asset.isLocal
|
|
||||||
? Icons.cloud_done_outlined
|
|
||||||
: Icons.cloud_outlined)
|
|
||||||
: Icons.cloud_off_outlined,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleArchive(Asset asset) async {
|
Future<void> toggleArchive(Asset asset) async {
|
||||||
if (!asset.isRemote) return;
|
if (asset.storage == AssetState.local) return;
|
||||||
|
|
||||||
_setArchiveForAssetId(asset.id, !_isArchive(asset.id));
|
_setArchiveForAssetId(asset.id, !_isArchive(asset.id));
|
||||||
|
|
||||||
|
|
|
@ -65,42 +65,44 @@ class ArchivePage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBottomBar() {
|
Widget buildBottomBar() {
|
||||||
return Align(
|
return SafeArea(
|
||||||
alignment: Alignment.bottomCenter,
|
child: Align(
|
||||||
child: SizedBox(
|
alignment: Alignment.bottomCenter,
|
||||||
height: 64,
|
child: SizedBox(
|
||||||
child: Card(
|
height: 64,
|
||||||
child: Column(
|
child: Card(
|
||||||
children: [
|
child: Column(
|
||||||
ListTile(
|
children: [
|
||||||
shape: RoundedRectangleBorder(
|
ListTile(
|
||||||
borderRadius: BorderRadius.circular(10),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(10),
|
||||||
leading: const Icon(
|
),
|
||||||
Icons.unarchive_rounded,
|
leading: const Icon(
|
||||||
),
|
Icons.unarchive_rounded,
|
||||||
title:
|
),
|
||||||
const Text("Unarchive", style: TextStyle(fontSize: 14)),
|
title:
|
||||||
onTap: () {
|
const Text("Unarchive", style: TextStyle(fontSize: 14)),
|
||||||
if (selection.value.isNotEmpty) {
|
onTap: () {
|
||||||
ref
|
if (selection.value.isNotEmpty) {
|
||||||
.watch(assetProvider.notifier)
|
ref
|
||||||
.toggleArchive(selection.value, false);
|
.watch(assetProvider.notifier)
|
||||||
|
.toggleArchive(selection.value, false);
|
||||||
|
|
||||||
final assetOrAssets =
|
final assetOrAssets =
|
||||||
selection.value.length > 1 ? 'assets' : 'asset';
|
selection.value.length > 1 ? 'assets' : 'asset';
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg:
|
msg:
|
||||||
'Moved ${selection.value.length} $assetOrAssets to library',
|
'Moved ${selection.value.length} $assetOrAssets to library',
|
||||||
gravity: ToastGravity.CENTER,
|
gravity: ToastGravity.CENTER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -26,8 +26,8 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<int>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleFavorite(Asset asset) async {
|
Future<void> toggleFavorite(Asset asset) async {
|
||||||
if (!asset.isRemote) return; // TODO support local favorite assets
|
// TODO support local favorite assets
|
||||||
|
if (asset.storage == AssetState.local) return;
|
||||||
_setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
|
_setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
|
||||||
|
|
||||||
await assetNotifier.toggleFavorite(
|
await assetNotifier.toggleFavorite(
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
|
@ -124,11 +125,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
asset.isRemote
|
storageIcon(asset),
|
||||||
? (asset.isLocal
|
|
||||||
? Icons.cloud_done_outlined
|
|
||||||
: Icons.cloud_outlined)
|
|
||||||
: Icons.cloud_off_outlined,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
|
|
@ -56,6 +56,7 @@ class Asset {
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset({
|
Asset({
|
||||||
|
this.id = Isar.autoIncrement,
|
||||||
this.remoteId,
|
this.remoteId,
|
||||||
required this.localId,
|
required this.localId,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
|
@ -133,6 +134,7 @@ class Asset {
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
|
/// `true` if this [Asset] is present on the device
|
||||||
bool isLocal;
|
bool isLocal;
|
||||||
|
|
||||||
bool isArchived;
|
bool isArchived;
|
||||||
|
@ -146,12 +148,26 @@ class Asset {
|
||||||
@ignore
|
@ignore
|
||||||
String get name => p.withoutExtension(fileName);
|
String get name => p.withoutExtension(fileName);
|
||||||
|
|
||||||
|
/// `true` if this [Asset] is present on the server
|
||||||
@ignore
|
@ignore
|
||||||
bool get isRemote => remoteId != null;
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
AssetState get storage {
|
||||||
|
if (isRemote && isLocal) {
|
||||||
|
return AssetState.merged;
|
||||||
|
} else if (isRemote) {
|
||||||
|
return AssetState.remote;
|
||||||
|
} else if (isLocal) {
|
||||||
|
return AssetState.local;
|
||||||
|
} else {
|
||||||
|
throw Exception("Asset has illegal state: $this");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
Duration get duration => Duration(seconds: durationInSeconds);
|
Duration get duration => Duration(seconds: durationInSeconds);
|
||||||
|
|
||||||
|
@ -198,38 +214,113 @@ class Asset {
|
||||||
isLocal.hashCode ^
|
isLocal.hashCode ^
|
||||||
isArchived.hashCode;
|
isArchived.hashCode;
|
||||||
|
|
||||||
bool updateFromAssetEntity(AssetEntity ae) {
|
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
||||||
// TODO check more fields;
|
bool canUpdate(Asset a) {
|
||||||
// width and height are most important because local assets require these
|
assert(isInDb);
|
||||||
final bool hasChanges =
|
|
||||||
isLocal == false || width != ae.width || height != ae.height;
|
|
||||||
if (hasChanges) {
|
|
||||||
isLocal = true;
|
|
||||||
width = ae.width;
|
|
||||||
height = ae.height;
|
|
||||||
}
|
|
||||||
return hasChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
Asset withUpdatesFromDto(AssetResponseDto dto) =>
|
|
||||||
Asset.remote(dto).updateFromDb(this);
|
|
||||||
|
|
||||||
Asset updateFromDb(Asset a) {
|
|
||||||
assert(localId == a.localId);
|
assert(localId == a.localId);
|
||||||
assert(deviceId == a.deviceId);
|
assert(deviceId == a.deviceId);
|
||||||
id = a.id;
|
assert(a.storage != AssetState.merged);
|
||||||
isLocal |= a.isLocal;
|
return a.updatedAt.isAfter(updatedAt) ||
|
||||||
remoteId ??= a.remoteId;
|
a.isRemote && !isRemote ||
|
||||||
width ??= a.width;
|
a.isLocal && !isLocal ||
|
||||||
height ??= a.height;
|
width == null && a.width != null ||
|
||||||
exifInfo ??= a.exifInfo;
|
height == null && a.height != null ||
|
||||||
exifInfo?.id = id;
|
exifInfo == null && a.exifInfo != null ||
|
||||||
if (!isRemote) {
|
livePhotoVideoId == null && a.livePhotoVideoId != null ||
|
||||||
isArchived = a.isArchived;
|
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
|
||||||
}
|
!isRemote && a.isRemote && isArchived != a.isArchived;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
||||||
|
Asset updatedCopy(Asset a) {
|
||||||
|
assert(canUpdate(a));
|
||||||
|
if (a.updatedAt.isAfter(updatedAt)) {
|
||||||
|
// take most values from newer asset
|
||||||
|
// keep vales that can never be set by the asset not in DB
|
||||||
|
if (a.isRemote) {
|
||||||
|
return a._copyWith(
|
||||||
|
id: id,
|
||||||
|
isLocal: isLocal,
|
||||||
|
width: a.width ?? width,
|
||||||
|
height: a.height ?? height,
|
||||||
|
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return a._copyWith(
|
||||||
|
id: id,
|
||||||
|
remoteId: remoteId,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
isArchived: isArchived,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fill in potentially missing values, i.e. merge assets
|
||||||
|
if (a.isRemote) {
|
||||||
|
// values from remote take precedence
|
||||||
|
return _copyWith(
|
||||||
|
remoteId: a.remoteId,
|
||||||
|
width: a.width,
|
||||||
|
height: a.height,
|
||||||
|
livePhotoVideoId: a.livePhotoVideoId,
|
||||||
|
// isFavorite + isArchived are not set by device-only assets
|
||||||
|
isFavorite: a.isFavorite,
|
||||||
|
isArchived: a.isArchived,
|
||||||
|
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// add only missing values (and set isLocal to true)
|
||||||
|
return _copyWith(
|
||||||
|
isLocal: true,
|
||||||
|
width: width ?? a.width,
|
||||||
|
height: height ?? a.height,
|
||||||
|
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Asset _copyWith({
|
||||||
|
Id? id,
|
||||||
|
String? remoteId,
|
||||||
|
String? localId,
|
||||||
|
int? deviceId,
|
||||||
|
int? ownerId,
|
||||||
|
DateTime? fileCreatedAt,
|
||||||
|
DateTime? fileModifiedAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
int? durationInSeconds,
|
||||||
|
AssetType? type,
|
||||||
|
short? width,
|
||||||
|
short? height,
|
||||||
|
String? fileName,
|
||||||
|
String? livePhotoVideoId,
|
||||||
|
bool? isFavorite,
|
||||||
|
bool? isLocal,
|
||||||
|
bool? isArchived,
|
||||||
|
ExifInfo? exifInfo,
|
||||||
|
}) =>
|
||||||
|
Asset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
remoteId: remoteId ?? this.remoteId,
|
||||||
|
localId: localId ?? this.localId,
|
||||||
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
|
||||||
|
fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
|
type: type ?? this.type,
|
||||||
|
width: width ?? this.width,
|
||||||
|
height: height ?? this.height,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
isLocal: isLocal ?? this.isLocal,
|
||||||
|
isArchived: isArchived ?? this.isArchived,
|
||||||
|
exifInfo: exifInfo ?? this.exifInfo,
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> put(Isar db) async {
|
Future<void> put(Isar db) async {
|
||||||
await db.assets.put(this);
|
await db.assets.put(this);
|
||||||
if (exifInfo != null) {
|
if (exifInfo != null) {
|
||||||
|
@ -311,6 +402,14 @@ extension AssetTypeEnumHelper on AssetTypeEnum {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes where the information of this asset came from:
|
||||||
|
/// only from the local device, only from the remote server or merged from both
|
||||||
|
enum AssetState {
|
||||||
|
local,
|
||||||
|
remote,
|
||||||
|
merged,
|
||||||
|
}
|
||||||
|
|
||||||
extension AssetsHelper on IsarCollection<Asset> {
|
extension AssetsHelper on IsarCollection<Asset> {
|
||||||
Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
|
Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
|
||||||
ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
|
ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
|
||||||
|
|
Binary file not shown.
|
@ -63,6 +63,7 @@ class ExifInfo {
|
||||||
description = dto.description;
|
description = dto.description;
|
||||||
|
|
||||||
ExifInfo({
|
ExifInfo({
|
||||||
|
this.id,
|
||||||
this.fileSize,
|
this.fileSize,
|
||||||
this.make,
|
this.make,
|
||||||
this.model,
|
this.model,
|
||||||
|
@ -78,6 +79,41 @@ class ExifInfo {
|
||||||
this.country,
|
this.country,
|
||||||
this.description,
|
this.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ExifInfo copyWith({
|
||||||
|
Id? id,
|
||||||
|
int? fileSize,
|
||||||
|
String? make,
|
||||||
|
String? model,
|
||||||
|
String? lens,
|
||||||
|
float? f,
|
||||||
|
float? mm,
|
||||||
|
short? iso,
|
||||||
|
float? exposureSeconds,
|
||||||
|
float? lat,
|
||||||
|
float? long,
|
||||||
|
String? city,
|
||||||
|
String? state,
|
||||||
|
String? country,
|
||||||
|
String? description,
|
||||||
|
}) =>
|
||||||
|
ExifInfo(
|
||||||
|
id: id ?? this.id,
|
||||||
|
fileSize: fileSize ?? this.fileSize,
|
||||||
|
make: make ?? this.make,
|
||||||
|
model: model ?? this.model,
|
||||||
|
lens: lens ?? this.lens,
|
||||||
|
f: f ?? this.f,
|
||||||
|
mm: mm ?? this.mm,
|
||||||
|
iso: iso ?? this.iso,
|
||||||
|
exposureSeconds: exposureSeconds ?? this.exposureSeconds,
|
||||||
|
lat: lat ?? this.lat,
|
||||||
|
long: long ?? this.long,
|
||||||
|
city: city ?? this.city,
|
||||||
|
state: state ?? this.state,
|
||||||
|
country: country ?? this.country,
|
||||||
|
description: description ?? this.description,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double? _exposureTimeToSeconds(String? s) {
|
double? _exposureTimeToSeconds(String? s) {
|
||||||
|
|
Binary file not shown.
|
@ -102,8 +102,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
await clearAssetsAndAlbums(_db);
|
await clearAssetsAndAlbums(_db);
|
||||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||||
} else if (_stateUpdateLock.enqueued <= 1) {
|
} else if (_stateUpdateLock.enqueued <= 1) {
|
||||||
final int cachedCount =
|
final int cachedCount = await _userAssetQuery(me.isarId).count();
|
||||||
await _db.assets.filter().ownerIdEqualTo(me.isarId).count();
|
|
||||||
if (cachedCount > 0 && cachedCount != state.allAssets.length) {
|
if (cachedCount > 0 && cachedCount != state.allAssets.length) {
|
||||||
await _stateUpdateLock.run(
|
await _stateUpdateLock.run(
|
||||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
||||||
|
@ -121,8 +120,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
if (!newRemote &&
|
if (!newRemote &&
|
||||||
!newLocal &&
|
!newLocal &&
|
||||||
state.allAssets.length ==
|
state.allAssets.length == await _userAssetQuery(me.isarId).count()) {
|
||||||
await _db.assets.filter().ownerIdEqualTo(me.isarId).count()) {
|
|
||||||
log.info("state is already up-to-date");
|
log.info("state is already up-to-date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -141,12 +139,13 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Asset>> _getUserAssets(int userId) => _db.assets
|
Future<List<Asset>> _getUserAssets(int userId) =>
|
||||||
.filter()
|
_userAssetQuery(userId).sortByFileCreatedAtDesc().findAll();
|
||||||
.ownerIdEqualTo(userId)
|
|
||||||
.isArchivedEqualTo(false)
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> _userAssetQuery(
|
||||||
.sortByFileCreatedAtDesc()
|
int userId,
|
||||||
.findAll();
|
) =>
|
||||||
|
_db.assets.filter().ownerIdEqualTo(userId).isArchivedEqualTo(false);
|
||||||
|
|
||||||
Future<void> clearAllAsset() {
|
Future<void> clearAllAsset() {
|
||||||
state = AssetsState.empty();
|
state = AssetsState.empty();
|
||||||
|
|
|
@ -101,7 +101,7 @@ class AssetService {
|
||||||
if (a.isRemote) {
|
if (a.isRemote) {
|
||||||
final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
|
final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
|
||||||
if (dto != null && dto.exifInfo != null) {
|
if (dto != null && dto.exifInfo != null) {
|
||||||
a = a.withUpdatesFromDto(dto);
|
a.exifInfo = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
|
||||||
if (a.isInDb) {
|
if (a.isInDb) {
|
||||||
_db.writeTxn(() => a.put(_db));
|
_db.writeTxn(() => a.put(_db));
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,7 +122,7 @@ class AssetService {
|
||||||
final dto =
|
final dto =
|
||||||
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
final updated = Asset.remote(dto).updateFromDb(asset);
|
final updated = asset.updatedCopy(Asset.remote(dto));
|
||||||
if (updated.isInDb) {
|
if (updated.isInDb) {
|
||||||
await _db.writeTxn(() => updated.put(_db));
|
await _db.writeTxn(() => updated.put(_db));
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,7 +136,7 @@ class SyncService {
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
// unify local/remote assets by replacing the
|
// unify local/remote assets by replacing the
|
||||||
// local-only asset in the DB with a local&remote asset
|
// local-only asset in the DB with a local&remote asset
|
||||||
newAsset.updateFromDb(match);
|
newAsset = match.updatedCopy(newAsset);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await _db.writeTxn(() => newAsset.put(_db));
|
await _db.writeTxn(() => newAsset.put(_db));
|
||||||
|
@ -581,12 +581,12 @@ class SyncService {
|
||||||
// client and server, thus never reaching "both" case below
|
// client and server, thus never reaching "both" case below
|
||||||
compare: Asset.compareByOwnerDeviceLocalId,
|
compare: Asset.compareByOwnerDeviceLocalId,
|
||||||
both: (Asset a, Asset b) {
|
both: (Asset a, Asset b) {
|
||||||
if ((a.isLocal || !b.isLocal) && (a.isRemote || !b.isRemote)) {
|
if (a.canUpdate(b)) {
|
||||||
|
toUpsert.add(a.updatedCopy(b));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
existing.add(a);
|
existing.add(a);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
toUpsert.add(b.updateFromDb(a));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onlyFirst: (Asset a) => _log.finer(
|
onlyFirst: (Asset a) => _log.finer(
|
||||||
|
@ -637,10 +637,8 @@ Triple<List<Asset>, List<Asset>, List<Asset>> _diffAssets(
|
||||||
assets,
|
assets,
|
||||||
compare: compare,
|
compare: compare,
|
||||||
both: (Asset a, Asset b) {
|
both: (Asset a, Asset b) {
|
||||||
if (a.updatedAt.isBefore(b.updatedAt) ||
|
if (a.canUpdate(b)) {
|
||||||
(!a.isLocal && b.isLocal) ||
|
toUpdate.add(a.updatedCopy(b));
|
||||||
(!a.isRemote && b.isRemote)) {
|
|
||||||
toUpdate.add(b.updateFromDb(a));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
14
mobile/lib/utils/storage_indicator.dart
Normal file
14
mobile/lib/utils/storage_indicator.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
|
/// Returns the suitable [IconData] to represent an [Asset]s storage location
|
||||||
|
IconData storageIcon(Asset asset) {
|
||||||
|
switch (asset.storage) {
|
||||||
|
case AssetState.local:
|
||||||
|
return Icons.cloud_off_outlined;
|
||||||
|
case AssetState.remote:
|
||||||
|
return Icons.cloud_outlined;
|
||||||
|
case AssetState.merged:
|
||||||
|
return Icons.cloud_done_outlined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -242,6 +242,22 @@ class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
|
||||||
returnValueForMissingStub: _i5.Future<bool>.value(false),
|
returnValueForMissingStub: _i5.Future<bool>.value(false),
|
||||||
) as _i5.Future<bool>);
|
) as _i5.Future<bool>);
|
||||||
@override
|
@override
|
||||||
|
_i5.Future<void> toggleArchive(
|
||||||
|
Iterable<_i4.Asset>? assets,
|
||||||
|
bool? status,
|
||||||
|
) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(
|
||||||
|
#toggleArchive,
|
||||||
|
[
|
||||||
|
assets,
|
||||||
|
status,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
returnValue: _i5.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||||
|
) as _i5.Future<void>);
|
||||||
|
@override
|
||||||
bool updateShouldNotify(
|
bool updateShouldNotify(
|
||||||
_i2.AssetsState? old,
|
_i2.AssetsState? old,
|
||||||
_i2.AssetsState? current,
|
_i2.AssetsState? current,
|
||||||
|
|
Loading…
Reference in a new issue