mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): Archive feature on mobile (#2258)
* update asset to include isArchive property * Not display archived assets on timeline * replace share button to archive button * Added archive page * Add bottom nav bar * clean up homepage * remove deadcode * improve on sync is archive * show archive asset correctly * better merge condition * Added back renderList to re-rendering don't jump around * Better way to handle showing archive assets * complete ArchiveSelectionNotifier * toggle archive * remove deadcode * fix unit tests * update assets in DB when changing assets * update asset state to reflect archived status * allow to archive assets via multi-select from timeline * fixed logic * Add options to bulk unarchive * regenerate api * Change position of toast message --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
parent
635eee9e5e
commit
2e5cd986dd
27 changed files with 523 additions and 114 deletions
|
|
@ -29,7 +29,8 @@ class Asset {
|
|||
ownerId = fastHash(remote.ownerId),
|
||||
exifInfo =
|
||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||
isFavorite = remote.isFavorite;
|
||||
isFavorite = remote.isFavorite,
|
||||
isArchived = remote.isArchived;
|
||||
|
||||
Asset.local(AssetEntity local)
|
||||
: localId = local.id,
|
||||
|
|
@ -44,6 +45,7 @@ class Asset {
|
|||
fileModifiedAt = local.modifiedDateTime,
|
||||
updatedAt = local.modifiedDateTime,
|
||||
isFavorite = local.isFavorite,
|
||||
isArchived = false,
|
||||
fileCreatedAt = local.createDateTime {
|
||||
if (fileCreatedAt.year == 1970) {
|
||||
fileCreatedAt = fileModifiedAt;
|
||||
|
|
@ -70,6 +72,7 @@ class Asset {
|
|||
this.exifInfo,
|
||||
required this.isFavorite,
|
||||
required this.isLocal,
|
||||
required this.isArchived,
|
||||
});
|
||||
|
||||
@ignore
|
||||
|
|
@ -132,6 +135,8 @@ class Asset {
|
|||
|
||||
bool isLocal;
|
||||
|
||||
bool isArchived;
|
||||
|
||||
@ignore
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
|
|
@ -168,7 +173,8 @@ class Asset {
|
|||
fileName == other.fileName &&
|
||||
livePhotoVideoId == other.livePhotoVideoId &&
|
||||
isFavorite == other.isFavorite &&
|
||||
isLocal == other.isLocal;
|
||||
isLocal == other.isLocal &&
|
||||
isArchived == other.isArchived;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -189,7 +195,8 @@ class Asset {
|
|||
fileName.hashCode ^
|
||||
livePhotoVideoId.hashCode ^
|
||||
isFavorite.hashCode ^
|
||||
isLocal.hashCode;
|
||||
isLocal.hashCode ^
|
||||
isArchived.hashCode;
|
||||
|
||||
bool updateFromAssetEntity(AssetEntity ae) {
|
||||
// TODO check more fields;
|
||||
|
|
@ -217,6 +224,9 @@ class Asset {
|
|||
height ??= a.height;
|
||||
exifInfo ??= a.exifInfo;
|
||||
exifInfo?.id = id;
|
||||
if (!isRemote) {
|
||||
isArchived = a.isArchived;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +281,8 @@ class Asset {
|
|||
"isFavorite": $isFavorite,
|
||||
"isLocal": $isLocal,
|
||||
"width": ${width ?? "N/A"},
|
||||
"height": ${height ?? "N/A"}
|
||||
"height": ${height ?? "N/A"},
|
||||
"isArchived": $isArchived
|
||||
}""";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,49 +47,54 @@ const AssetSchema = CollectionSchema(
|
|||
name: r'height',
|
||||
type: IsarType.int,
|
||||
),
|
||||
r'isFavorite': PropertySchema(
|
||||
r'isArchived': PropertySchema(
|
||||
id: 6,
|
||||
name: r'isArchived',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isFavorite': PropertySchema(
|
||||
id: 7,
|
||||
name: r'isFavorite',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isLocal': PropertySchema(
|
||||
id: 7,
|
||||
id: 8,
|
||||
name: r'isLocal',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
id: 8,
|
||||
id: 9,
|
||||
name: r'livePhotoVideoId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'ownerId': PropertySchema(
|
||||
id: 10,
|
||||
id: 11,
|
||||
name: r'ownerId',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 11,
|
||||
id: 12,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 12,
|
||||
id: 13,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 13,
|
||||
id: 14,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 14,
|
||||
id: 15,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
|
|
@ -175,15 +180,16 @@ void _assetSerialize(
|
|||
writer.writeDateTime(offsets[3], object.fileModifiedAt);
|
||||
writer.writeString(offsets[4], object.fileName);
|
||||
writer.writeInt(offsets[5], object.height);
|
||||
writer.writeBool(offsets[6], object.isFavorite);
|
||||
writer.writeBool(offsets[7], object.isLocal);
|
||||
writer.writeString(offsets[8], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[9], object.localId);
|
||||
writer.writeLong(offsets[10], object.ownerId);
|
||||
writer.writeString(offsets[11], object.remoteId);
|
||||
writer.writeByte(offsets[12], object.type.index);
|
||||
writer.writeDateTime(offsets[13], object.updatedAt);
|
||||
writer.writeInt(offsets[14], object.width);
|
||||
writer.writeBool(offsets[6], object.isArchived);
|
||||
writer.writeBool(offsets[7], object.isFavorite);
|
||||
writer.writeBool(offsets[8], object.isLocal);
|
||||
writer.writeString(offsets[9], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[10], object.localId);
|
||||
writer.writeLong(offsets[11], object.ownerId);
|
||||
writer.writeString(offsets[12], object.remoteId);
|
||||
writer.writeByte(offsets[13], object.type.index);
|
||||
writer.writeDateTime(offsets[14], object.updatedAt);
|
||||
writer.writeInt(offsets[15], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
|
|
@ -199,16 +205,17 @@ Asset _assetDeserialize(
|
|||
fileModifiedAt: reader.readDateTime(offsets[3]),
|
||||
fileName: reader.readString(offsets[4]),
|
||||
height: reader.readIntOrNull(offsets[5]),
|
||||
isFavorite: reader.readBool(offsets[6]),
|
||||
isLocal: reader.readBool(offsets[7]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
|
||||
localId: reader.readString(offsets[9]),
|
||||
ownerId: reader.readLong(offsets[10]),
|
||||
remoteId: reader.readStringOrNull(offsets[11]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
|
||||
isArchived: reader.readBool(offsets[6]),
|
||||
isFavorite: reader.readBool(offsets[7]),
|
||||
isLocal: reader.readBool(offsets[8]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
|
||||
localId: reader.readString(offsets[10]),
|
||||
ownerId: reader.readLong(offsets[11]),
|
||||
remoteId: reader.readStringOrNull(offsets[12]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[13]),
|
||||
width: reader.readIntOrNull(offsets[14]),
|
||||
updatedAt: reader.readDateTime(offsets[14]),
|
||||
width: reader.readIntOrNull(offsets[15]),
|
||||
);
|
||||
object.id = id;
|
||||
return object;
|
||||
|
|
@ -238,19 +245,21 @@ P _assetDeserializeProp<P>(
|
|||
case 7:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 12:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 13:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 14:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 15:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
|
|
@ -1024,6 +1033,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isArchivedEqualTo(
|
||||
bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isArchived',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isFavoriteEqualTo(
|
||||
bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
|
@ -1771,6 +1790,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsArchived() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isArchived', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsArchivedDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isArchived', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsFavorite() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isFavorite', Sort.asc);
|
||||
|
|
@ -1965,6 +1996,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsArchived() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isArchived', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsArchivedDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isArchived', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsFavorite() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isFavorite', Sort.asc);
|
||||
|
|
@ -2112,6 +2155,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsArchived() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isArchived');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsFavorite() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isFavorite');
|
||||
|
|
@ -2214,6 +2263,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isArchivedProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isArchived');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isFavoriteProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isFavorite');
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
|
|
@ -19,6 +20,8 @@ import 'package:logging/logging.dart';
|
|||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
/// State does not contain archived assets.
|
||||
/// Use database provider if you want to access the isArchived assets
|
||||
class AssetsState {
|
||||
final List<Asset> allAssets;
|
||||
final RenderList? renderList;
|
||||
|
|
@ -76,6 +79,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
GroupAssetsBy
|
||||
.values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||
);
|
||||
|
||||
state = await AssetsState.fromAssetList(newAssetList)
|
||||
.withRenderDataStructure(layout);
|
||||
}
|
||||
|
|
@ -112,6 +116,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
}
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
stopwatch.reset();
|
||||
if (!newRemote &&
|
||||
|
|
@ -139,6 +144,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
Future<List<Asset>> _getUserAssets(int userId) => _db.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(userId)
|
||||
.isArchivedEqualTo(false)
|
||||
.sortByFileCreatedAtDesc()
|
||||
.findAll();
|
||||
|
||||
|
|
@ -224,13 +230,46 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
}
|
||||
|
||||
final index = state.allAssets.indexWhere((a) => asset.id == a.id);
|
||||
if (index > 0) {
|
||||
if (index != -1) {
|
||||
state.allAssets[index] = newAsset;
|
||||
_updateAssetsState(state.allAssets);
|
||||
}
|
||||
|
||||
return newAsset.isFavorite;
|
||||
}
|
||||
|
||||
Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
|
||||
final newAssets = await Future.wait(
|
||||
assets.map((a) => _assetService.changeArchiveStatus(a, status)),
|
||||
);
|
||||
int i = 0;
|
||||
bool unArchived = false;
|
||||
for (Asset oldAsset in assets) {
|
||||
final newAsset = newAssets[i++];
|
||||
if (newAsset == null) {
|
||||
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
||||
continue;
|
||||
}
|
||||
final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
|
||||
if (newAsset.isArchived) {
|
||||
// remove from state
|
||||
if (index != -1) {
|
||||
state.allAssets.removeAt(index);
|
||||
}
|
||||
} else {
|
||||
// add to state is difficult because the list is sorted
|
||||
unArchived = true;
|
||||
}
|
||||
}
|
||||
if (unArchived) {
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
await _stateUpdateLock.run(
|
||||
() async => _updateAssetsState(await _getUserAssets(me.isarId)),
|
||||
);
|
||||
} else {
|
||||
_updateAssetsState(state.allAssets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||
|
|
|
|||
|
|
@ -121,10 +121,21 @@ class AssetService {
|
|||
) async {
|
||||
final dto =
|
||||
await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
|
||||
return dto == null ? null : Asset.remote(dto);
|
||||
if (dto != null) {
|
||||
final updated = Asset.remote(dto).updateFromDb(asset);
|
||||
if (updated.isInDb) {
|
||||
await _db.writeTxn(() => updated.put(_db));
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
|
||||
return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
|
||||
}
|
||||
|
||||
Future<Asset?> changeArchiveStatus(Asset asset, bool isArchive) {
|
||||
return updateAsset(asset, UpdateAssetDto(isArchived: isArchive));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue