You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-14 07:04:24 +02:00
feat(mobile): sqlite timeline (#19197)
* wip: timeline * more segment extensions * added scrubber * refactor: timeline state * more refactors * fix scrubber segments * added remote thumb & thumbhash provider * feat: merged view * scrub / merged asset fixes * rename stuff & add tile indicators * fix local album timeline query * ignore hidden assets during sync * ignore recovered assets during sync * old scrubber * add video indicator * handle groupBy * handle partner inTimeline * show duration * reduce widget nesting in thumb tile * merge main * chore: extend cacheExtent * ignore touch events on scrub label when not visible * scrub label ignore events and hide immediately * auto reload on sync * refactor image providers * throttle db updates --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@ -1,4 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@ -15,3 +17,16 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
||||
LocalAsset toDto() => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: checksum,
|
||||
type: type,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
);
|
||||
}
|
||||
|
81
mobile/lib/infrastructure/entities/merged_asset.drift
Normal file
81
mobile/lib/infrastructure/entities/merged_asset.drift
Normal file
@ -0,0 +1,81 @@
|
||||
import 'remote_asset.entity.dart';
|
||||
import 'local_asset.entity.dart';
|
||||
|
||||
mergedAsset: SELECT * FROM
|
||||
(
|
||||
SELECT
|
||||
rae.id as remote_id,
|
||||
lae.id as local_id,
|
||||
rae.name,
|
||||
rae."type",
|
||||
rae.created_at,
|
||||
rae.updated_at,
|
||||
rae.width,
|
||||
rae.height,
|
||||
rae.duration_in_seconds,
|
||||
rae.is_favorite,
|
||||
rae.thumb_hash,
|
||||
rae.checksum,
|
||||
rae.owner_id
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||
WHERE
|
||||
rae.visibility = 0 AND rae.owner_id in ?
|
||||
UNION ALL
|
||||
SELECT
|
||||
NULL as remote_id,
|
||||
lae.id as local_id,
|
||||
lae.name,
|
||||
lae."type",
|
||||
lae.created_at,
|
||||
lae.updated_at,
|
||||
lae.width,
|
||||
lae.height,
|
||||
lae.duration_in_seconds,
|
||||
lae.is_favorite,
|
||||
NULL as thumb_hash,
|
||||
lae.checksum,
|
||||
NULL as owner_id
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
LEFT JOIN
|
||||
remote_asset_entity rae ON rae.checksum = lae.checksum
|
||||
WHERE
|
||||
rae.id IS NULL
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit;
|
||||
|
||||
mergedBucket(:group_by AS INTEGER):
|
||||
SELECT
|
||||
COUNT(*) as asset_count,
|
||||
CASE
|
||||
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at) -- day
|
||||
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at) -- month
|
||||
END AS bucket_date
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
rae.name,
|
||||
rae.created_at
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||
WHERE
|
||||
rae.visibility = 0 AND rae.owner_id in ?
|
||||
UNION ALL
|
||||
SELECT
|
||||
lae.name,
|
||||
lae.created_at
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
LEFT JOIN
|
||||
remote_asset_entity rae ON rae.checksum = lae.checksum
|
||||
WHERE
|
||||
rae.id IS NULL
|
||||
)
|
||||
GROUP BY bucket_date
|
||||
ORDER BY bucket_date DESC;
|
114
mobile/lib/infrastructure/entities/merged_asset.drift.dart
generated
Normal file
114
mobile/lib/infrastructure/entities/merged_asset.drift.dart
generated
Normal file
@ -0,0 +1,114 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift/internal/modular.dart' as i1;
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i4;
|
||||
|
||||
class MergedAssetDrift extends i1.ModularAccessor {
|
||||
MergedAssetDrift(i0.GeneratedDatabase db) : super(db);
|
||||
i0.Selectable<MergedAssetResult> mergedAsset(List<String> var1,
|
||||
{required i0.Limit limit}) {
|
||||
var $arrayStartIndex = 1;
|
||||
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
|
||||
$arrayStartIndex += var1.length;
|
||||
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
return customSelect(
|
||||
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
variables: [
|
||||
for (var $ in var1) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables
|
||||
],
|
||||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
...generatedlimit.watchedTables,
|
||||
}).map((i0.QueryRow row) => MergedAssetResult(
|
||||
remoteId: row.readNullable<String>('remote_id'),
|
||||
localId: row.readNullable<String>('local_id'),
|
||||
name: row.read<String>('name'),
|
||||
type: i3.$RemoteAssetEntityTable.$convertertype
|
||||
.fromSql(row.read<int>('type')),
|
||||
createdAt: row.read<DateTime>('created_at'),
|
||||
updatedAt: row.read<DateTime>('updated_at'),
|
||||
width: row.readNullable<int>('width'),
|
||||
height: row.readNullable<int>('height'),
|
||||
durationInSeconds: row.readNullable<int>('duration_in_seconds'),
|
||||
isFavorite: row.read<bool>('is_favorite'),
|
||||
thumbHash: row.readNullable<String>('thumb_hash'),
|
||||
checksum: row.readNullable<String>('checksum'),
|
||||
ownerId: row.readNullable<String>('owner_id'),
|
||||
));
|
||||
}
|
||||
|
||||
i0.Selectable<MergedBucketResult> mergedBucket(List<String> var2,
|
||||
{required int groupBy}) {
|
||||
var $arrayStartIndex = 2;
|
||||
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
||||
$arrayStartIndex += var2.length;
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at) END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||
variables: [
|
||||
i0.Variable<int>(groupBy),
|
||||
for (var $ in var2) i0.Variable<String>($)
|
||||
],
|
||||
readsFrom: {
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
}).map((i0.QueryRow row) => MergedBucketResult(
|
||||
assetCount: row.read<int>('asset_count'),
|
||||
bucketDate: row.read<String>('bucket_date'),
|
||||
));
|
||||
}
|
||||
|
||||
i3.$RemoteAssetEntityTable get remoteAssetEntity =>
|
||||
i1.ReadDatabaseContainer(attachedDatabase)
|
||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity');
|
||||
i4.$LocalAssetEntityTable get localAssetEntity =>
|
||||
i1.ReadDatabaseContainer(attachedDatabase)
|
||||
.resultSet<i4.$LocalAssetEntityTable>('local_asset_entity');
|
||||
}
|
||||
|
||||
class MergedAssetResult {
|
||||
final String? remoteId;
|
||||
final String? localId;
|
||||
final String name;
|
||||
final i2.AssetType type;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? durationInSeconds;
|
||||
final bool isFavorite;
|
||||
final String? thumbHash;
|
||||
final String? checksum;
|
||||
final String? ownerId;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.width,
|
||||
this.height,
|
||||
this.durationInSeconds,
|
||||
required this.isFavorite,
|
||||
this.thumbHash,
|
||||
this.checksum,
|
||||
this.ownerId,
|
||||
});
|
||||
}
|
||||
|
||||
class MergedBucketResult {
|
||||
final int assetCount;
|
||||
final String bucketDate;
|
||||
MergedBucketResult({
|
||||
required this.assetCount,
|
||||
required this.bucketDate,
|
||||
});
|
||||
}
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
columns: {#checksum, #ownerId},
|
||||
unique: true,
|
||||
)
|
||||
@TableIndex(name: 'idx_remote_asset_checksum', columns: {#checksum})
|
||||
class RemoteAssetEntity extends Table
|
||||
with DriftDefaultsMixin, AssetEntityMixin {
|
||||
const RemoteAssetEntity();
|
||||
|
@ -1178,3 +1178,6 @@ class RemoteAssetEntityCompanion
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
i0.Index get idxRemoteAssetChecksum => i0.Index('idx_remote_asset_checksum',
|
||||
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
|
||||
|
@ -0,0 +1,28 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetMediaRepository implements IAssetMediaRepository {
|
||||
const AssetMediaRepository();
|
||||
|
||||
@override
|
||||
Future<Uint8List?> getThumbnail(
|
||||
String id, {
|
||||
int quality = 80,
|
||||
Size size = const Size.square(256),
|
||||
}) =>
|
||||
AssetEntity(
|
||||
id: id,
|
||||
// The below fields are not used in thumbnailDataWithSize but are required
|
||||
// to create an AssetEntity instance. It is faster to create a dummy AssetEntity
|
||||
// instance than to fetch the asset from the device first.
|
||||
typeInt: AssetType.image.index,
|
||||
width: size.width.toInt(),
|
||||
height: size.height.toInt(),
|
||||
).thumbnailDataWithSize(
|
||||
ThumbnailSize(size.width.toInt(), size.height.toInt()),
|
||||
quality: quality,
|
||||
);
|
||||
}
|
@ -41,6 +41,9 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
RemoteAssetEntity,
|
||||
RemoteExifEntity,
|
||||
],
|
||||
include: {
|
||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||
},
|
||||
)
|
||||
class Drift extends $Drift implements IDatabaseRepository {
|
||||
Drift([QueryExecutor? executor])
|
||||
|
@ -3,59 +3,72 @@
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i9;
|
||||
import 'package:drift/internal/modular.dart' as i10;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
$DriftManager get managers => $DriftManager(this);
|
||||
late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
|
||||
late final i2.$UserMetadataEntityTable userMetadataEntity =
|
||||
i2.$UserMetadataEntityTable(this);
|
||||
late final i3.$PartnerEntityTable partnerEntity =
|
||||
i3.$PartnerEntityTable(this);
|
||||
late final i4.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i4.$LocalAlbumEntityTable(this);
|
||||
late final i5.$LocalAssetEntityTable localAssetEntity =
|
||||
i5.$LocalAssetEntityTable(this);
|
||||
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||
i6.$LocalAlbumAssetEntityTable(this);
|
||||
late final i7.$RemoteAssetEntityTable remoteAssetEntity =
|
||||
i7.$RemoteAssetEntityTable(this);
|
||||
late final i2.$RemoteAssetEntityTable remoteAssetEntity =
|
||||
i2.$RemoteAssetEntityTable(this);
|
||||
late final i3.$LocalAssetEntityTable localAssetEntity =
|
||||
i3.$LocalAssetEntityTable(this);
|
||||
late final i4.$UserMetadataEntityTable userMetadataEntity =
|
||||
i4.$UserMetadataEntityTable(this);
|
||||
late final i5.$PartnerEntityTable partnerEntity =
|
||||
i5.$PartnerEntityTable(this);
|
||||
late final i6.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i6.$LocalAlbumEntityTable(this);
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||
i7.$LocalAlbumAssetEntityTable(this);
|
||||
late final i8.$RemoteExifEntityTable remoteExifEntity =
|
||||
i8.$RemoteExifEntityTable(this);
|
||||
i9.MergedAssetDrift get mergedAssetDrift => i10.ReadDatabaseContainer(this)
|
||||
.accessor<i9.MergedAssetDrift>(i9.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
localAssetEntity,
|
||||
i3.idxLocalAssetChecksum,
|
||||
i2.uQRemoteAssetOwnerChecksum,
|
||||
i2.idxRemoteAssetChecksum,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
localAlbumEntity,
|
||||
localAssetEntity,
|
||||
localAlbumAssetEntity,
|
||||
remoteAssetEntity,
|
||||
remoteExifEntity,
|
||||
i5.idxLocalAssetChecksum,
|
||||
i7.uQRemoteAssetOwnerChecksum
|
||||
remoteExifEntity
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||
const i0.StreamQueryUpdateRules(
|
||||
[
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
result: [
|
||||
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
@ -94,13 +107,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
result: [
|
||||
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
@ -120,18 +126,18 @@ class $DriftManager {
|
||||
$DriftManager(this._db);
|
||||
i1.$$UserEntityTableTableManager get userEntity =>
|
||||
i1.$$UserEntityTableTableManager(_db, _db.userEntity);
|
||||
i2.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i3.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
|
||||
i2.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||
i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i5.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||
i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
}
|
||||
|
180
mobile/lib/infrastructure/repositories/timeline.repository.dart
Normal file
180
mobile/lib/infrastructure/repositories/timeline.repository.dart
Normal file
@ -0,0 +1,180 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/timeline.interface.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/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
class DriftTimelineRepository extends DriftDatabaseRepository
|
||||
implements ITimelineRepository {
|
||||
final Drift _db;
|
||||
|
||||
const DriftTimelineRepository(super._db) : _db = _db;
|
||||
|
||||
List<Bucket> _generateBuckets(int count) {
|
||||
final numBuckets = (count / kTimelineNoneSegmentSize).floor();
|
||||
final buckets = List.generate(
|
||||
numBuckets,
|
||||
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
|
||||
);
|
||||
if (count % kTimelineNoneSegmentSize != 0) {
|
||||
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<Bucket>> watchMainBucket(
|
||||
List<String> userIds, {
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
throw UnsupportedError(
|
||||
"GroupAssetsBy.none is not supported for watchMainBucket",
|
||||
);
|
||||
}
|
||||
|
||||
return _db.mergedAssetDrift
|
||||
.mergedBucket(userIds, groupBy: groupBy.index)
|
||||
.map((row) {
|
||||
final date = row.bucketDate.dateFmt(groupBy);
|
||||
return TimeBucket(date: date, assetCount: row.assetCount);
|
||||
})
|
||||
.watch()
|
||||
.throttle(const Duration(seconds: 3), trailing: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BaseAsset>> getMainBucketAssets(
|
||||
List<String> userIds, {
|
||||
required int offset,
|
||||
required int count,
|
||||
}) {
|
||||
return _db.mergedAssetDrift
|
||||
.mergedAsset(userIds, limit: Limit(count, offset))
|
||||
.map(
|
||||
(row) => row.remoteId != null
|
||||
? Asset(
|
||||
id: row.remoteId!,
|
||||
localId: row.localId,
|
||||
name: row.name,
|
||||
checksum: row.checksum,
|
||||
type: row.type,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
thumbHash: row.thumbHash,
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
isFavorite: row.isFavorite,
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
)
|
||||
: LocalAsset(
|
||||
id: row.localId!,
|
||||
remoteId: row.remoteId,
|
||||
name: row.name,
|
||||
checksum: row.checksum,
|
||||
type: row.type,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
isFavorite: row.isFavorite,
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<Bucket>> watchLocalBucket(
|
||||
String albumId, {
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
return _db.localAlbumAssetEntity
|
||||
.count(where: (row) => row.albumId.equals(albumId))
|
||||
.map(_generateBuckets)
|
||||
.watchSingle();
|
||||
}
|
||||
|
||||
final assetCountExp = _db.localAssetEntity.id.count();
|
||||
final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy);
|
||||
|
||||
final query = _db.localAssetEntity.selectOnly()
|
||||
..addColumns([assetCountExp, dateExp])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumAssetEntity,
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
),
|
||||
])
|
||||
..where(_db.localAlbumAssetEntity.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();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BaseAsset>> getLocalBucketAssets(
|
||||
String albumId, {
|
||||
required int offset,
|
||||
required int count,
|
||||
}) {
|
||||
final query = _db.localAssetEntity.select().join(
|
||||
[
|
||||
innerJoin(
|
||||
_db.localAlbumAssetEntity,
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
),
|
||||
],
|
||||
)
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||
..limit(count, offset: offset);
|
||||
return query
|
||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
||||
extension on Expression<DateTime> {
|
||||
Expression<String> dateFmt(GroupAssetsBy groupBy) {
|
||||
// DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting
|
||||
// to create the correct time bucket
|
||||
final localTimeExp = modify(const DateTimeModifier.localTime());
|
||||
return switch (groupBy) {
|
||||
GroupAssetsBy.day => localTimeExp.date,
|
||||
GroupAssetsBy.month => localTimeExp.strftime("%Y-%m"),
|
||||
GroupAssetsBy.none => throw ArgumentError(
|
||||
"GroupAssetsBy.none is not supported for date formatting",
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
DateTime dateFmt(GroupAssetsBy groupBy) {
|
||||
final format = switch (groupBy) {
|
||||
GroupAssetsBy.day => "y-M-d",
|
||||
GroupAssetsBy.month => "y-M",
|
||||
GroupAssetsBy.none => throw ArgumentError(
|
||||
"GroupAssetsBy.none is not supported for date formatting",
|
||||
),
|
||||
};
|
||||
try {
|
||||
return DateFormat(format).parse(this);
|
||||
} catch (e) {
|
||||
throw FormatException("Invalid date format: $this", e);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user