Skip to content

Commit

Permalink
refactor(mobile): add AssetState and proper asset updating (immich-ap…
Browse files Browse the repository at this point in the history
…p#2270)

* refactor(mobile): add AssetState and proper asset updating

* generate files

---------

Co-authored-by: Fynn Petersen-Frey <[email protected]>
Co-authored-by: Alex Tran <[email protected]>
  • Loading branch information
3 people authored Apr 18, 2023
1 parent b970a40 commit e80d37b
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 97 deletions.
7 changes: 2 additions & 5 deletions mobile/lib/modules/album/ui/album_viewer_thumbnail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';

class AlbumViewerThumbnail extends HookConsumerWidget {
final Asset asset;
Expand Down Expand Up @@ -85,11 +86,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
right: 10,
bottom: 5,
child: Icon(
asset.isRemote
? (asset.isLocal
? Icons.cloud_done_outlined
: Icons.cloud_outlined)
: Icons.cloud_off_outlined,
storageIcon(asset),
color: Colors.white,
size: 18,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
}

Future<void> toggleArchive(Asset asset) async {
if (!asset.isRemote) return;
if (asset.storage == AssetState.local) return;

_setArchiveForAssetId(asset.id, !_isArchive(asset.id));

Expand Down
70 changes: 36 additions & 34 deletions mobile/lib/modules/archive/views/archive_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,42 +65,44 @@ class ArchivePage extends HookConsumerWidget {
}

Widget buildBottomBar() {
return Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Card(
child: Column(
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
leading: const Icon(
Icons.unarchive_rounded,
),
title:
const Text("Unarchive", style: TextStyle(fontSize: 14)),
onTap: () {
if (selection.value.isNotEmpty) {
ref
.watch(assetProvider.notifier)
.toggleArchive(selection.value, false);
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Card(
child: Column(
children: [
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
leading: const Icon(
Icons.unarchive_rounded,
),
title:
const Text("Unarchive", style: TextStyle(fontSize: 14)),
onTap: () {
if (selection.value.isNotEmpty) {
ref
.watch(assetProvider.notifier)
.toggleArchive(selection.value, false);

final assetOrAssets =
selection.value.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg:
'Moved ${selection.value.length} $assetOrAssets to library',
gravity: ToastGravity.CENTER,
);
}
final assetOrAssets =
selection.value.length > 1 ? 'assets' : 'asset';
ImmichToast.show(
context: context,
msg:
'Moved ${selection.value.length} $assetOrAssets to library',
gravity: ToastGravity.CENTER,
);
}

selectionEnabledHook.value = false;
},
)
],
selectionEnabledHook.value = false;
},
)
],
),
),
),
),
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/modules/favorite/providers/favorite_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<int>> {
}

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));

await assetNotifier.toggleFavorite(
Expand Down
7 changes: 2 additions & 5 deletions mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';

class ThumbnailImage extends HookConsumerWidget {
final Asset asset;
Expand Down Expand Up @@ -124,11 +125,7 @@ class ThumbnailImage extends HookConsumerWidget {
right: 10,
bottom: 5,
child: Icon(
asset.isRemote
? (asset.isLocal
? Icons.cloud_done_outlined
: Icons.cloud_outlined)
: Icons.cloud_off_outlined,
storageIcon(asset),
color: Colors.white,
size: 18,
),
Expand Down
153 changes: 126 additions & 27 deletions mobile/lib/shared/models/asset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Asset {
}

Asset({
this.id = Isar.autoIncrement,
this.remoteId,
required this.localId,
required this.deviceId,
Expand Down Expand Up @@ -133,6 +134,7 @@ class Asset {

bool isFavorite;

/// `true` if this [Asset] is present on the device
bool isLocal;

bool isArchived;
Expand All @@ -146,12 +148,26 @@ class Asset {
@ignore
String get name => p.withoutExtension(fileName);

/// `true` if this [Asset] is present on the server
@ignore
bool get isRemote => remoteId != null;

@ignore
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
Duration get duration => Duration(seconds: durationInSeconds);

Expand Down Expand Up @@ -198,38 +214,113 @@ class Asset {
isLocal.hashCode ^
isArchived.hashCode;

bool updateFromAssetEntity(AssetEntity ae) {
// TODO check more fields;
// width and height are most important because local assets require these
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) {
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
assert(isInDb);
assert(localId == a.localId);
assert(deviceId == a.deviceId);
id = a.id;
isLocal |= a.isLocal;
remoteId ??= a.remoteId;
width ??= a.width;
height ??= a.height;
exifInfo ??= a.exifInfo;
exifInfo?.id = id;
if (!isRemote) {
isArchived = a.isArchived;
assert(a.storage != AssetState.merged);
return a.updatedAt.isAfter(updatedAt) ||
a.isRemote && !isRemote ||
a.isLocal && !isLocal ||
width == null && a.width != null ||
height == null && a.height != null ||
exifInfo == null && a.exifInfo != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
!isRemote && a.isRemote && isFavorite != a.isFavorite ||
!isRemote && a.isRemote && isArchived != a.isArchived;
}

/// 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),
);
}
}
return this;
}

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 {
await db.assets.put(this);
if (exifInfo != null) {
Expand Down Expand Up @@ -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> {
Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/shared/models/asset.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e80d37b

Please sign in to comment.