0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-02-11 01:18:24 -05:00

fix(mobile): handle readonly and offline assets (#5565)

* feat: add isReadOnly and isOffline fields to Asset collection

* refactor: move asset iterable filters to extension

* hide asset actions based on offline and readOnly fields

* pr changes

* chore: doc comments

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2024-01-06 03:02:16 +00:00 committed by GitHub
parent 56cde0438c
commit a233e176e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 333 additions and 105 deletions

View file

@ -189,6 +189,10 @@
"home_page_building_timeline": "Building the timeline", "home_page_building_timeline": "Building the timeline",
"home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_delete_err_partner": "Can not delete partner assets, skipping",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_share_err_local": "Can not share local assets via link, skipping",

View file

@ -1,6 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
extension ListExtension<E> on List<E> { extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({ List<E> uniqueConsecutive({
@ -39,3 +41,58 @@ extension IntListExtension on Iterable<int> {
return list; return list;
} }
} }
extension AssetListExtension on Iterable<Asset> {
/// Returns the assets that are already available in the Immich server
Iterable<Asset> remoteOnly({
void Function()? errorCallback,
}) {
final bool onlyRemote = every((e) => e.isRemote);
if (!onlyRemote) {
if (errorCallback != null) errorCallback();
return where((a) => a.isRemote);
}
return this;
}
/// Returns the assets that are owned by the user passed to the [owner] param
/// If [owner] is null, an empty list is returned
Iterable<Asset> ownedOnly(
User? owner, {
void Function()? errorCallback,
}) {
if (owner == null) return [];
final userId = owner.isarId;
final bool onlyOwned = every((e) => e.ownerId == userId);
if (!onlyOwned) {
if (errorCallback != null) errorCallback();
return where((a) => a.ownerId == userId);
}
return this;
}
/// Returns the assets that are present on a file system which has write permission
/// This filters out assets on readOnly external library to which we cannot perform any write operation
Iterable<Asset> writableOnly({
void Function()? errorCallback,
}) {
final bool onlyWritable = every((e) => !e.isReadOnly);
if (!onlyWritable) {
if (errorCallback != null) errorCallback();
return where((a) => !a.isReadOnly);
}
return this;
}
/// Filters out offline assets and returns those that are still accessible by the Immich server
Iterable<Asset> nonOfflineOnly({
void Function()? errorCallback,
}) {
final bool onlyLive = every((e) => !e.isOffline);
if (!onlyLive) {
if (errorCallback != null) errorCallback();
return where((a) => !a.isOffline);
}
return this;
}
}

View file

@ -156,7 +156,7 @@ class ExifBottomSheet extends HookConsumerWidget {
buildLocation() { buildLocation() {
// Guard no lat/lng // Guard no lat/lng
if (!hasCoordinates()) { if (!hasCoordinates()) {
return asset.isRemote return asset.isRemote && !asset.isReadOnly
? ListTile( ? ListTile(
minLeadingWidth: 0, minLeadingWidth: 0,
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
@ -194,7 +194,7 @@ class ExifBottomSheet extends HookConsumerWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
).tr(), ).tr(),
if (asset.isRemote) if (asset.isRemote && !asset.isReadOnly)
IconButton( IconButton(
onPressed: () => handleEditLocation( onPressed: () => handleEditLocation(
ref, ref,
@ -251,7 +251,7 @@ class ExifBottomSheet extends HookConsumerWidget {
fontSize: 14, fontSize: 14,
), ),
), ),
if (asset.isRemote) if (asset.isRemote && !asset.isReadOnly)
IconButton( IconButton(
onPressed: () => handleEditDateTime( onPressed: () => handleEditDateTime(
ref, ref,

View file

@ -166,7 +166,8 @@ class TopControlAppBar extends HookConsumerWidget {
if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.isRemote && isOwner) buildFavoriteButton(a),
if (asset.livePhotoVideoId != null) buildLivePhotoButton(), if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && !asset.isLocal && !asset.isOffline && isOwner)
buildDownloadButton(),
if (asset.isRemote && (isOwner || isPartner)) buildAddToAlbumButtom(), if (asset.isRemote && (isOwner || isPartner)) buildAddToAlbumButtom(),
if (album != null && album.shared) buildActivitiesButton(), if (album != null && album.shared) buildActivitiesButton(),
buildMoreInfoButton(), buildMoreInfoButton(),

View file

@ -187,8 +187,8 @@ class GalleryViewerPage extends HookConsumerWidget {
void showInfo() { void showInfo() {
showModalBottomSheet( showModalBottomSheet(
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0), borderRadius: BorderRadius.all(Radius.circular(15.0)),
), ),
barrierColor: Colors.transparent, barrierColor: Colors.transparent,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@ -220,6 +220,16 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
void handleDelete(Asset deleteAsset) async { void handleDelete(Asset deleteAsset) async {
// Cannot delete readOnly / external assets. They are handled through library offline jobs
if (asset().isReadOnly) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_delete_err_read_only'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
Future<bool> onDelete(bool force) async { Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets( final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{deleteAsset}, {deleteAsset},
@ -319,11 +329,20 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
shareAsset() { shareAsset() {
ref.watch(imageViewerStateProvider.notifier).shareAsset(asset(), context); if (asset().isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).shareAsset(asset(), context);
} }
handleArchive(Asset asset) { handleArchive(Asset asset) {
ref.watch(assetProvider.notifier).toggleArchive([asset]); ref.read(assetProvider.notifier).toggleArchive([asset]);
if (isParent) { if (isParent) {
context.popRoute(); context.popRoute();
return; return;
@ -346,6 +365,26 @@ class GalleryViewerPage extends HookConsumerWidget {
); );
} }
handleDownload() {
if (asset().isLocal) {
return;
}
if (asset().isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).downloadAsset(
asset(),
context,
);
}
handleActivities() { handleActivities() {
if (album != null && album.shared && album.remoteId != null) { if (album != null && album.shared && album.remoteId != null) {
context.pushRoute(const ActivitiesRoute()); context.pushRoute(const ActivitiesRoute());
@ -371,12 +410,11 @@ class GalleryViewerPage extends HookConsumerWidget {
asset().isLocal ? () => handleUpload(asset()) : null, asset().isLocal ? () => handleUpload(asset()) : null,
onDownloadPressed: asset().isLocal onDownloadPressed: asset().isLocal
? null ? null
: () => ref : () =>
.watch(imageViewerStateProvider.notifier) ref.read(imageViewerStateProvider.notifier).downloadAsset(
.downloadAsset( asset(),
asset(), context,
context, ),
),
onToggleMotionVideo: (() { onToggleMotionVideo: (() {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value; isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}), }),
@ -641,13 +679,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (isOwner) (_) => handleArchive(asset()), if (isOwner) (_) => handleArchive(asset()),
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
if (isOwner) (_) => handleDelete(asset()), if (isOwner) (_) => handleDelete(asset()),
if (!isOwner) if (!isOwner) (_) => handleDownload(),
(_) => asset().isLocal
? null
: ref.watch(imageViewerStateProvider.notifier).downloadAsset(
asset(),
context,
),
]; ];
return IgnorePointer( return IgnorePointer(

View file

@ -32,6 +32,8 @@ class Asset {
isFavorite = remote.isFavorite, isFavorite = remote.isFavorite,
isArchived = remote.isArchived, isArchived = remote.isArchived,
isTrashed = remote.isTrashed, isTrashed = remote.isTrashed,
isReadOnly = remote.isReadOnly,
isOffline = remote.isOffline,
stackParentId = remote.stackParentId, stackParentId = remote.stackParentId,
stackCount = remote.stackCount; stackCount = remote.stackCount;
@ -49,6 +51,8 @@ class Asset {
isFavorite = local.isFavorite, isFavorite = local.isFavorite,
isArchived = false, isArchived = false,
isTrashed = false, isTrashed = false,
isReadOnly = false,
isOffline = false,
stackCount = 0, stackCount = 0,
fileCreatedAt = local.createDateTime { fileCreatedAt = local.createDateTime {
if (fileCreatedAt.year == 1970) { if (fileCreatedAt.year == 1970) {
@ -77,11 +81,13 @@ class Asset {
required this.fileName, required this.fileName,
this.livePhotoVideoId, this.livePhotoVideoId,
this.exifInfo, this.exifInfo,
required this.isFavorite, this.isFavorite = false,
required this.isArchived, this.isArchived = false,
required this.isTrashed, this.isTrashed = false,
this.stackParentId, this.stackParentId,
required this.stackCount, this.stackCount = 0,
this.isReadOnly = false,
this.isOffline = false,
}); });
@ignore @ignore
@ -148,6 +154,10 @@ class Asset {
bool isTrashed; bool isTrashed;
bool isReadOnly;
bool isOffline;
@ignore @ignore
ExifInfo? exifInfo; ExifInfo? exifInfo;
@ -256,6 +266,8 @@ class Asset {
isFavorite != a.isFavorite || isFavorite != a.isFavorite ||
isArchived != a.isArchived || isArchived != a.isArchived ||
isTrashed != a.isTrashed || isTrashed != a.isTrashed ||
isReadOnly != a.isReadOnly ||
isOffline != a.isOffline ||
a.exifInfo?.latitude != exifInfo?.latitude || a.exifInfo?.latitude != exifInfo?.latitude ||
a.exifInfo?.longitude != exifInfo?.longitude || a.exifInfo?.longitude != exifInfo?.longitude ||
// no local stack count or different count from remote // no local stack count or different count from remote
@ -288,6 +300,7 @@ class Asset {
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id), exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
); );
} else { } else {
// TODO: Revisit this and remove all bool field assignments
return a._copyWith( return a._copyWith(
id: id, id: id,
remoteId: remoteId, remoteId: remoteId,
@ -297,6 +310,8 @@ class Asset {
isFavorite: isFavorite, isFavorite: isFavorite,
isArchived: isArchived, isArchived: isArchived,
isTrashed: isTrashed, isTrashed: isTrashed,
isReadOnly: isReadOnly,
isOffline: isOffline,
); );
} }
} else { } else {
@ -314,6 +329,8 @@ class Asset {
isFavorite: a.isFavorite, isFavorite: a.isFavorite,
isArchived: a.isArchived, isArchived: a.isArchived,
isTrashed: a.isTrashed, isTrashed: a.isTrashed,
isReadOnly: a.isReadOnly,
isOffline: a.isOffline,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo, exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
); );
} else { } else {
@ -346,6 +363,8 @@ class Asset {
bool? isFavorite, bool? isFavorite,
bool? isArchived, bool? isArchived,
bool? isTrashed, bool? isTrashed,
bool? isReadOnly,
bool? isOffline,
ExifInfo? exifInfo, ExifInfo? exifInfo,
String? stackParentId, String? stackParentId,
int? stackCount, int? stackCount,
@ -368,6 +387,8 @@ class Asset {
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
isArchived: isArchived ?? this.isArchived, isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed, isTrashed: isTrashed ?? this.isTrashed,
isReadOnly: isReadOnly ?? this.isReadOnly,
isOffline: isOffline ?? this.isOffline,
exifInfo: exifInfo ?? this.exifInfo, exifInfo: exifInfo ?? this.exifInfo,
stackParentId: stackParentId ?? this.stackParentId, stackParentId: stackParentId ?? this.stackParentId,
stackCount: stackCount ?? this.stackCount, stackCount: stackCount ?? this.stackCount,
@ -426,7 +447,9 @@ class Asset {
"width": ${width ?? "N/A"}, "width": ${width ?? "N/A"},
"height": ${height ?? "N/A"}, "height": ${height ?? "N/A"},
"isArchived": $isArchived, "isArchived": $isArchived,
"isTrashed": $isTrashed "isTrashed": $isTrashed,
"isReadOnly": $isReadOnly,
"isOffline": $isOffline,
}"""; }""";
} }
} }

View file

@ -57,54 +57,64 @@ const AssetSchema = CollectionSchema(
name: r'isFavorite', name: r'isFavorite',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isTrashed': PropertySchema( r'isOffline': PropertySchema(
id: 8, id: 8,
name: r'isOffline',
type: IsarType.bool,
),
r'isReadOnly': PropertySchema(
id: 9,
name: r'isReadOnly',
type: IsarType.bool,
),
r'isTrashed': PropertySchema(
id: 10,
name: r'isTrashed', name: r'isTrashed',
type: IsarType.bool, type: IsarType.bool,
), ),
r'livePhotoVideoId': PropertySchema( r'livePhotoVideoId': PropertySchema(
id: 9, id: 11,
name: r'livePhotoVideoId', name: r'livePhotoVideoId',
type: IsarType.string, type: IsarType.string,
), ),
r'localId': PropertySchema( r'localId': PropertySchema(
id: 10, id: 12,
name: r'localId', name: r'localId',
type: IsarType.string, type: IsarType.string,
), ),
r'ownerId': PropertySchema( r'ownerId': PropertySchema(
id: 11, id: 13,
name: r'ownerId', name: r'ownerId',
type: IsarType.long, type: IsarType.long,
), ),
r'remoteId': PropertySchema( r'remoteId': PropertySchema(
id: 12, id: 14,
name: r'remoteId', name: r'remoteId',
type: IsarType.string, type: IsarType.string,
), ),
r'stackCount': PropertySchema( r'stackCount': PropertySchema(
id: 13, id: 15,
name: r'stackCount', name: r'stackCount',
type: IsarType.long, type: IsarType.long,
), ),
r'stackParentId': PropertySchema( r'stackParentId': PropertySchema(
id: 14, id: 16,
name: r'stackParentId', name: r'stackParentId',
type: IsarType.string, type: IsarType.string,
), ),
r'type': PropertySchema( r'type': PropertySchema(
id: 15, id: 17,
name: r'type', name: r'type',
type: IsarType.byte, type: IsarType.byte,
enumMap: _AssettypeEnumValueMap, enumMap: _AssettypeEnumValueMap,
), ),
r'updatedAt': PropertySchema( r'updatedAt': PropertySchema(
id: 16, id: 18,
name: r'updatedAt', name: r'updatedAt',
type: IsarType.dateTime, type: IsarType.dateTime,
), ),
r'width': PropertySchema( r'width': PropertySchema(
id: 17, id: 19,
name: r'width', name: r'width',
type: IsarType.int, type: IsarType.int,
) )
@ -217,16 +227,18 @@ void _assetSerialize(
writer.writeInt(offsets[5], object.height); writer.writeInt(offsets[5], object.height);
writer.writeBool(offsets[6], object.isArchived); writer.writeBool(offsets[6], object.isArchived);
writer.writeBool(offsets[7], object.isFavorite); writer.writeBool(offsets[7], object.isFavorite);
writer.writeBool(offsets[8], object.isTrashed); writer.writeBool(offsets[8], object.isOffline);
writer.writeString(offsets[9], object.livePhotoVideoId); writer.writeBool(offsets[9], object.isReadOnly);
writer.writeString(offsets[10], object.localId); writer.writeBool(offsets[10], object.isTrashed);
writer.writeLong(offsets[11], object.ownerId); writer.writeString(offsets[11], object.livePhotoVideoId);
writer.writeString(offsets[12], object.remoteId); writer.writeString(offsets[12], object.localId);
writer.writeLong(offsets[13], object.stackCount); writer.writeLong(offsets[13], object.ownerId);
writer.writeString(offsets[14], object.stackParentId); writer.writeString(offsets[14], object.remoteId);
writer.writeByte(offsets[15], object.type.index); writer.writeLong(offsets[15], object.stackCount);
writer.writeDateTime(offsets[16], object.updatedAt); writer.writeString(offsets[16], object.stackParentId);
writer.writeInt(offsets[17], object.width); writer.writeByte(offsets[17], object.type.index);
writer.writeDateTime(offsets[18], object.updatedAt);
writer.writeInt(offsets[19], object.width);
} }
Asset _assetDeserialize( Asset _assetDeserialize(
@ -243,19 +255,21 @@ Asset _assetDeserialize(
fileName: reader.readString(offsets[4]), fileName: reader.readString(offsets[4]),
height: reader.readIntOrNull(offsets[5]), height: reader.readIntOrNull(offsets[5]),
id: id, id: id,
isArchived: reader.readBool(offsets[6]), isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
isFavorite: reader.readBool(offsets[7]), isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
isTrashed: reader.readBool(offsets[8]), isOffline: reader.readBoolOrNull(offsets[8]) ?? false,
livePhotoVideoId: reader.readStringOrNull(offsets[9]), isReadOnly: reader.readBoolOrNull(offsets[9]) ?? false,
localId: reader.readStringOrNull(offsets[10]), isTrashed: reader.readBoolOrNull(offsets[10]) ?? false,
ownerId: reader.readLong(offsets[11]), livePhotoVideoId: reader.readStringOrNull(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]), localId: reader.readStringOrNull(offsets[12]),
stackCount: reader.readLongOrNull(offsets[13]), ownerId: reader.readLong(offsets[13]),
stackParentId: reader.readStringOrNull(offsets[14]), remoteId: reader.readStringOrNull(offsets[14]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[15])] ?? stackCount: reader.readLongOrNull(offsets[15]),
stackParentId: reader.readStringOrNull(offsets[16]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
AssetType.other, AssetType.other,
updatedAt: reader.readDateTime(offsets[16]), updatedAt: reader.readDateTime(offsets[18]),
width: reader.readIntOrNull(offsets[17]), width: reader.readIntOrNull(offsets[19]),
); );
return object; return object;
} }
@ -280,29 +294,33 @@ P _assetDeserializeProp<P>(
case 5: case 5:
return (reader.readIntOrNull(offset)) as P; return (reader.readIntOrNull(offset)) as P;
case 6: case 6:
return (reader.readBool(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 7: case 7:
return (reader.readBool(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 8: case 8:
return (reader.readBool(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 9: case 9:
return (reader.readStringOrNull(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 10: case 10:
return (reader.readStringOrNull(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 11: case 11:
return (reader.readLong(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 12: case 12:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 13: case 13:
return (reader.readLongOrNull(offset)) as P; return (reader.readLong(offset)) as P;
case 14: case 14:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 15: case 15:
return (reader.readLongOrNull(offset)) as P;
case 16:
return (reader.readStringOrNull(offset)) as P;
case 17:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P; AssetType.other) as P;
case 16: case 18:
return (reader.readDateTime(offset)) as P; return (reader.readDateTime(offset)) as P;
case 17: case 19:
return (reader.readIntOrNull(offset)) as P; return (reader.readIntOrNull(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); throw IsarError('Unknown property with id $propertyId');
@ -1323,6 +1341,26 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterFilterCondition> isOfflineEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isOffline',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isReadOnlyEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isReadOnly',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo( QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
bool value) { bool value) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -2316,6 +2354,30 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsReadOnly() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isReadOnly', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsReadOnlyDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isReadOnly', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() { QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc); return query.addSortBy(r'isTrashed', Sort.asc);
@ -2546,6 +2608,30 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsReadOnly() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isReadOnly', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsReadOnlyDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isReadOnly', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() { QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc); return query.addSortBy(r'isTrashed', Sort.asc);
@ -2718,6 +2804,18 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
}); });
} }
QueryBuilder<Asset, Asset, QDistinct> distinctByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isOffline');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsReadOnly() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isReadOnly');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() { QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isTrashed'); return query.addDistinctBy(r'isTrashed');
@ -2840,6 +2938,18 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
}); });
} }
QueryBuilder<Asset, bool, QQueryOperations> isOfflineProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isOffline');
});
}
QueryBuilder<Asset, bool, QQueryOperations> isReadOnlyProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isReadOnly');
});
}
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() { QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isTrashed'); return query.addPropertyName(r'isTrashed');

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart';
@ -108,52 +109,33 @@ class MultiselectGrid extends HookConsumerWidget {
) )
: null; : null;
Iterable<Asset> remoteOnly(
Iterable<Asset> assets, {
void Function()? errorCallback,
}) {
final bool onlyRemote = assets.every((e) => e.isRemote);
if (!onlyRemote) {
if (errorCallback != null) errorCallback();
return assets.where((a) => a.isRemote);
}
return assets;
}
Iterable<Asset> ownedOnly(
Iterable<Asset> assets, {
void Function()? errorCallback,
}) {
if (currentUser == null) return [];
final userId = currentUser.isarId;
final bool onlyOwned = assets.every((e) => e.ownerId == userId);
if (!onlyOwned) {
if (errorCallback != null) errorCallback();
return assets.where((a) => a.ownerId == userId);
}
return assets;
}
Iterable<Asset> ownedRemoteSelection({ Iterable<Asset> ownedRemoteSelection({
String? localErrorMessage, String? localErrorMessage,
String? ownerErrorMessage, String? ownerErrorMessage,
}) { }) {
final assets = selection.value; final assets = selection.value;
return remoteOnly( return assets
ownedOnly(assets, errorCallback: errorBuilder(ownerErrorMessage)), .remoteOnly(errorCallback: errorBuilder(ownerErrorMessage))
errorCallback: errorBuilder(localErrorMessage), .ownedOnly(
); currentUser,
errorCallback: errorBuilder(localErrorMessage),
);
} }
Iterable<Asset> remoteSelection({String? errorMessage}) => remoteOnly( Iterable<Asset> remoteSelection({String? errorMessage}) =>
selection.value, selection.value.remoteOnly(
errorCallback: errorBuilder(errorMessage), errorCallback: errorBuilder(errorMessage),
); );
void onShareAssets(bool shareLocal) { void onShareAssets(bool shareLocal) {
processing.value = true; processing.value = true;
if (shareLocal) { if (shareLocal) {
handleShareAssets(ref, context, selection.value.toList()); // Share = Download + Send to OS specific share sheet
// Filter offline assets since we cannot fetch their original file
final liveAssets = selection.value.nonOfflineOnly(
errorCallback: errorBuilder('asset_action_share_err_offline'.tr()),
);
handleShareAssets(ref, context, liveAssets);
} else { } else {
final ids = final ids =
remoteSelection(errorMessage: "home_page_share_err_local".tr()) remoteSelection(errorMessage: "home_page_share_err_local".tr())
@ -199,10 +181,17 @@ class MultiselectGrid extends HookConsumerWidget {
try { try {
final trashEnabled = final trashEnabled =
ref.read(serverInfoProvider.select((v) => v.serverFeatures.trash)); ref.read(serverInfoProvider.select((v) => v.serverFeatures.trash));
final toDelete = ownedOnly( final toDelete = selection.value
selection.value, .ownedOnly(
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()), currentUser,
).toList(); errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
)
// Cannot delete readOnly / external assets. They are handled through library offline jobs
.writableOnly(
errorCallback:
errorBuilder('asset_action_delete_err_read_only'.tr()),
)
.toList();
await ref await ref
.read(assetProvider.notifier) .read(assetProvider.notifier)
.deleteAssets(toDelete, force: !trashEnabled); .deleteAssets(toDelete, force: !trashEnabled);
@ -331,6 +320,11 @@ class MultiselectGrid extends HookConsumerWidget {
final remoteAssets = ownedRemoteSelection( final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(), localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
).writableOnly(
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
errorCallback: errorBuilder(
'multiselect_grid_edit_date_time_err_read_only'.tr(),
),
); );
if (remoteAssets.isNotEmpty) { if (remoteAssets.isNotEmpty) {
handleEditDateTime(ref, context, remoteAssets.toList()); handleEditDateTime(ref, context, remoteAssets.toList());
@ -345,6 +339,11 @@ class MultiselectGrid extends HookConsumerWidget {
final remoteAssets = ownedRemoteSelection( final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(), localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
).writableOnly(
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
errorCallback: errorBuilder(
'multiselect_grid_edit_gps_err_read_only'.tr(),
),
); );
if (remoteAssets.isNotEmpty) { if (remoteAssets.isNotEmpty) {
handleEditLocation(ref, context, remoteAssets.toList()); handleEditLocation(ref, context, remoteAssets.toList());

View file

@ -13,6 +13,8 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
await _migrateTo(db, 3); await _migrateTo(db, 3);
case 3: case 3:
await _migrateTo(db, 4); await _migrateTo(db, 4);
case 4:
await _migrateTo(db, 5);
} }
} }

View file

@ -17,7 +17,7 @@ import 'package:latlong2/latlong.dart';
void handleShareAssets( void handleShareAssets(
WidgetRef ref, WidgetRef ref,
BuildContext context, BuildContext context,
List<Asset> selection, Iterable<Asset> selection,
) { ) {
showDialog( showDialog(
context: context, context: context,