mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat(mobile): add album asset sync (#19522)
* feat(mobile): add album asset sync * add SyncAlbumToAssetDeleteV1 to openapi-spec * update delete queries to use where in statements * clear remote album when clear remote data * fix: bad merge * fix: bad merge * fix: _SyncAckV1 return type --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: wuzihao051119 <wuzihao051119@outlook.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
24a4cba953
commit
ea3a14ed25
39 changed files with 744 additions and 93 deletions
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
Binary file not shown.
79
mobile/lib/domain/models/album/album.model.dart
Normal file
79
mobile/lib/domain/models/album/album.model.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
enum AlbumAssetOrder {
|
||||||
|
// do not change this order!
|
||||||
|
asc,
|
||||||
|
desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AlbumUserRole {
|
||||||
|
// do not change this order!
|
||||||
|
editor,
|
||||||
|
viewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model for an album stored in the server
|
||||||
|
class Album {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String ownerId;
|
||||||
|
final String description;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String? thumbnailAssetId;
|
||||||
|
final bool isActivityEnabled;
|
||||||
|
final AlbumAssetOrder order;
|
||||||
|
|
||||||
|
const Album({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.description,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.thumbnailAssetId,
|
||||||
|
required this.isActivityEnabled,
|
||||||
|
required this.order,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''Album {
|
||||||
|
id: $id,
|
||||||
|
name: $name,
|
||||||
|
ownerId: $ownerId,
|
||||||
|
description: $description,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
updatedAt: $updatedAt,
|
||||||
|
isActivityEnabled: $isActivityEnabled,
|
||||||
|
order: $order,
|
||||||
|
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
||||||
|
}''';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! Album) return false;
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return id == other.id &&
|
||||||
|
name == other.name &&
|
||||||
|
ownerId == other.ownerId &&
|
||||||
|
description == other.description &&
|
||||||
|
createdAt == other.createdAt &&
|
||||||
|
updatedAt == other.updatedAt &&
|
||||||
|
thumbnailAssetId == other.thumbnailAssetId &&
|
||||||
|
isActivityEnabled == other.isActivityEnabled &&
|
||||||
|
order == other.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
|
description.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
updatedAt.hashCode ^
|
||||||
|
thumbnailAssetId.hashCode ^
|
||||||
|
isActivityEnabled.hashCode ^
|
||||||
|
order.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,8 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,76 @@ class SyncStreamService {
|
||||||
case SyncEntityType.assetExifV1:
|
case SyncEntityType.assetExifV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||||
case SyncEntityType.partnerAssetV1:
|
case SyncEntityType.partnerAssetV1:
|
||||||
return _syncStreamRepository.updatePartnerAssetsV1(data.cast());
|
return _syncStreamRepository.updateAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerAssetBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner backfill',
|
||||||
|
);
|
||||||
case SyncEntityType.partnerAssetDeleteV1:
|
case SyncEntityType.partnerAssetDeleteV1:
|
||||||
return _syncStreamRepository.deletePartnerAssetsV1(data.cast());
|
return _syncStreamRepository.deleteAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: "partner",
|
||||||
|
);
|
||||||
case SyncEntityType.partnerAssetExifV1:
|
case SyncEntityType.partnerAssetExifV1:
|
||||||
return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast());
|
return _syncStreamRepository.updateAssetsExifV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerAssetExifBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumV1:
|
||||||
|
return _syncStreamRepository.updateAlbumsV1(data.cast());
|
||||||
|
case SyncEntityType.albumDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteAlbumsV1(data.cast());
|
||||||
|
case SyncEntityType.albumUserV1:
|
||||||
|
return _syncStreamRepository.updateAlbumUsersV1(data.cast());
|
||||||
|
case SyncEntityType.albumUserBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAlbumUsersV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumUserDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
||||||
|
case SyncEntityType.albumAssetV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'album',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumAssetBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'album backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumAssetExifV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'album',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumAssetExifBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'album backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumToAssetV1:
|
||||||
|
return _syncStreamRepository.updateAlbumToAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.albumToAssetBackfillV1:
|
||||||
|
return _syncStreamRepository.updateAlbumToAssetsV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.albumToAssetDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteAlbumToAssetsV1(data.cast());
|
||||||
|
// No-op. SyncAckV1 entities are checkpoints in the sync stream
|
||||||
|
// to acknowledge that the client has processed all the backfill events
|
||||||
|
case SyncEntityType.syncAckV1:
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_logger.warning("Unknown sync data type: $type");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,13 @@ class TimelineFactory {
|
||||||
bucketSource: () =>
|
bucketSource: () =>
|
||||||
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
|
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TimelineService remoteAlbum({required String albumId}) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getRemoteBucketAssets(albumId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
|
|
|
||||||
Binary file not shown.
34
mobile/lib/infrastructure/entities/remote_album.entity.dart
Normal file
34
mobile/lib/infrastructure/entities/remote_album.entity.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const RemoteAlbumEntity();
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
TextColumn get name => text()();
|
||||||
|
|
||||||
|
TextColumn get description => text().withDefault(const Constant(''))();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
TextColumn get ownerId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get thumbnailAssetId => text()
|
||||||
|
.references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull)
|
||||||
|
.nullable()();
|
||||||
|
|
||||||
|
BoolColumn get isActivityEnabled =>
|
||||||
|
boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
|
IntColumn get order => intEnum<AlbumAssetOrder>()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
BIN
mobile/lib/infrastructure/entities/remote_album.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/remote_album.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const RemoteAlbumAssetEntity();
|
||||||
|
|
||||||
|
TextColumn get assetId =>
|
||||||
|
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get albumId =>
|
||||||
|
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {assetId, albumId};
|
||||||
|
}
|
||||||
BIN
mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumUserEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const RemoteAlbumUserEntity();
|
||||||
|
|
||||||
|
TextColumn get albumId =>
|
||||||
|
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get userId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
IntColumn get role => intEnum<AlbumUserRole>()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {albumId, userId};
|
||||||
|
}
|
||||||
BIN
mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
@ -34,3 +35,21 @@ class RemoteAssetEntity extends Table
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||||
|
Asset toDto() => Asset(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
checksum: checksum,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
thumbHash: thumbHash,
|
||||||
|
visibility: visibility,
|
||||||
|
localId: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
|
|
@ -40,6 +43,9 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||||
LocalAlbumAssetEntity,
|
LocalAlbumAssetEntity,
|
||||||
RemoteAssetEntity,
|
RemoteAssetEntity,
|
||||||
RemoteExifEntity,
|
RemoteExifEntity,
|
||||||
|
RemoteAlbumEntity,
|
||||||
|
RemoteAlbumAssetEntity,
|
||||||
|
RemoteAlbumUserEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
enum SortRemoteAlbumsBy { id }
|
||||||
|
|
||||||
|
class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
||||||
|
final query = _db.remoteAlbumEntity.select();
|
||||||
|
|
||||||
|
if (sortBy.isNotEmpty) {
|
||||||
|
final orderings = <OrderClauseGenerator<$RemoteAlbumEntityTable>>[];
|
||||||
|
for (final sort in sortBy) {
|
||||||
|
orderings.add(
|
||||||
|
switch (sort) {
|
||||||
|
SortRemoteAlbumsBy.id => (row) => OrderingTerm.asc(row.id),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
query.orderBy(orderings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on RemoteAlbumEntityData {
|
||||||
|
Album toDto() {
|
||||||
|
return Album(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
ownerId: ownerId,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
description: description,
|
||||||
|
thumbnailAssetId: thumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled,
|
||||||
|
order: order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,11 +42,16 @@ class SyncApiRepository {
|
||||||
SyncStreamDto(
|
SyncStreamDto(
|
||||||
types: [
|
types: [
|
||||||
SyncRequestType.usersV1,
|
SyncRequestType.usersV1,
|
||||||
SyncRequestType.partnersV1,
|
|
||||||
SyncRequestType.assetsV1,
|
SyncRequestType.assetsV1,
|
||||||
SyncRequestType.partnerAssetsV1,
|
|
||||||
SyncRequestType.assetExifsV1,
|
SyncRequestType.assetExifsV1,
|
||||||
|
SyncRequestType.partnersV1,
|
||||||
|
SyncRequestType.partnerAssetsV1,
|
||||||
SyncRequestType.partnerAssetExifsV1,
|
SyncRequestType.partnerAssetExifsV1,
|
||||||
|
SyncRequestType.albumsV1,
|
||||||
|
SyncRequestType.albumUsersV1,
|
||||||
|
SyncRequestType.albumAssetsV1,
|
||||||
|
SyncRequestType.albumAssetExifsV1,
|
||||||
|
SyncRequestType.albumToAssetsV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
|
|
@ -135,6 +140,25 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||||
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.albumV1: SyncAlbumV1.fromJson,
|
||||||
|
SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson,
|
||||||
|
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
||||||
|
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||||
|
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetExifV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
||||||
|
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class _SyncAckV1 {
|
||||||
|
static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart' as api show AssetVisibility;
|
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole;
|
||||||
import 'package:openapi/api.dart' hide AssetVisibility;
|
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole;
|
||||||
|
|
||||||
class SyncStreamRepository extends DriftDatabaseRepository {
|
class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
final Logger _logger = Logger('DriftSyncStreamRepository');
|
final Logger _logger = Logger('DriftSyncStreamRepository');
|
||||||
|
|
@ -17,16 +21,10 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
|
|
||||||
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) {
|
await _db.userEntity
|
||||||
for (final user in data) {
|
.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
|
||||||
batch.delete(
|
|
||||||
_db.userEntity,
|
|
||||||
UserEntityCompanion(id: Value(user.userId)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error while processing SyncUserDeleteV1', error, stack);
|
_logger.severe('Error: SyncUserDeleteV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +46,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error while processing SyncUserV1', error, stack);
|
_logger.severe('Error: SyncUserV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +65,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe('Error while processing SyncPartnerDeleteV1', e, s);
|
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,67 +88,30 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe('Error while processing SyncPartnerV1', e, s);
|
_logger.severe('Error: SyncPartnerV1', e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
|
Future<void> deleteAssetsV1(
|
||||||
|
Iterable<SyncAssetDeleteV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _deleteAssetsV1(data);
|
await _db.remoteAssetEntity
|
||||||
|
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.severe('Error while processing deleteAssetsV1', e, s);
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
|
Future<void> updateAssetsV1(
|
||||||
|
Iterable<SyncAssetV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _updateAssetsV1(data);
|
await _db.batch((batch) {
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updateAssetsV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
|
|
||||||
try {
|
|
||||||
await _deleteAssetsV1(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing deletePartnerAssetsV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
|
|
||||||
try {
|
|
||||||
await _updateAssetsV1(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updatePartnerAssetsV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
|
||||||
try {
|
|
||||||
await _updateAssetExifV1(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updateAssetsExifV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
|
||||||
try {
|
|
||||||
await _updateAssetExifV1(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.severe('Error while processing updatePartnerAssetsExifV1', e, s);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateAssetsV1(Iterable<SyncAssetV1> data) =>
|
|
||||||
_db.batch((batch) {
|
|
||||||
for (final asset in data) {
|
for (final asset in data) {
|
||||||
final companion = RemoteAssetEntityCompanion(
|
final companion = RemoteAssetEntityCompanion(
|
||||||
name: Value(asset.originalFileName),
|
name: Value(asset.originalFileName),
|
||||||
|
|
@ -175,19 +136,18 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _deleteAssetsV1(Iterable<SyncAssetDeleteV1> assets) =>
|
Future<void> updateAssetsExifV1(
|
||||||
_db.batch((batch) {
|
Iterable<SyncAssetExifV1> data, {
|
||||||
for (final asset in assets) {
|
String debugLabel = 'user',
|
||||||
batch.delete(
|
}) async {
|
||||||
_db.remoteAssetEntity,
|
try {
|
||||||
RemoteAssetEntityCompanion(id: Value(asset.assetId)),
|
await _db.batch((batch) {
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> _updateAssetExifV1(Iterable<SyncAssetExifV1> data) =>
|
|
||||||
_db.batch((batch) {
|
|
||||||
for (final exif in data) {
|
for (final exif in data) {
|
||||||
final companion = RemoteExifEntityCompanion(
|
final companion = RemoteExifEntityCompanion(
|
||||||
city: Value(exif.city),
|
city: Value(exif.city),
|
||||||
|
|
@ -219,6 +179,141 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.remoteAlbumEntity
|
||||||
|
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: deleteAlbumsV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAlbumsV1(Iterable<SyncAlbumV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
final companion = RemoteAlbumEntityCompanion(
|
||||||
|
name: Value(album.name),
|
||||||
|
description: Value(album.description),
|
||||||
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
|
order: Value(album.order.toAlbumAssetOrder()),
|
||||||
|
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||||
|
ownerId: Value(album.ownerId),
|
||||||
|
createdAt: Value(album.createdAt),
|
||||||
|
updatedAt: Value(album.updatedAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.remoteAlbumEntity,
|
||||||
|
companion.copyWith(id: Value(album.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: updateAlbumsV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlbumUsersV1(Iterable<SyncAlbumUserDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
batch.delete(
|
||||||
|
_db.remoteAlbumUserEntity,
|
||||||
|
RemoteAlbumUserEntityCompanion(
|
||||||
|
albumId: Value(album.albumId),
|
||||||
|
userId: Value(album.userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: deleteAlbumUsersV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAlbumUsersV1(
|
||||||
|
Iterable<SyncAlbumUserV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
final companion = RemoteAlbumUserEntityCompanion(
|
||||||
|
role: Value(album.role.toAlbumUserRole()),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.remoteAlbumUserEntity,
|
||||||
|
companion.copyWith(
|
||||||
|
albumId: Value(album.albumId),
|
||||||
|
userId: Value(album.userId),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlbumToAssetsV1(
|
||||||
|
Iterable<SyncAlbumToAssetDeleteV1> data,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
batch.delete(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
RemoteAlbumAssetEntityCompanion(
|
||||||
|
albumId: Value(album.albumId),
|
||||||
|
assetId: Value(album.assetId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAlbumToAssetsV1(
|
||||||
|
Iterable<SyncAlbumToAssetV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
final companion = RemoteAlbumAssetEntityCompanion(
|
||||||
|
albumId: Value(album.albumId),
|
||||||
|
assetId: Value(album.assetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
companion,
|
||||||
|
onConflict: DoNothing(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AssetTypeEnum {
|
extension on AssetTypeEnum {
|
||||||
|
|
@ -231,6 +326,22 @@ extension on AssetTypeEnum {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on AssetOrder {
|
||||||
|
AlbumAssetOrder toAlbumAssetOrder() => switch (this) {
|
||||||
|
AssetOrder.asc => AlbumAssetOrder.asc,
|
||||||
|
AssetOrder.desc => AlbumAssetOrder.desc,
|
||||||
|
_ => throw Exception('Unknown AssetOrder value: $this'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on api.AlbumUserRole {
|
||||||
|
AlbumUserRole toAlbumUserRole() => switch (this) {
|
||||||
|
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||||
|
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
|
||||||
|
_ => throw Exception('Unknown AlbumUserRole value: $this'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
extension on api.AssetVisibility {
|
extension on api.AssetVisibility {
|
||||||
AssetVisibility toAssetVisibility() => switch (this) {
|
AssetVisibility toAssetVisibility() => switch (this) {
|
||||||
api.AssetVisibility.timeline => AssetVisibility.timeline,
|
api.AssetVisibility.timeline => AssetVisibility.timeline,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:stream_transform/stream_transform.dart';
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
|
|
@ -139,6 +140,62 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchRemoteBucket(
|
||||||
|
String albumId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAlbumAssetEntity
|
||||||
|
.count(where: (row) => row.albumId.equals(albumId))
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.assetId
|
||||||
|
.equalsExp(_db.remoteAssetEntity.id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getRemoteBucketAssets(
|
||||||
|
String albumId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.assetId
|
||||||
|
.equalsExp(_db.remoteAssetEntity.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Expression<DateTime> {
|
extension on Expression<DateTime> {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ final _features = [
|
||||||
final db = ref.read(driftProvider);
|
final db = ref.read(driftProvider);
|
||||||
await db.remoteAssetEntity.deleteAll();
|
await db.remoteAssetEntity.deleteAll();
|
||||||
await db.remoteExifEntity.deleteAll();
|
await db.remoteExifEntity.deleteAll();
|
||||||
|
await db.remoteAlbumEntity.deleteAll();
|
||||||
|
await db.remoteAlbumUserEntity.deleteAll();
|
||||||
|
await db.remoteAlbumAssetEntity.deleteAll();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,10 @@ class _Summary extends StatelessWidget {
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
subtitle = const Icon(Icons.error_rounded);
|
subtitle = const Icon(Icons.error_rounded);
|
||||||
} else {
|
} else {
|
||||||
subtitle = Text('${snapshot.data ?? 0}');
|
subtitle = Text(
|
||||||
|
'${snapshot.data ?? 0}',
|
||||||
|
style: ctx.textTheme.bodyLarge,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: leading,
|
leading: leading,
|
||||||
|
|
@ -147,6 +150,10 @@ final _remoteStats = [
|
||||||
name: 'Exif Entities',
|
name: 'Exif Entities',
|
||||||
load: (db) => db.managers.remoteExifEntity.count(),
|
load: (db) => db.managers.remoteExifEntity.count(),
|
||||||
),
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'Remote Albums',
|
||||||
|
load: (db) => db.managers.remoteAlbumEntity.count(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|
@ -160,6 +167,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
||||||
body: Consumer(
|
body: Consumer(
|
||||||
builder: (ctx, ref, __) {
|
builder: (ctx, ref, __) {
|
||||||
final db = ref.watch(driftProvider);
|
final db = ref.watch(driftProvider);
|
||||||
|
final albumsFuture = ref.watch(remoteAlbumRepository).getAll();
|
||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|
@ -171,6 +179,49 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
itemCount: _remoteStats.length,
|
itemCount: _remoteStats.length,
|
||||||
),
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: Text(
|
||||||
|
"Album summary",
|
||||||
|
style: ctx.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: albumsFuture,
|
||||||
|
builder: (_, snap) {
|
||||||
|
final albums = snap.data ?? [];
|
||||||
|
if (albums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
}
|
||||||
|
|
||||||
|
albums.sortBy((a) => a.name);
|
||||||
|
return SliverList.builder(
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final album = albums[index];
|
||||||
|
final countFuture = db.managers.remoteAlbumAssetEntity
|
||||||
|
.filter((f) => f.albumId.id.equals(album.id))
|
||||||
|
.count();
|
||||||
|
return _Summary(
|
||||||
|
leading: const Icon(Icons.photo_album_rounded),
|
||||||
|
name: album.name,
|
||||||
|
countFuture: countFuture,
|
||||||
|
onTap: () => context.router.push(
|
||||||
|
RemoteTimelineRoute(albumId: album.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: albums.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
32
mobile/lib/presentation/pages/dev/remote_timeline.page.dart
Normal file
32
mobile/lib/presentation/pages/dev/remote_timeline.page.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class RemoteTimelinePage extends StatelessWidget {
|
||||||
|
final String albumId;
|
||||||
|
|
||||||
|
const RemoteTimelinePage({super.key, required this.albumId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService = ref
|
||||||
|
.watch(timelineFactoryProvider)
|
||||||
|
.remoteAlbum(albumId: albumId);
|
||||||
|
ref.onDispose(() => unawaited(timelineService.dispose()));
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
||||||
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
||||||
|
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ class AuthRepository extends DatabaseRepository {
|
||||||
db.users.clear(),
|
db.users.clear(),
|
||||||
_drift.remoteAssetEntity.deleteAll(),
|
_drift.remoteAssetEntity.deleteAll(),
|
||||||
_drift.remoteExifEntity.deleteAll(),
|
_drift.remoteExifEntity.deleteAll(),
|
||||||
|
_drift.userEntity.deleteAll(),
|
||||||
|
_drift.userMetadataEntity.deleteAll(),
|
||||||
|
_drift.partnerEntity.deleteAll(),
|
||||||
|
_drift.remoteAlbumEntity.deleteAll(),
|
||||||
|
_drift.remoteAlbumAssetEntity.deleteAll(),
|
||||||
|
_drift.remoteAlbumUserEntity.deleteAll(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.da
|
||||||
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
|
|
@ -365,6 +366,10 @@ class AppRouter extends RootStackRouter {
|
||||||
page: MainTimelineRoute.page,
|
page: MainTimelineRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: RemoteTimelineRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|
|
||||||
|
|
@ -1425,6 +1425,43 @@ class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RemoteTimelinePage]
|
||||||
|
class RemoteTimelineRoute extends PageRouteInfo<RemoteTimelineRouteArgs> {
|
||||||
|
RemoteTimelineRoute({
|
||||||
|
Key? key,
|
||||||
|
required String albumId,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
RemoteTimelineRoute.name,
|
||||||
|
args: RemoteTimelineRouteArgs(key: key, albumId: albumId),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RemoteTimelineRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<RemoteTimelineRouteArgs>();
|
||||||
|
return RemoteTimelinePage(key: args.key, albumId: args.albumId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteTimelineRouteArgs {
|
||||||
|
const RemoteTimelineRouteArgs({this.key, required this.albumId});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final String albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SearchPage]
|
/// [SearchPage]
|
||||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
||||||
|
|
|
||||||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart
generated
Normal file
BIN
mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart
generated
Normal file
Binary file not shown.
|
|
@ -59,16 +59,28 @@ void main() {
|
||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.updateAssetsV1(any()))
|
when(() => mockSyncStreamRepo.updateAssetsV1(any()))
|
||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.updateAssetsV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.deleteAssetsV1(any()))
|
when(() => mockSyncStreamRepo.deleteAssetsV1(any()))
|
||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.deleteAssetsV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.updateAssetsExifV1(any()))
|
when(() => mockSyncStreamRepo.updateAssetsExifV1(any()))
|
||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.updatePartnerAssetsV1(any()))
|
when(
|
||||||
.thenAnswer(successHandler);
|
() => mockSyncStreamRepo.updateAssetsExifV1(
|
||||||
when(() => mockSyncStreamRepo.deletePartnerAssetsV1(any()))
|
any(),
|
||||||
.thenAnswer(successHandler);
|
debugLabel: any(named: 'debugLabel'),
|
||||||
when(() => mockSyncStreamRepo.updatePartnerAssetsExifV1(any()))
|
),
|
||||||
.thenAnswer(successHandler);
|
).thenAnswer(successHandler);
|
||||||
|
|
||||||
sut = SyncStreamService(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
|
|
||||||
2
mobile/test/fixtures/album.stub.dart
vendored
2
mobile/test/fixtures/album.stub.dart
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13450,6 +13450,21 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAlbumToAssetDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"albumId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"assetId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"albumId",
|
||||||
|
"assetId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAlbumToAssetV1": {
|
"SyncAlbumToAssetV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ const responseDtos = [
|
||||||
SyncAlbumUserV1,
|
SyncAlbumUserV1,
|
||||||
SyncAlbumUserDeleteV1,
|
SyncAlbumUserDeleteV1,
|
||||||
SyncAlbumToAssetV1,
|
SyncAlbumToAssetV1,
|
||||||
|
SyncAlbumToAssetDeleteV1,
|
||||||
SyncAckV1,
|
SyncAckV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue