Source Code added
This commit is contained in:
parent
800376eafd
commit
9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions
50
mobile/lib/repositories/activity_api.repository.dart
Normal file
50
mobile/lib/repositories/activity_api.repository.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final activityApiRepositoryProvider = Provider(
|
||||
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
|
||||
);
|
||||
|
||||
class ActivityApiRepository extends ApiRepository {
|
||||
final ActivitiesApi _api;
|
||||
|
||||
ActivityApiRepository(this._api);
|
||||
|
||||
Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
|
||||
final response = await checkNull(_api.getActivities(albumId, assetId: assetId));
|
||||
return response.map(_toActivity).toList();
|
||||
}
|
||||
|
||||
Future<Activity> create(String albumId, ActivityType type, {String? assetId, String? comment}) async {
|
||||
final dto = ActivityCreateDto(
|
||||
albumId: albumId,
|
||||
type: type == ActivityType.comment ? ReactionType.comment : ReactionType.like,
|
||||
assetId: assetId,
|
||||
comment: comment,
|
||||
);
|
||||
final response = await checkNull(_api.createActivity(dto));
|
||||
return _toActivity(response);
|
||||
}
|
||||
|
||||
Future<void> delete(String id) {
|
||||
return checkNull(_api.deleteActivity(id));
|
||||
}
|
||||
|
||||
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
|
||||
final response = await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));
|
||||
return ActivityStats(comments: response.comments);
|
||||
}
|
||||
|
||||
static Activity _toActivity(ActivityResponseDto dto) => Activity(
|
||||
id: dto.id,
|
||||
createdAt: dto.createdAt,
|
||||
type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like,
|
||||
user: UserConverter.fromSimpleUserDto(dto.user),
|
||||
assetId: dto.assetId,
|
||||
comment: dto.comment,
|
||||
);
|
||||
}
|
||||
139
mobile/lib/repositories/album.repository.dart
Normal file
139
mobile/lib/repositories/album.repository.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
171
mobile/lib/repositories/album_api.repository.dart
Normal file
171
mobile/lib/repositories/album_api.repository.dart
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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 == BulkIdResponseDtoErrorEnum.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
mobile/lib/repositories/album_media.repository.dart
Normal file
99
mobile/lib/repositories/album_media.repository.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
9
mobile/lib/repositories/api.repository.dart
Normal file
9
mobile/lib/repositories/api.repository.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:immich_mobile/constants/errors.dart';
|
||||
|
||||
abstract class ApiRepository {
|
||||
Future<T> checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw const NoResponseDtoError();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
220
mobile/lib/repositories/asset.repository.dart
Normal file
220
mobile/lib/repositories/asset.repository.dart
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
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();
|
||||
114
mobile/lib/repositories/asset_api.repository.dart
Normal file
114
mobile/lib/repositories/asset_api.repository.dart
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
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';
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> delete(List<String> ids, bool force) async {
|
||||
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force));
|
||||
}
|
||||
|
||||
Future<void> restoreTrash(List<String> ids) async {
|
||||
await _trashApi.restoreAssets(BulkIdsDto(ids: ids));
|
||||
}
|
||||
|
||||
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility)));
|
||||
}
|
||||
|
||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
||||
}
|
||||
|
||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: dateTime.toIso8601String()));
|
||||
}
|
||||
|
||||
Future<StackResponse> stack(List<String> ids) async {
|
||||
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
||||
|
||||
return responseDto.toStack();
|
||||
}
|
||||
|
||||
Future<void> unStack(List<String> ids) async {
|
||||
return _stacksApi.deleteStacks(BulkIdsDto(ids: ids));
|
||||
}
|
||||
|
||||
Future<Response> downloadAsset(String id, {required bool edited}) {
|
||||
return _api.downloadAssetWithHttpInfo(id, edited: edited);
|
||||
}
|
||||
|
||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||
AssetVisibilityEnum.locked => AssetVisibility.locked,
|
||||
AssetVisibilityEnum.archive => AssetVisibility.archive,
|
||||
};
|
||||
|
||||
Future<String?> getAssetMIMEType(String assetId) async {
|
||||
final response = await checkNull(_api.getAssetInfo(assetId));
|
||||
|
||||
// we need to get the MIME of the thumbnail once that gets added to the API
|
||||
return response.originalMimeType;
|
||||
}
|
||||
|
||||
Future<void> updateDescription(String assetId, String description) {
|
||||
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
|
||||
}
|
||||
|
||||
Future<void> updateRating(String assetId, int rating) {
|
||||
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
|
||||
}
|
||||
}
|
||||
|
||||
extension on StackResponseDto {
|
||||
StackResponse toStack() {
|
||||
return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList());
|
||||
}
|
||||
}
|
||||
169
mobile/lib/repositories/asset_media.repository.dart
Normal file
169
mobile/lib/repositories/asset_media.repository.dart
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
|
||||
|
||||
class AssetMediaRepository {
|
||||
final AssetApiRepository _assetApiRepository;
|
||||
|
||||
static final Logger _log = Logger("AssetMediaRepository");
|
||||
|
||||
const AssetMediaRepository(this._assetApiRepository);
|
||||
|
||||
Future<bool> _androidSupportsTrash() async {
|
||||
if (Platform.isAndroid) {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
int sdkVersion = androidInfo.version.sdkInt;
|
||||
return sdkVersion >= 31;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> deleteAll(List<String> ids) async {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
if (await _androidSupportsTrash()) {
|
||||
return PhotoManager.editor.android.moveToTrash(
|
||||
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
|
||||
);
|
||||
} else {
|
||||
return PhotoManager.editor.deleteWithIds(ids);
|
||||
}
|
||||
}
|
||||
return PhotoManager.editor.deleteWithIds(ids);
|
||||
}
|
||||
|
||||
Future<asset_entity.Asset?> 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;
|
||||
}
|
||||
|
||||
Future<String?> getOriginalFilename(String id) async {
|
||||
final entity = await AssetEntity.fromId(id);
|
||||
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// titleAsync gets the correct original filename for some assets on iOS
|
||||
// otherwise using the `entity.title` would return a random GUID
|
||||
final originalFilename = await entity.titleAsync;
|
||||
// treat empty filename as missing
|
||||
return originalFilename.isNotEmpty ? originalFilename : null;
|
||||
} catch (e) {
|
||||
_log.warning("Failed to get original filename for asset: $id. Error: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make this more efficient
|
||||
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context) async {
|
||||
final downloadedXFiles = <XFile>[];
|
||||
final tempFiles = <File>[];
|
||||
|
||||
for (var asset in assets) {
|
||||
final localId = (asset is LocalAsset)
|
||||
? asset.id
|
||||
: asset is RemoteAsset
|
||||
? asset.localId
|
||||
: null;
|
||||
if (localId != null && !asset.isEdited) {
|
||||
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
|
||||
downloadedXFiles.add(XFile(f!.path));
|
||||
if (CurrentPlatform.isIOS) {
|
||||
tempFiles.add(f);
|
||||
}
|
||||
} else {
|
||||
final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId;
|
||||
if (remoteId == null) {
|
||||
_log.warning("Asset has no remote ID for sharing: $asset");
|
||||
continue;
|
||||
}
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final name = asset.name;
|
||||
final tempFile = await File('${tempDir.path}/$name').create();
|
||||
final res = await _assetApiRepository.downloadAsset(remoteId, edited: true);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe("Download for $name failed", res.toLoggerString());
|
||||
continue;
|
||||
}
|
||||
|
||||
await tempFile.writeAsBytes(res.bodyBytes);
|
||||
downloadedXFiles.add(XFile(tempFile.path));
|
||||
tempFiles.add(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedXFiles.isEmpty) {
|
||||
_log.warning("No asset can be retrieved for share");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we dont want to await the share result since the
|
||||
// "preparing" dialog will not disappear until
|
||||
final size = context.sizeData;
|
||||
unawaited(
|
||||
Share.shareXFiles(
|
||||
downloadedXFiles,
|
||||
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
|
||||
).then((result) async {
|
||||
for (var file in tempFiles) {
|
||||
try {
|
||||
await file.delete();
|
||||
} catch (e) {
|
||||
_log.warning("Failed to delete temporary file: ${file.path}", e);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return downloadedXFiles.length;
|
||||
}
|
||||
}
|
||||
69
mobile/lib/repositories/auth.repository.dart
Normal file
69
mobile/lib/repositories/auth.repository.dart
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
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)),
|
||||
);
|
||||
|
||||
class AuthRepository extends DatabaseRepository {
|
||||
final Drift _drift;
|
||||
|
||||
const AuthRepository(super.db, 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(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
String getAccessToken() {
|
||||
return Store.get(StoreKey.accessToken);
|
||||
}
|
||||
|
||||
bool getEndpointSwitchingFeature() {
|
||||
return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false;
|
||||
}
|
||||
|
||||
String? getPreferredWifiName() {
|
||||
return Store.tryGet(StoreKey.preferredWifiName);
|
||||
}
|
||||
|
||||
String? getLocalEndpoint() {
|
||||
return Store.tryGet(StoreKey.localEndpoint);
|
||||
}
|
||||
|
||||
List<AuxilaryEndpoint> getExternalEndpointList() {
|
||||
final jsonString = Store.tryGet(StoreKey.externalEndpointList);
|
||||
|
||||
if (jsonString == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> jsonList = jsonDecode(jsonString);
|
||||
final endpointList = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
|
||||
|
||||
return endpointList;
|
||||
}
|
||||
}
|
||||
61
mobile/lib/repositories/auth_api.repository.dart
Normal file
61
mobile/lib/repositories/auth_api.repository.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final authApiRepositoryProvider = Provider((ref) => AuthApiRepository(ref.watch(apiServiceProvider)));
|
||||
|
||||
class AuthApiRepository extends ApiRepository {
|
||||
final ApiService _apiService;
|
||||
|
||||
AuthApiRepository(this._apiService);
|
||||
|
||||
Future<void> changePassword(String newPassword) async {
|
||||
await _apiService.usersApi.updateMyUser(UserUpdateMeDto(password: newPassword));
|
||||
}
|
||||
|
||||
Future<LoginResponse> login(String email, String password) async {
|
||||
final loginResponseDto = await checkNull(
|
||||
_apiService.authenticationApi.login(LoginCredentialDto(email: email, password: password)),
|
||||
);
|
||||
|
||||
return _mapLoginReponse(loginResponseDto);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
if (_apiService.apiClient.basePath.isEmpty) return;
|
||||
|
||||
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
||||
}
|
||||
|
||||
_mapLoginReponse(LoginResponseDto dto) {
|
||||
return LoginResponse(
|
||||
accessToken: dto.accessToken,
|
||||
isAdmin: dto.isAdmin,
|
||||
name: dto.name,
|
||||
profileImagePath: dto.profileImagePath,
|
||||
shouldChangePassword: dto.shouldChangePassword,
|
||||
userEmail: dto.userEmail,
|
||||
userId: dto.userId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> unlockPinCode(String pinCode) async {
|
||||
try {
|
||||
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: pinCode));
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setupPinCode(String pinCode) {
|
||||
return _apiService.authenticationApi.setupPinCode(PinCodeSetupDto(pinCode: pinCode));
|
||||
}
|
||||
|
||||
Future<void> lockPinCode() {
|
||||
return _apiService.authenticationApi.lockAuthSession();
|
||||
}
|
||||
}
|
||||
32
mobile/lib/repositories/backup.repository.dart
Normal file
32
mobile/lib/repositories/backup.repository.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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));
|
||||
}
|
||||
24
mobile/lib/repositories/biometric.repository.dart
Normal file
24
mobile/lib/repositories/biometric.repository.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
final biometricRepositoryProvider = Provider((ref) => BiometricRepository(LocalAuthentication()));
|
||||
|
||||
class BiometricRepository {
|
||||
final LocalAuthentication _localAuth;
|
||||
|
||||
const BiometricRepository(this._localAuth);
|
||||
|
||||
Future<BiometricStatus> getStatus() async {
|
||||
final bool canAuthenticateWithBiometrics = await _localAuth.canCheckBiometrics;
|
||||
final bool canAuthenticate = canAuthenticateWithBiometrics || await _localAuth.isDeviceSupported();
|
||||
final availableBiometric = await _localAuth.getAvailableBiometrics();
|
||||
|
||||
return BiometricStatus(canAuthenticate: canAuthenticate, availableBiometrics: availableBiometric);
|
||||
}
|
||||
|
||||
Future<bool> authenticate(String? message) async {
|
||||
return _localAuth.authenticate(localizedReason: message ?? 'please_auth_to_access'.tr());
|
||||
}
|
||||
}
|
||||
25
mobile/lib/repositories/database.repository.dart
Normal file
25
mobile/lib/repositories/database.repository.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
137
mobile/lib/repositories/download.repository.dart
Normal file
137
mobile/lib/repositories/download.repository.dart
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
|
||||
|
||||
class DownloadRepository {
|
||||
static final _downloader = FileDownloader();
|
||||
static final _dummyTask = DownloadTask(
|
||||
taskId: 'dummy',
|
||||
url: '',
|
||||
filename: 'dummy',
|
||||
group: '',
|
||||
updates: Updates.statusAndProgress,
|
||||
);
|
||||
static final _dummyMetadata = {'part': LivePhotosPart.image.index, 'id': ''};
|
||||
|
||||
void Function(TaskStatusUpdate)? onImageDownloadStatus;
|
||||
|
||||
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
|
||||
|
||||
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
|
||||
|
||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||
|
||||
DownloadRepository() {
|
||||
_downloader.registerCallbacks(
|
||||
group: kDownloadGroupImage,
|
||||
taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
|
||||
_downloader.registerCallbacks(
|
||||
group: kDownloadGroupVideo,
|
||||
taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
|
||||
_downloader.registerCallbacks(
|
||||
group: kDownloadGroupLivePhoto,
|
||||
taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
|
||||
return _downloader.enqueueAll(tasks);
|
||||
}
|
||||
|
||||
Future<void> deleteAllTrackingRecords() {
|
||||
return _downloader.database.deleteAllRecords();
|
||||
}
|
||||
|
||||
Future<bool> cancel(String id) {
|
||||
return _downloader.cancelTaskWithId(id);
|
||||
}
|
||||
|
||||
Future<List<TaskRecord>> getLiveVideoTasks() {
|
||||
return _downloader.database.allRecordsWithStatus(TaskStatus.complete, group: kDownloadGroupLivePhoto);
|
||||
}
|
||||
|
||||
Future<void> deleteRecordsWithIds(List<String> ids) {
|
||||
return _downloader.database.deleteRecordsWithIds(ids);
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAllAssets(List<RemoteAsset> assets) async {
|
||||
if (assets.isEmpty) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
final length = Platform.isAndroid ? assets.length : assets.length * 2;
|
||||
final tasks = List.filled(length, _dummyTask);
|
||||
int taskIndex = 0;
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
for (final asset in assets) {
|
||||
if (!asset.isRemoteOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final id = asset.id;
|
||||
final livePhotoVideoId = asset.livePhotoVideoId;
|
||||
final isVideo = asset.isVideo;
|
||||
final url = getOriginalUrlForRemoteId(id);
|
||||
|
||||
// on iOS it cannot link the image, check if the filename has .MP extension
|
||||
// to avoid downloading the video part
|
||||
final isAndroidMotionPhoto = asset.name.contains(".MP");
|
||||
|
||||
if (Platform.isAndroid || livePhotoVideoId == null || isVideo || isAndroidMotionPhoto) {
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: id,
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: asset.name,
|
||||
updates: Updates.statusAndProgress,
|
||||
group: isVideo ? kDownloadGroupVideo : kDownloadGroupImage,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
_dummyMetadata['part'] = LivePhotosPart.image.index;
|
||||
_dummyMetadata['id'] = id;
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: id,
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: asset.name,
|
||||
updates: Updates.statusAndProgress,
|
||||
group: kDownloadGroupLivePhoto,
|
||||
metaData: json.encode(_dummyMetadata),
|
||||
);
|
||||
|
||||
_dummyMetadata['part'] = LivePhotosPart.video.index;
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: livePhotoVideoId,
|
||||
url: getOriginalUrlForRemoteId(livePhotoVideoId),
|
||||
headers: headers,
|
||||
filename: asset.name.toUpperCase().replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
|
||||
updates: Updates.statusAndProgress,
|
||||
group: kDownloadGroupLivePhoto,
|
||||
metaData: json.encode(_dummyMetadata),
|
||||
);
|
||||
}
|
||||
if (taskIndex == 0) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
return _downloader.enqueueAll(tasks.slice(0, taskIndex));
|
||||
}
|
||||
}
|
||||
118
mobile/lib/repositories/drift_album_api_repository.dart
Normal file
118
mobile/lib/repositories/drift_album_api_repository.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
// ignore: import_rule_openapi
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final driftAlbumApiRepositoryProvider = Provider(
|
||||
(ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||
);
|
||||
|
||||
class DriftAlbumApiRepository extends ApiRepository {
|
||||
final AlbumsApi _api;
|
||||
|
||||
DriftAlbumApiRepository(this._api);
|
||||
|
||||
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 responseDto.toRemoteAlbum();
|
||||
}
|
||||
|
||||
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<({List<String> added, List<String> failed})> addAssets(String albumId, Iterable<String> assetIds) async {
|
||||
final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList())));
|
||||
final List<String> added = [], failed = [];
|
||||
for (final dto in response) {
|
||||
if (dto.success) {
|
||||
added.add(dto.id);
|
||||
} else {
|
||||
failed.add(dto.id);
|
||||
}
|
||||
}
|
||||
|
||||
return (added: added, failed: failed);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? description,
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
}) async {
|
||||
AssetOrder? apiOrder;
|
||||
if (order != null) {
|
||||
apiOrder = order == AlbumAssetOrder.asc ? AssetOrder.asc : AssetOrder.desc;
|
||||
}
|
||||
|
||||
final responseDto = await checkNull(
|
||||
_api.updateAlbumInfo(
|
||||
albumId,
|
||||
UpdateAlbumDto(
|
||||
albumName: name,
|
||||
description: description,
|
||||
albumThumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: apiOrder,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return responseDto.toRemoteAlbum();
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) {
|
||||
return _api.deleteAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> 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 response.toRemoteAlbum();
|
||||
}
|
||||
|
||||
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||
await _api.removeUserFromAlbum(albumId, userId);
|
||||
}
|
||||
|
||||
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
|
||||
final response = await checkNull(_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: isEnabled)));
|
||||
return response.isActivityEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
extension on AlbumResponseDto {
|
||||
RemoteAlbum toRemoteAlbum() {
|
||||
return RemoteAlbum(
|
||||
id: id,
|
||||
name: albumName,
|
||||
ownerId: owner.id,
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
thumbnailAssetId: albumThumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
||||
assetCount: assetCount,
|
||||
ownerName: owner.name,
|
||||
isShared: albumUsers.length > 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
27
mobile/lib/repositories/etag.repository.dart
Normal file
27
mobile/lib/repositories/etag.repository.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
52
mobile/lib/repositories/file_media.repository.dart
Normal file
52
mobile/lib/repositories/file_media.repository.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
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);
|
||||
|
||||
return LocalAsset(
|
||||
id: entity.id,
|
||||
name: title,
|
||||
type: AssetType.image,
|
||||
createdAt: entity.createDateTime,
|
||||
updatedAt: entity.modifiedDateTime,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Asset?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
|
||||
final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
|
||||
return AssetMediaRepository.toAsset(entity);
|
||||
}
|
||||
|
||||
Future<Asset?> 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);
|
||||
}
|
||||
|
||||
Future<Asset?> saveVideo(File file, {required String title, String? relativePath}) async {
|
||||
final entity = await PhotoManager.editor.saveVideo(file, title: title, relativePath: relativePath);
|
||||
return AssetMediaRepository.toAsset(entity);
|
||||
}
|
||||
|
||||
Future<void> clearFileCache() => PhotoManager.clearFileCache();
|
||||
|
||||
Future<void> enableBackgroundAccess() => PhotoManager.setIgnorePermissionCheck(true);
|
||||
|
||||
Future<void> requestExtendedPermissions() => PhotoManager.requestPermissionExtend();
|
||||
}
|
||||
35
mobile/lib/repositories/folder_api.repository.dart
Normal file
35
mobile/lib/repositories/folder_api.repository.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.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:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final folderApiRepositoryProvider = Provider((ref) => FolderApiRepository(ref.watch(apiServiceProvider).viewApi));
|
||||
|
||||
class FolderApiRepository extends ApiRepository {
|
||||
final ViewsApi _api;
|
||||
final Logger _log = Logger("FolderApiRepository");
|
||||
|
||||
FolderApiRepository(this._api);
|
||||
|
||||
Future<List<String>> getAllUniquePaths() async {
|
||||
try {
|
||||
final list = await _api.getUniqueOriginalPaths();
|
||||
return list ?? [];
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to fetch unique original links", e, stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>> getAssetsForPath(String? path) async {
|
||||
try {
|
||||
final list = await _api.getAssetsByOriginalPath(path ?? '/');
|
||||
return list != null ? list.map(Asset.remote).toList() : [];
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to fetch Assets by original path", e, stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
68
mobile/lib/repositories/gcast.repository.dart
Normal file
68
mobile/lib/repositories/gcast.repository.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:cast/device.dart';
|
||||
import 'package:cast/session.dart';
|
||||
import 'package:cast/session_manager.dart';
|
||||
import 'package:cast/discovery_service.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final gCastRepositoryProvider = Provider((_) {
|
||||
return GCastRepository();
|
||||
});
|
||||
|
||||
class GCastRepository {
|
||||
CastSession? _castSession;
|
||||
|
||||
void Function(CastSessionState)? onCastStatus;
|
||||
void Function(Map<String, dynamic>)? onCastMessage;
|
||||
|
||||
Map<String, dynamic>? _receiverStatus;
|
||||
|
||||
GCastRepository();
|
||||
|
||||
Future<void> connect(CastDevice device) async {
|
||||
_castSession = await CastSessionManager().startSession(device);
|
||||
|
||||
_castSession?.stateStream.listen((state) {
|
||||
onCastStatus?.call(state);
|
||||
});
|
||||
|
||||
_castSession?.messageStream.listen((message) {
|
||||
onCastMessage?.call(message);
|
||||
if (message['type'] == 'RECEIVER_STATUS') {
|
||||
_receiverStatus = message;
|
||||
}
|
||||
});
|
||||
|
||||
// open the default receiver
|
||||
sendMessage(CastSession.kNamespaceReceiver, {'type': 'LAUNCH', 'appId': 'CC1AD845'});
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
final sessionID = getSessionId();
|
||||
|
||||
sendMessage(CastSession.kNamespaceReceiver, {'type': "STOP", "sessionId": sessionID});
|
||||
|
||||
// wait 500ms to ensure the stop command is processed
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
await _castSession?.close();
|
||||
}
|
||||
|
||||
String? getSessionId() {
|
||||
if (_receiverStatus == null) {
|
||||
return null;
|
||||
}
|
||||
return _receiverStatus!['status']['applications'][0]['sessionId'];
|
||||
}
|
||||
|
||||
void sendMessage(String namespace, Map<String, dynamic> message) {
|
||||
if (_castSession == null) {
|
||||
throw Exception("Cast session is not established");
|
||||
}
|
||||
|
||||
_castSession!.sendMessage(namespace, message);
|
||||
}
|
||||
|
||||
Future<List<CastDevice>> listDestinations() async {
|
||||
return await CastDiscoveryService().search(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
}
|
||||
51
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
51
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/services/local_files_manager.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final localFilesManagerRepositoryProvider = Provider(
|
||||
(ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)),
|
||||
);
|
||||
|
||||
class LocalFilesManagerRepository {
|
||||
LocalFilesManagerRepository(this._service);
|
||||
|
||||
final Logger _logger = Logger('LocalFilesManagerRepo');
|
||||
final LocalFilesManagerService _service;
|
||||
|
||||
Future<bool> moveToTrash(List<String> mediaUrls) async {
|
||||
return await _service.moveToTrash(mediaUrls);
|
||||
}
|
||||
|
||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||
return await _service.restoreFromTrash(fileName, type);
|
||||
}
|
||||
|
||||
Future<bool> requestManageMediaPermission() async {
|
||||
return await _service.requestManageMediaPermission();
|
||||
}
|
||||
|
||||
Future<bool> hasManageMediaPermission() async {
|
||||
return await _service.hasManageMediaPermission();
|
||||
}
|
||||
|
||||
Future<bool> manageMediaPermission() async {
|
||||
return await _service.manageMediaPermission();
|
||||
}
|
||||
|
||||
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
|
||||
final restoredIds = <String>[];
|
||||
for (final asset in assets) {
|
||||
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
||||
try {
|
||||
final result = await _service.restoreFromTrashById(asset.id, asset.type.index);
|
||||
if (result) {
|
||||
restoredIds.add(asset.id);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.warning("Restoring failure: $e");
|
||||
}
|
||||
}
|
||||
return restoredIds;
|
||||
}
|
||||
}
|
||||
34
mobile/lib/repositories/network.repository.dart
Normal file
34
mobile/lib/repositories/network.repository.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
|
||||
final networkRepositoryProvider = Provider((_) {
|
||||
final networkInfo = NetworkInfo();
|
||||
|
||||
return NetworkRepository(networkInfo);
|
||||
});
|
||||
|
||||
class NetworkRepository {
|
||||
final NetworkInfo _networkInfo;
|
||||
|
||||
const NetworkRepository(this._networkInfo);
|
||||
|
||||
Future<String?> getWifiName() {
|
||||
if (Platform.isAndroid) {
|
||||
// remove quote around the return value on Android
|
||||
// https://github.com/fluttercommunity/plus_plugins/tree/main/packages/network_info_plus/network_info_plus#android
|
||||
return _networkInfo.getWifiName().then((value) {
|
||||
if (value != null) {
|
||||
return value.replaceAll(RegExp(r'"'), '');
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
return _networkInfo.getWifiName();
|
||||
}
|
||||
|
||||
Future<String?> getWifiIp() {
|
||||
return _networkInfo.getWifiIP();
|
||||
}
|
||||
}
|
||||
34
mobile/lib/repositories/partner.repository.dart
Normal file
34
mobile/lib/repositories/partner.repository.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
mobile/lib/repositories/partner_api.repository.dart
Normal file
35
mobile/lib/repositories/partner_api.repository.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
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';
|
||||
|
||||
enum Direction { sharedWithMe, sharedByMe }
|
||||
|
||||
final partnerApiRepositoryProvider = Provider((ref) => PartnerApiRepository(ref.watch(apiServiceProvider).partnersApi));
|
||||
|
||||
class PartnerApiRepository extends ApiRepository {
|
||||
final PartnersApi _api;
|
||||
|
||||
PartnerApiRepository(this._api);
|
||||
|
||||
Future<List<UserDto>> getAll(Direction direction) async {
|
||||
final response = await checkNull(
|
||||
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.by : PartnerDirection.with_),
|
||||
);
|
||||
return response.map(UserConverter.fromPartnerDto).toList();
|
||||
}
|
||||
|
||||
Future<UserDto> create(String id) async {
|
||||
final dto = await checkNull(_api.createPartnerDeprecated(id));
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
|
||||
Future<void> delete(String id) => _api.removePartner(id);
|
||||
|
||||
Future<UserDto> update(String id, {required bool inTimeline}) async {
|
||||
final dto = await checkNull(_api.updatePartner(id, PartnerUpdateDto(inTimeline: inTimeline)));
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
}
|
||||
45
mobile/lib/repositories/permission.repository.dart
Normal file
45
mobile/lib/repositories/permission.repository.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
final permissionRepositoryProvider = Provider((_) {
|
||||
return const PermissionRepository();
|
||||
});
|
||||
|
||||
class PermissionRepository implements IPermissionRepository {
|
||||
const PermissionRepository();
|
||||
|
||||
@override
|
||||
Future<bool> hasLocationWhenInUsePermission() {
|
||||
return Permission.locationWhenInUse.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestLocationWhenInUsePermission() async {
|
||||
final result = await Permission.locationWhenInUse.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasLocationAlwaysPermission() {
|
||||
return Permission.locationAlways.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestLocationAlwaysPermission() async {
|
||||
final result = await Permission.locationAlways.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> openSettings() {
|
||||
return openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
abstract interface class IPermissionRepository {
|
||||
Future<bool> hasLocationWhenInUsePermission();
|
||||
Future<bool> requestLocationWhenInUsePermission();
|
||||
Future<bool> hasLocationAlwaysPermission();
|
||||
Future<bool> requestLocationAlwaysPermission();
|
||||
Future<bool> openSettings();
|
||||
}
|
||||
33
mobile/lib/repositories/person_api.repository.dart
Normal file
33
mobile/lib/repositories/person_api.repository.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final personApiRepositoryProvider = Provider((ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi));
|
||||
|
||||
class PersonApiRepository extends ApiRepository {
|
||||
final PeopleApi _api;
|
||||
|
||||
PersonApiRepository(this._api);
|
||||
|
||||
Future<List<PersonDto>> getAll() async {
|
||||
final dto = await checkNull(_api.getAllPeople());
|
||||
return dto.people.map(_toPerson).toList();
|
||||
}
|
||||
|
||||
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
||||
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
||||
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
|
||||
final response = await checkNull(_api.updatePerson(id, dto));
|
||||
return _toPerson(response);
|
||||
}
|
||||
|
||||
static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
|
||||
birthDate: dto.birthDate,
|
||||
id: dto.id,
|
||||
isHidden: dto.isHidden,
|
||||
name: dto.name,
|
||||
thumbnailPath: dto.thumbnailPath,
|
||||
);
|
||||
}
|
||||
22
mobile/lib/repositories/secure_storage.repository.dart
Normal file
22
mobile/lib/repositories/secure_storage.repository.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final secureStorageRepositoryProvider = Provider((ref) => const SecureStorageRepository(FlutterSecureStorage()));
|
||||
|
||||
class SecureStorageRepository {
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
const SecureStorageRepository(this._secureStorage);
|
||||
|
||||
Future<String?> read(String key) {
|
||||
return _secureStorage.read(key: key);
|
||||
}
|
||||
|
||||
Future<void> write(String key, String value) {
|
||||
return _secureStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
Future<void> delete(String key) {
|
||||
return _secureStorage.delete(key: key);
|
||||
}
|
||||
}
|
||||
32
mobile/lib/repositories/sessions_api.repository.dart
Normal file
32
mobile/lib/repositories/sessions_api.repository.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final sessionsAPIRepositoryProvider = Provider(
|
||||
(ref) => SessionsAPIRepository(ref.watch(apiServiceProvider).sessionsApi),
|
||||
);
|
||||
|
||||
class SessionsAPIRepository extends ApiRepository {
|
||||
final SessionsApi _api;
|
||||
|
||||
SessionsAPIRepository(this._api);
|
||||
|
||||
Future<SessionCreateResponse> createSession(String deviceType, String deviceOS, {int? duration}) async {
|
||||
final dto = await checkNull(
|
||||
_api.createSession(SessionCreateDto(deviceType: deviceType, deviceOS: deviceOS, duration: duration)),
|
||||
);
|
||||
|
||||
return SessionCreateResponse(
|
||||
id: dto.id,
|
||||
current: dto.current,
|
||||
deviceType: deviceType,
|
||||
deviceOS: deviceOS,
|
||||
expiresAt: dto.expiresAt,
|
||||
createdAt: dto.createdAt,
|
||||
updatedAt: dto.updatedAt,
|
||||
token: dto.token,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
mobile/lib/repositories/share_handler.repository.dart
Normal file
56
mobile/lib/repositories/share_handler.repository.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:share_handler/share_handler.dart';
|
||||
|
||||
final shareHandlerRepositoryProvider = Provider((ref) => ShareHandlerRepository());
|
||||
|
||||
class ShareHandlerRepository {
|
||||
ShareHandlerRepository();
|
||||
|
||||
void Function(List<ShareIntentAttachment> attachments)? onSharedMedia;
|
||||
|
||||
Future<void> init() async {
|
||||
final handler = ShareHandlerPlatform.instance;
|
||||
final media = await handler.getInitialSharedMedia();
|
||||
|
||||
if (media != null && media.attachments != null) {
|
||||
onSharedMedia?.call(_buildPayload(media.attachments!));
|
||||
}
|
||||
|
||||
handler.sharedMediaStream.listen((SharedMedia media) {
|
||||
if (media.attachments != null) {
|
||||
onSharedMedia?.call(_buildPayload(media.attachments!));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<ShareIntentAttachment> _buildPayload(List<SharedAttachment?> attachments) {
|
||||
final payload = <ShareIntentAttachment>[];
|
||||
|
||||
for (final attachment in attachments) {
|
||||
if (attachment == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final type = attachment.type == SharedAttachmentType.image
|
||||
? ShareIntentAttachmentType.image
|
||||
: ShareIntentAttachmentType.video;
|
||||
|
||||
final fileLength = File(attachment.path).lengthSync();
|
||||
|
||||
payload.add(
|
||||
ShareIntentAttachment(
|
||||
path: attachment.path,
|
||||
type: type,
|
||||
status: UploadStatus.enqueued,
|
||||
uploadProgress: 0.0,
|
||||
fileLength: fileLength,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
146
mobile/lib/repositories/timeline.repository.dart
Normal file
146
mobile/lib/repositories/timeline.repository.dart
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
207
mobile/lib/repositories/upload.repository.dart
Normal file
207
mobile/lib/repositories/upload.repository.dart
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class UploadTaskWithFile {
|
||||
final File file;
|
||||
final UploadTask task;
|
||||
|
||||
const UploadTaskWithFile({required this.file, required this.task});
|
||||
}
|
||||
|
||||
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
||||
|
||||
class UploadRepository {
|
||||
final Logger logger = Logger('UploadRepository');
|
||||
void Function(TaskStatusUpdate)? onUploadStatus;
|
||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||
|
||||
UploadRepository() {
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kBackupGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kBackupLivePhotoGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kManualUploadGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> enqueueBackground(UploadTask task) {
|
||||
return FileDownloader().enqueue(task);
|
||||
}
|
||||
|
||||
Future<List<bool>> enqueueBackgroundAll(List<UploadTask> tasks) {
|
||||
return FileDownloader().enqueueAll(tasks);
|
||||
}
|
||||
|
||||
Future<void> deleteDatabaseRecords(String group) {
|
||||
return FileDownloader().database.deleteAllRecords(group: group);
|
||||
}
|
||||
|
||||
Future<bool> cancelAll(String group) {
|
||||
return FileDownloader().cancelAll(group: group);
|
||||
}
|
||||
|
||||
Future<int> reset(String group) {
|
||||
return FileDownloader().reset(group: group);
|
||||
}
|
||||
|
||||
/// Get a list of tasks that are ENQUEUED or RUNNING
|
||||
Future<List<Task>> getActiveTasks(String group) {
|
||||
return FileDownloader().allTasks(group: group);
|
||||
}
|
||||
|
||||
Future<void> start() {
|
||||
return FileDownloader().start();
|
||||
}
|
||||
|
||||
Future<void> getUploadInfo() async {
|
||||
final [enqueuedTasks, runningTasks, canceledTasks, waitingTasks, pausedTasks] = await Future.wait([
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.enqueued, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.running, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.canceled, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.waitingToRetry, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
|
||||
]);
|
||||
|
||||
dPrint(
|
||||
() =>
|
||||
"""
|
||||
Upload Info:
|
||||
Enqueued: ${enqueuedTasks.length}
|
||||
Running: ${runningTasks.length}
|
||||
Canceled: ${canceledTasks.length}
|
||||
Waiting: ${waitingTasks.length}
|
||||
Paused: ${pausedTasks.length}
|
||||
""",
|
||||
);
|
||||
}
|
||||
|
||||
Future<UploadResult> uploadFile({
|
||||
required File file,
|
||||
required String originalFileName,
|
||||
required Map<String, String> headers,
|
||||
required Map<String, String> fields,
|
||||
required Client httpClient,
|
||||
required CancellationToken cancelToken,
|
||||
required void Function(int bytes, int totalBytes) onProgress,
|
||||
required String logContext,
|
||||
}) async {
|
||||
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
|
||||
try {
|
||||
final fileStream = file.openRead();
|
||||
final assetRawUploadData = MultipartFile("assetData", fileStream, file.lengthSync(), filename: originalFileName);
|
||||
|
||||
final baseRequest = _CustomMultipartRequest('POST', Uri.parse('$savedEndpoint/assets'), onProgress: onProgress);
|
||||
|
||||
baseRequest.headers.addAll(headers);
|
||||
baseRequest.fields.addAll(fields);
|
||||
baseRequest.files.add(assetRawUploadData);
|
||||
|
||||
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
|
||||
final responseBodyString = await response.stream.bytesToString();
|
||||
|
||||
if (![200, 201].contains(response.statusCode)) {
|
||||
String? errorMessage;
|
||||
|
||||
if (response.statusCode == 413) {
|
||||
errorMessage = 'Error(413) File is too large to upload';
|
||||
return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
final error = jsonDecode(responseBodyString);
|
||||
errorMessage = error['message'] ?? error['error'];
|
||||
} catch (_) {
|
||||
errorMessage = responseBodyString.isNotEmpty
|
||||
? responseBodyString
|
||||
: 'Upload failed with status ${response.statusCode}';
|
||||
}
|
||||
|
||||
return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
final responseBody = jsonDecode(responseBodyString);
|
||||
return UploadResult.success(remoteAssetId: responseBody['id'] as String);
|
||||
} catch (e) {
|
||||
return UploadResult.error(errorMessage: 'Failed to parse server response');
|
||||
}
|
||||
} on CancelledException {
|
||||
logger.warning("Upload $logContext was cancelled");
|
||||
return UploadResult.cancelled();
|
||||
} catch (error, stackTrace) {
|
||||
logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace");
|
||||
return UploadResult.error(errorMessage: error.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UploadResult {
|
||||
final bool isSuccess;
|
||||
final bool isCancelled;
|
||||
final String? remoteAssetId;
|
||||
final String? errorMessage;
|
||||
final int? statusCode;
|
||||
|
||||
const UploadResult({
|
||||
required this.isSuccess,
|
||||
required this.isCancelled,
|
||||
this.remoteAssetId,
|
||||
this.errorMessage,
|
||||
this.statusCode,
|
||||
});
|
||||
|
||||
factory UploadResult.success({required String remoteAssetId}) {
|
||||
return UploadResult(isSuccess: true, isCancelled: false, remoteAssetId: remoteAssetId);
|
||||
}
|
||||
|
||||
factory UploadResult.error({String? errorMessage, int? statusCode}) {
|
||||
return UploadResult(isSuccess: false, isCancelled: false, errorMessage: errorMessage, statusCode: statusCode);
|
||||
}
|
||||
|
||||
factory UploadResult.cancelled() {
|
||||
return const UploadResult(isSuccess: false, isCancelled: true);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultipartRequest extends MultipartRequest {
|
||||
_CustomMultipartRequest(super.method, super.url, {required this.onProgress});
|
||||
|
||||
final void Function(int bytes, int totalBytes) onProgress;
|
||||
|
||||
@override
|
||||
ByteStream finalize() {
|
||||
final byteStream = super.finalize();
|
||||
final total = contentLength;
|
||||
var bytes = 0;
|
||||
|
||||
final t = StreamTransformer.fromHandlers(
|
||||
handleData: (List<int> data, EventSink<List<int>> sink) {
|
||||
bytes += data.length;
|
||||
onProgress.call(bytes, total);
|
||||
sink.add(data);
|
||||
},
|
||||
);
|
||||
final stream = byteStream.transform(t);
|
||||
return ByteStream(stream);
|
||||
}
|
||||
}
|
||||
20
mobile/lib/repositories/widget.repository.dart
Normal file
20
mobile/lib/repositories/widget.repository.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:home_widget/home_widget.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final widgetRepositoryProvider = Provider((_) => const WidgetRepository());
|
||||
|
||||
class WidgetRepository {
|
||||
const WidgetRepository();
|
||||
|
||||
Future<void> saveData(String key, String value) async {
|
||||
await HomeWidget.saveWidgetData<String>(key, value);
|
||||
}
|
||||
|
||||
Future<void> refresh(String iosName, String androidName) async {
|
||||
await HomeWidget.updateWidget(iOSName: iosName, qualifiedAndroidName: androidName);
|
||||
}
|
||||
|
||||
Future<void> setAppGroupId(String appGroupId) async {
|
||||
await HomeWidget.setAppGroupId(appGroupId);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue