refactor: yeet old timeline (#27666)

* refactor: yank old timeline

# Conflicts:
#	mobile/lib/presentation/pages/editing/drift_edit.page.dart
#	mobile/lib/providers/websocket.provider.dart
#	mobile/lib/routing/router.dart

* more cleanup

* remove native code

* chore: bump sqlite-data version

* remove old background tasks from BGTaskSchedulerPermittedIdentifiers

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-04-15 23:00:27 +05:30
committed by GitHub
parent 6dd6053222
commit 79fccdbee0
367 changed files with 332 additions and 50870 deletions
@@ -1,139 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
enum AlbumSort { remoteId, localId }
final albumRepositoryProvider = Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
class AlbumRepository extends DatabaseRepository {
const AlbumRepository(super.db);
Future<int> count({bool? local}) {
final baseQuery = db.albums.where();
final QueryBuilder<Album, Album, QAfterWhereClause> query = switch (local) {
null => baseQuery.noOp(),
true => baseQuery.localIdIsNotNull(),
false => baseQuery.remoteIdIsNotNull(),
};
return query.count();
}
Future<Album> create(Album album) => txn(() => db.albums.store(album));
Future<Album?> getByName(String name, {bool? shared, bool? remote, bool? owner}) {
var query = db.albums.filter().nameEqualTo(name);
if (shared != null) {
query = query.sharedEqualTo(shared);
}
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
if (owner == true) {
query = query.owner((q) => q.isarIdEqualTo(isarUserId));
} else if (owner == false) {
query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
}
if (remote == true) {
query = query.localIdIsNull();
} else if (remote == false) {
query = query.remoteIdIsNull();
}
return query.findFirst();
}
Future<Album> update(Album album) => txn(() => db.albums.store(album));
Future<void> delete(int albumId) => txn(() => db.albums.delete(albumId));
Future<List<Album>> getAll({bool? shared, bool? remote, int? ownerId, AlbumSort? sortBy}) {
final baseQuery = db.albums.where();
final QueryBuilder<Album, Album, QAfterWhereClause> afterWhere;
if (remote == null) {
afterWhere = baseQuery.noOp();
} else if (remote) {
afterWhere = baseQuery.remoteIdIsNotNull();
} else {
afterWhere = baseQuery.localIdIsNotNull();
}
QueryBuilder<Album, Album, QAfterFilterCondition> filterQuery = afterWhere.filter().noOp();
if (shared != null) {
filterQuery = filterQuery.sharedEqualTo(true);
}
if (ownerId != null) {
filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId));
}
final QueryBuilder<Album, Album, QAfterSortBy> query = switch (sortBy) {
null => filterQuery.noOp(),
AlbumSort.remoteId => filterQuery.sortByRemoteId(),
AlbumSort.localId => filterQuery.sortByLocalId(),
};
return query.findAll();
}
Future<Album?> get(int id) => db.albums.get(id);
Future<Album?> getByRemoteId(String remoteId) {
return db.albums.filter().remoteIdEqualTo(remoteId).findFirst();
}
Future<void> removeUsers(Album album, List<UserDto> users) =>
txn(() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto)));
Future<void> addAssets(Album album, List<Asset> assets) => txn(() => album.assets.update(link: assets));
Future<void> removeAssets(Album album, List<Asset> assets) => txn(() => album.assets.update(unlink: assets));
Future<Album> recalculateMetadata(Album album) async {
album.startDate = await album.assets.filter().fileCreatedAtProperty().min();
album.endDate = await album.assets.filter().fileCreatedAtProperty().max();
album.lastModifiedAssetTimestamp = await album.assets.filter().updatedAtProperty().max();
return album;
}
Future<void> addUsers(Album album, List<UserDto> users) =>
txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto)));
Future<void> deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll());
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode) async {
var query = db.albums.filter().nameContains(searchTerm, caseSensitive: false).remoteIdIsNotNull();
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
switch (filterMode) {
case QuickFilterMode.sharedWithMe:
query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
case QuickFilterMode.myAlbums:
query = query.owner((q) => q.isarIdEqualTo(isarUserId));
case QuickFilterMode.all:
break;
}
return await query.findAll();
}
Future<void> clearTable() async {
await txn(() async {
await db.albums.clear();
});
}
Stream<List<Album>> watchRemoteAlbums() {
return db.albums.where().remoteIdIsNotNull().watch();
}
Stream<List<Album>> watchLocalAlbums() {
return db.albums.where().localIdIsNotNull().watch();
}
Stream<Album?> watchAlbum(int id) {
return db.albums.watchObject(id, fireImmediately: true);
}
}
@@ -1,171 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart' show AlbumAssetOrder, RemoteAlbum;
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
final albumApiRepositoryProvider = Provider((ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi));
class AlbumApiRepository extends ApiRepository {
final AlbumsApi _api;
AlbumApiRepository(this._api);
Future<Album> get(String id) async {
final dto = await checkNull(_api.getAlbumInfo(id));
return _toAlbum(dto);
}
Future<List<Album>> getAll({bool? shared}) async {
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
return dtos.map(_toAlbum).toList();
}
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
String? description,
}) async {
final users = sharedUserIds.map((id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor));
final responseDto = await checkNull(
_api.createAlbum(
CreateAlbumDto(
albumName: name,
description: description,
assetIds: assetIds.toList(),
albumUsers: users.toList(),
),
),
);
return _toAlbum(responseDto);
}
// TODO: Change name after removing old method
Future<RemoteAlbum> createDriftAlbum(String name, {required Iterable<String> assetIds, String? description}) async {
final responseDto = await checkNull(
_api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())),
);
return _toRemoteAlbum(responseDto);
}
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
SortOrder? sortOrder,
}) async {
AssetOrder? order;
if (sortOrder != null) {
order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc;
}
final response = await checkNull(
_api.updateAlbumInfo(
albumId,
UpdateAlbumDto(
albumName: name,
albumThumbnailAssetId: thumbnailAssetId,
description: description,
isActivityEnabled: activityEnabled,
order: order,
),
),
);
return _toAlbum(response);
}
Future<void> delete(String albumId) {
return _api.deleteAlbum(albumId);
}
Future<({List<String> added, List<String> duplicates})> addAssets(String albumId, Iterable<String> assetIds) async {
final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList())));
final List<String> added = [];
final List<String> duplicates = [];
for (final result in response) {
if (result.success) {
added.add(result.id);
} else if (result.error == BulkIdErrorReason.duplicate) {
duplicates.add(result.id);
}
}
return (added: added, duplicates: duplicates);
}
Future<({List<String> removed, List<String> failed})> removeAssets(String albumId, Iterable<String> assetIds) async {
final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList())));
final List<String> removed = [], failed = [];
for (final dto in response) {
if (dto.success) {
removed.add(dto.id);
} else {
failed.add(dto.id);
}
}
return (removed: removed, failed: failed);
}
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
return _toAlbum(response);
}
Future<void> removeUser(String albumId, {required String userId}) {
return _api.removeUserFromAlbum(albumId, userId);
}
static Album _toAlbum(AlbumResponseDto dto) {
final Album album = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
description: dto.description,
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
);
album.remoteAssetCount = dto.assetCount;
album.owner.value = entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner));
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
final users = dto.albumUsers.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user));
album.sharedUsers.addAll(users.map(entity.User.fromDto));
final assets = dto.assets.map(Asset.remote).toList();
album.assets.addAll(assets);
return album;
}
static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) {
return RemoteAlbum(
id: dto.id,
name: dto.albumName,
ownerId: dto.owner.id,
description: dto.description,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
thumbnailAssetId: dto.albumThumbnailAssetId,
isActivityEnabled: dto.isActivityEnabled,
order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
assetCount: dto.assetCount,
ownerName: dto.owner.name,
isShared: dto.albumUsers.length > 2,
);
}
}
@@ -1,99 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final albumMediaRepositoryProvider = Provider((ref) => const AlbumMediaRepository());
class AlbumMediaRepository {
const AlbumMediaRepository();
bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, true);
FilterOptionGroup? _getAlbumFilter({
DateTimeCond? updateTimeCond,
bool? containsPathModified,
List<OrderOption>? orderBy,
}) => useCustomFilter
? FilterOptionGroup(
imageOption: const FilterOption(needTitle: true, sizeConstraint: SizeConstraint(ignoreSize: true)),
videoOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
durationConstraint: DurationConstraint(allowNullable: true),
),
containsPathModified: containsPathModified ?? false,
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
updateTimeCond: updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true),
orders: orderBy ?? [],
)
: null;
Future<List<Album>> getAll() async {
final filter = useCustomFilter
? CustomFilter.sql(where: '${CustomColumns.base.width} > 0')
: FilterOptionGroup(containsPathModified: true);
final List<AssetPathEntity> assetPathEntities = await PhotoManager.getAssetPathList(
hasAll: true,
filterOption: filter,
);
return assetPathEntities.map(_toAlbum).toList();
}
Future<List<String>> getAssetIds(String albumId) async {
final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
final List<AssetEntity> assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
return assets.map((e) => e.id).toList();
}
Future<int> getAssetCount(String albumId) async {
final album = await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
return album.assetCountAsync;
}
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
int end = 0x7fffffffffffffff,
DateTime? modifiedFrom,
DateTime? modifiedUntil,
bool orderByModificationDate = false,
}) async {
final onDevice = await AssetPathEntity.fromId(
albumId,
filterOption: _getAlbumFilter(
updateTimeCond: modifiedFrom == null && modifiedUntil == null
? null
: DateTimeCond(min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760)),
orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [],
),
);
final List<AssetEntity> assets = await onDevice.getAssetListRange(start: start, end: end);
return assets.map(AssetMediaRepository.toAsset).toList().cast();
}
Future<Album> get(String id) async {
final assetPathEntity = await AssetPathEntity.fromId(id, filterOption: _getAlbumFilter(containsPathModified: true));
return _toAlbum(assetPathEntity);
}
static Album _toAlbum(AssetPathEntity assetPathEntity) {
final Album album = Album(
name: assetPathEntity.name,
createdAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
modifiedAt: assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
shared: false,
activityEnabled: false,
);
album.owner.value = User.fromDto(Store.get(StoreKey.currentUser));
album.localId = assetPathEntity.id;
album.isAll = assetPathEntity.isAll;
return album;
}
}
@@ -1,220 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
enum AssetSort { checksum, ownerIdChecksum }
final assetRepositoryProvider = Provider((ref) => AssetRepository(ref.watch(dbProvider)));
class AssetRepository extends DatabaseRepository {
const AssetRepository(super.db);
Future<List<Asset>> getByAlbum(
Album album, {
Iterable<String> notOwnedBy = const [],
String? ownerId,
AssetState? state,
AssetSort? sortBy,
}) {
var query = album.assets.filter();
final isarUserIds = notOwnedBy.map(fastHash).toList();
if (notOwnedBy.length == 1) {
query = query.not().ownerIdEqualTo(isarUserIds.first);
} else if (notOwnedBy.isNotEmpty) {
query = query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id));
}
if (ownerId != null) {
query = query.ownerIdEqualTo(fastHash(ownerId));
}
if (state != null) {
query = switch (state) {
AssetState.local => query.remoteIdIsNull(),
AssetState.remote => query.localIdIsNull(),
AssetState.merged => query.localIdIsNotNull().remoteIdIsNotNull(),
};
}
final QueryBuilder<Asset, Asset, QAfterSortBy> sortedQuery = switch (sortBy) {
null => query.noOp(),
AssetSort.checksum => query.sortByChecksum(),
AssetSort.ownerIdChecksum => query.sortByOwnerId().thenByChecksum(),
};
return sortedQuery.findAll();
}
Future<void> deleteByIds(List<int> ids) => txn(() async {
await db.assets.deleteAll(ids);
await db.exifInfos.deleteAll(ids);
});
Future<Asset?> getByRemoteId(String id) => db.assets.getByRemoteId(id);
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids, {AssetState? state}) async {
if (ids.isEmpty) {
return [];
}
return _getAllByRemoteIdImpl(ids, state).findAll();
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> _getAllByRemoteIdImpl(Iterable<String> ids, AssetState? state) {
final query = db.assets.remote(ids).filter();
return switch (state) {
null => query.noOp(),
AssetState.local => query.remoteIdIsNull(),
AssetState.remote => query.localIdIsNull(),
AssetState.merged => query.localIdIsNotEmpty().remoteIdIsNotNull(),
};
}
Future<List<Asset>> getAll({required String ownerId, AssetState? state, AssetSort? sortBy, int? limit}) {
final baseQuery = db.assets.where();
final isarUserIds = fastHash(ownerId);
final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery = switch (state) {
null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(),
AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull().ownerIdEqualTo(isarUserIds),
AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull().ownerIdEqualTo(isarUserIds),
AssetState.merged =>
baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).filter().remoteIdIsNotNull().localIdIsNotNull(),
};
final QueryBuilder<Asset, Asset, QAfterSortBy> query = switch (sortBy) {
null => filteredQuery.noOp(),
AssetSort.checksum => filteredQuery.sortByChecksum(),
AssetSort.ownerIdChecksum => filteredQuery.sortByOwnerId().thenByChecksum(),
};
return limit == null ? query.findAll() : query.limit(limit).findAll();
}
Future<List<Asset>> updateAll(List<Asset> assets) async {
await txn(() => db.assets.putAll(assets));
return assets;
}
Future<List<Asset>> getMatches({
required List<Asset> assets,
required String ownerId,
AssetState? state,
int limit = 100,
}) {
final baseQuery = db.assets.where();
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query = switch (state) {
null => baseQuery.noOp(),
AssetState.local => baseQuery.remoteIdIsNull().filter().localIdIsNotNull(),
AssetState.remote => baseQuery.localIdIsNull().filter().remoteIdIsNotNull(),
AssetState.merged => baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(),
};
return _getMatchesImpl(query, fastHash(ownerId), assets, limit);
}
Future<Asset> update(Asset asset) async {
await txn(() => asset.put(db));
return asset;
}
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets) =>
txn(() => db.duplicatedAssets.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()));
Future<List<String>> getAllDuplicatedAssetIds() => db.duplicatedAssets.where().idProperty().findAll();
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum) =>
db.assets.getByOwnerIdChecksum(ownerId, checksum);
Future<List<Asset?>> getAllByOwnerIdChecksum(List<int> ownerIds, List<String> checksums) =>
db.assets.getAllByOwnerIdChecksum(ownerIds, checksums);
Future<List<Asset>> getAllLocal() => db.assets.where().localIdIsNotNull().findAll();
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state}) =>
txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll());
Future<List<Asset>> getStackAssets(String stackId) {
return db.assets
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackIdEqualTo(stackId)
// orders primary asset first as its ID is null
.sortByStackPrimaryAssetId()
.thenByFileCreatedAtDesc()
.findAll();
}
Future<void> clearTable() async {
await txn(() async {
await db.assets.clear();
});
}
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}) {
return db.assets.watchObject(id, fireImmediately: fireImmediately);
}
Future<List<Asset>> getTrashAssets(String userId) {
return db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(true)
.findAll();
}
Future<List<Asset>> getRecentlyTakenAssets(String userId) {
return db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.sortByFileCreatedAtDesc()
.findAll();
}
Future<List<Asset>> getMotionAssets(String userId) {
return db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.livePhotoVideoIdIsNotNull()
.findAll();
}
}
Future<List<Asset>> _getMatchesImpl(
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
int ownerId,
List<Asset> assets,
int limit,
) => query
.ownerIdEqualTo(ownerId)
.anyOf(
assets,
(q, Asset a) => q
.fileNameEqualTo(a.fileName)
.and()
.durationInSecondsEqualTo(a.durationInSeconds)
.and()
.fileCreatedAtBetween(
a.fileCreatedAt.subtract(const Duration(hours: 12)),
a.fileCreatedAt.add(const Duration(hours: 12)),
)
.and()
.not()
.checksumEqualTo(a.checksum),
)
.sortByFileName()
.thenByFileCreatedAt()
.thenByFileModifiedAt()
.limit(limit)
.findAll();
@@ -3,7 +3,6 @@ import 'package:http/http.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@@ -12,7 +11,6 @@ import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(
ref.watch(apiServiceProvider).assetsApi,
ref.watch(apiServiceProvider).searchApi,
ref.watch(apiServiceProvider).stacksApi,
ref.watch(apiServiceProvider).trashApi,
),
@@ -20,32 +18,10 @@ final assetApiRepositoryProvider = Provider(
class AssetApiRepository extends ApiRepository {
final AssetsApi _api;
final SearchApi _searchApi;
final StacksApi _stacksApi;
final TrashApi _trashApi;
AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi);
Future<Asset> update(String id, {String? description}) async {
final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description)));
return Asset.remote(response);
}
Future<List<Asset>> search({List<String> personIds = const []}) async {
// TODO this always fetches all assets, change API and usage to actually do pagination
final List<Asset> result = [];
bool hasNext = true;
int currentPage = 1;
while (hasNext) {
final response = await checkNull(
_searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)),
);
result.addAll(response.assets.items.map(Asset.remote));
hasNext = response.assets.nextPage != null;
currentPage++;
}
return result;
}
AssetApiRepository(this._api, this._stacksApi, this._trashApi);
Future<void> delete(List<String> ids, bool force) async {
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force));
@@ -5,15 +5,10 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -50,39 +45,9 @@ class AssetMediaRepository {
return PhotoManager.editor.deleteWithIds(ids);
}
Future<asset_entity.Asset?> get(String id) async {
Future<AssetEntity?> get(String id) async {
final entity = await AssetEntity.fromId(id);
return toAsset(entity);
}
static asset_entity.Asset? toAsset(AssetEntity? local) {
if (local == null) return null;
final asset_entity.Asset asset = asset_entity.Asset(
checksum: "",
localId: local.id,
ownerId: fastHash(Store.get(StoreKey.currentUser).id),
fileCreatedAt: local.createDateTime,
fileModifiedAt: local.modifiedDateTime,
updatedAt: local.modifiedDateTime,
durationInSeconds: local.duration,
type: asset_entity.AssetType.values[local.typeInt],
fileName: local.title!,
width: local.width,
height: local.height,
isFavorite: local.isFavorite,
);
if (asset.fileCreatedAt.year == 1970) {
asset.fileCreatedAt = asset.fileModifiedAt;
}
if (local.latitude != null) {
asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude);
}
asset.local = local;
return asset;
return entity;
}
Future<String?> getOriginalFilename(String id) async {
+3 -22
View File
@@ -2,40 +2,21 @@ import 'dart:convert';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepository(ref.watch(dbProvider), ref.watch(driftProvider)),
);
final authRepositoryProvider = Provider<AuthRepository>((ref) => AuthRepository(ref.watch(driftProvider)));
class AuthRepository extends DatabaseRepository {
class AuthRepository {
final Drift _drift;
const AuthRepository(super.db, this._drift);
const AuthRepository(this._drift);
Future<void> clearLocalData() async {
await SyncStreamRepository(_drift).reset();
return db.writeTxn(() {
return Future.wait([
db.assets.clear(),
db.exifInfos.clear(),
db.albums.clear(),
db.eTags.clear(),
db.users.clear(),
]);
});
}
bool getEndpointSwitchingFeature() {
@@ -1,32 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
enum BackupAlbumSort { id }
final backupAlbumRepositoryProvider = Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider)));
class BackupAlbumRepository extends DatabaseRepository {
const BackupAlbumRepository(super.db);
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
final baseQuery = db.backupAlbums.where();
final QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> query = switch (sort) {
null => baseQuery.noOp(),
BackupAlbumSort.id => baseQuery.sortById(),
};
return query.findAll();
}
Future<List<String>> getIdsBySelection(BackupSelection backup) =>
db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup) =>
db.backupAlbums.filter().selectionEqualTo(backup).findAll();
Future<void> deleteAll(List<int> ids) => txn(() => db.backupAlbums.deleteAll(ids));
Future<void> updateAll(List<BackupAlbum> backupAlbums) => txn(() => db.backupAlbums.putAll(backupAlbums));
}
@@ -1,25 +0,0 @@
import 'dart:async';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:isar/isar.dart';
/// copied from Isar; needed to check if an async transaction is already active
const Symbol _zoneTxn = #zoneTxn;
abstract class DatabaseRepository implements IDatabaseRepository {
final Isar db;
const DatabaseRepository(this.db);
bool get inTxn => Zone.current[_zoneTxn] != null;
Future<T> txn<T>(Future<T> Function() callback) => inTxn ? callback() : transaction(callback);
@override
Future<T> transaction<T>(Future<T> Function() callback) => db.writeTxn(callback);
}
extension Asd<T> on QueryBuilder<T, dynamic, dynamic> {
QueryBuilder<T, T, O> noOp<O>() {
// ignore: invalid_use_of_protected_member
return QueryBuilder.apply(this, (query) => query);
}
}
@@ -1,27 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
final etagRepositoryProvider = Provider((ref) => ETagRepository(ref.watch(dbProvider)));
class ETagRepository extends DatabaseRepository {
const ETagRepository(super.db);
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
Future<ETag?> get(String id) => db.eTags.getById(id);
Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags));
Future<void> deleteByIds(List<String> ids) => txn(() => db.eTags.deleteAllById(ids));
Future<ETag?> getById(String id) => db.eTags.getById(id);
Future<void> clearTable() async {
await txn(() async {
await db.eTags.clear();
});
}
}
@@ -3,18 +3,12 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType;
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository());
class FileMediaRepository {
const FileMediaRepository();
Future<Asset?> saveImage(Uint8List data, {required String title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath);
return AssetMediaRepository.toAsset(entity);
}
Future<LocalAsset?> saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath);
@@ -30,24 +24,18 @@ class FileMediaRepository {
);
}
Future<Asset?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
Future<AssetEntity?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
return AssetMediaRepository.toAsset(entity);
return entity;
}
Future<Asset?> saveLivePhoto({required File image, required File video, required String title}) async {
Future<AssetEntity?> saveLivePhoto({required File image, required File video, required String title}) async {
final entity = await PhotoManager.editor.darwin.saveLivePhoto(imageFile: image, videoFile: video, title: title);
return AssetMediaRepository.toAsset(entity);
return entity;
}
Future<Asset?> saveVideo(File file, {required String title, String? relativePath}) async {
Future<AssetEntity?> saveVideo(File file, {required String title, String? relativePath}) async {
final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath);
return AssetMediaRepository.toAsset(entity);
return entity;
}
Future<void> clearFileCache() => PhotoManager.clearFileCache();
Future<void> enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true);
Future<void> requestExtendedPermissions() => PhotoManager.requestPermissionExtend();
}
@@ -1,34 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
final partnerRepositoryProvider = Provider((ref) => PartnerRepository(ref.watch(dbProvider)));
class PartnerRepository extends DatabaseRepository {
const PartnerRepository(super.db);
Future<List<UserDto>> getSharedBy() async {
return (await db.users.filter().isPartnerSharedByEqualTo(true).sortById().findAll()).map((u) => u.toDto()).toList();
}
Future<List<UserDto>> getSharedWith() async {
return (await db.users.filter().isPartnerSharedWithEqualTo(true).sortById().findAll())
.map((u) => u.toDto())
.toList();
}
Stream<List<UserDto>> watchSharedBy() {
return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch()).map(
(users) => users.map((u) => u.toDto()).toList(),
);
}
Stream<List<UserDto>> watchSharedWith() {
return (db.users.filter().isPartnerSharedWithEqualTo(true).sortById().watch()).map(
(users) => users.map((u) => u.toDto()).toList(),
);
}
}
@@ -1,146 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:isar/isar.dart';
final timelineRepositoryProvider = Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
class TimelineRepository extends DatabaseRepository {
const TimelineRepository(super.db);
Future<List<String>> getTimelineUserIds(String id) {
return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().findAll();
}
Stream<List<String>> watchTimelineUsers(String id) {
return db.users.filter().inTimelineEqualTo(true).or().idEqualTo(id).idProperty().watch();
}
Stream<RenderList> watchArchiveTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isTrashedEqualTo(false)
.visibilityEqualTo(AssetVisibilityEnum.archive)
.sortByFileCreatedAtDesc();
return _watchRenderList(query, GroupAssetsBy.none);
}
Stream<RenderList> watchFavoriteTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isFavoriteEqualTo(true)
.not()
.visibilityEqualTo(AssetVisibilityEnum.locked)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
return _watchRenderList(query, GroupAssetsBy.none);
}
Stream<RenderList> watchAlbumTimeline(Album album, GroupAssetsBy groupAssetByOption) {
final query = album.assets.filter().isTrashedEqualTo(false).not().visibilityEqualTo(AssetVisibilityEnum.locked);
final withSortedOption = switch (album.sortOrder) {
SortOrder.asc => query.sortByFileCreatedAt(),
SortOrder.desc => query.sortByFileCreatedAtDesc(),
};
return _watchRenderList(withSortedOption, groupAssetByOption);
}
Stream<RenderList> watchTrashTimeline(String userId) {
final query = db.assets.filter().ownerIdEqualTo(fastHash(userId)).isTrashedEqualTo(true).sortByFileCreatedAtDesc();
return _watchRenderList(query, GroupAssetsBy.none);
}
Stream<RenderList> watchAllVideosTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isTrashedEqualTo(false)
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.typeEqualTo(AssetType.video)
.sortByFileCreatedAtDesc();
return _watchRenderList(query, GroupAssetsBy.none);
}
Stream<RenderList> watchHomeTimeline(String userId, GroupAssetsBy groupAssetByOption) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.sortByFileCreatedAtDesc();
return _watchRenderList(query, groupAssetByOption);
}
Stream<RenderList> watchMultiUsersTimeline(List<String> userIds, GroupAssetsBy groupAssetByOption) {
final isarUserIds = userIds.map(fastHash).toList();
final query = db.assets
.where()
.anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id))
.filter()
.isTrashedEqualTo(false)
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
return _watchRenderList(query, groupAssetByOption);
}
Future<RenderList> getTimelineFromAssets(List<Asset> assets, GroupAssetsBy getGroupByOption) {
return RenderList.fromAssets(assets, getGroupByOption);
}
Stream<RenderList> watchAssetSelectionTimeline(String userId) {
final query = db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(fastHash(userId))
.visibilityEqualTo(AssetVisibilityEnum.timeline)
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();
return _watchRenderList(query, GroupAssetsBy.none);
}
Stream<RenderList> watchLockedTimeline(String userId, GroupAssetsBy getGroupByOption) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.visibilityEqualTo(AssetVisibilityEnum.locked)
.isTrashedEqualTo(false)
.sortByFileCreatedAtDesc();
return _watchRenderList(query, getGroupByOption);
}
Stream<RenderList> _watchRenderList(
QueryBuilder<Asset, Asset, QAfterSortBy> query,
GroupAssetsBy groupAssetsBy,
) async* {
yield await RenderList.fromQuery(query, groupAssetsBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupAssetsBy);
}
}
}