diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1870ef477..5cdec3d92 100644 Binary files a/mobile/drift_schemas/main/drift_schema_v1.json and b/mobile/drift_schemas/main/drift_schema_v1.json differ diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart index e2bb1fc49..c170f7f84 100644 --- a/mobile/lib/domain/models/asset/asset.model.dart +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -1,9 +1,17 @@ part of 'base_asset.model.dart'; +enum AssetVisibility { + timeline, + hidden, + archive, + locked, +} + // Model for an asset stored in the server class Asset extends BaseAsset { final String id; final String? localId; + final AssetVisibility visibility; const Asset({ required this.id, @@ -17,6 +25,7 @@ class Asset extends BaseAsset { super.height, super.durationInSeconds, super.isFavorite = false, + this.visibility = AssetVisibility.timeline, }); @override @@ -32,6 +41,7 @@ class Asset extends BaseAsset { durationInSeconds: ${durationInSeconds ?? ""}, localId: ${localId ?? ""}, isFavorite: $isFavorite, + visibility: $visibility, }'''; } @@ -39,9 +49,13 @@ class Asset extends BaseAsset { bool operator ==(Object other) { if (other is! Asset) return false; if (identical(this, other)) return true; - return super == other && id == other.id && localId == other.localId; + return super == other && + id == other.id && + localId == other.localId && + visibility == other.visibility; } @override - int get hashCode => super.hashCode ^ id.hashCode ^ localId.hashCode; + int get hashCode => + super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode; } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index ac63734b0..00f97825b 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -63,7 +63,6 @@ class SyncStreamService { Iterable data, ) async { _logger.fine("Processing sync data for $type of length ${data.length}"); - // ignore: prefer-switch-expression switch (type) { case SyncEntityType.userV1: return _syncStreamRepository.updateUsersV1(data.cast()); diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 73c8c2d34..67411013e 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,7 +1,3 @@ -import 'dart:typed_data'; - -import 'package:uuid/parsing.dart'; - extension StringExtension on String { String capitalize() { return split(" ") @@ -33,8 +29,3 @@ extension DurationExtension on String { return int.parse(this); } } - -extension UUIDExtension on String { - Uint8List toUuidByte({bool shouldValidate = false}) => - UuidParsing.parseAsByteList(this, validate: shouldValidate); -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 5a93bc976..11730b776 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -1,4 +1,7 @@ +import 'package:drift/drift.dart' hide Query; import 'package:immich_mobile/domain/models/exif.model.dart' as domain; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:isar/isar.dart'; @@ -90,3 +93,53 @@ class ExifInfo { exposureSeconds: exposureSeconds, ); } + +class RemoteExifEntity extends Table with DriftDefaultsMixin { + const RemoteExifEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get city => text().nullable()(); + + TextColumn get state => text().nullable()(); + + TextColumn get country => text().nullable()(); + + DateTimeColumn get dateTimeOriginal => dateTime().nullable()(); + + TextColumn get description => text().nullable()(); + + IntColumn get height => integer().nullable()(); + + IntColumn get width => integer().nullable()(); + + TextColumn get exposureTime => text().nullable()(); + + IntColumn get fNumber => integer().nullable()(); + + IntColumn get fileSize => integer().nullable()(); + + IntColumn get focalLength => integer().nullable()(); + + IntColumn get latitude => integer().nullable()(); + + IntColumn get longitude => integer().nullable()(); + + IntColumn get iso => integer().nullable()(); + + TextColumn get make => text().nullable()(); + + TextColumn get model => text().nullable()(); + + TextColumn get orientation => text().nullable()(); + + TextColumn get timeZone => text().nullable()(); + + IntColumn get rating => integer().nullable()(); + + TextColumn get projectionType => text().nullable()(); + + @override + Set get primaryKey => {assetId}; +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart new file mode 100644 index 000000000..10025d9cb Binary files /dev/null and b/mobile/lib/infrastructure/entities/exif.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 724cf532c..ff5ee7481 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -2,7 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -@TableIndex(name: 'local_asset_checksum', columns: {#checksum}) +@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum}) class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const LocalAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index 0a4896a4a..68bc1b3c5 100644 Binary files a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart and b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index b7925a8ee..8b51d93e6 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); - BlobColumn get sharedById => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedById => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - BlobColumn get sharedWithId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedWithId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 974a9e3c3..26a5dd2fe 100644 Binary files a/mobile/lib/infrastructure/entities/partner.entity.drift.dart and b/mobile/lib/infrastructure/entities/partner.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart new file mode 100644 index 000000000..96f4077a2 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.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/drift_default.mixin.dart'; + +@TableIndex( + name: 'UQ_remote_asset_owner_checksum', + columns: {#checksum, #ownerId}, + unique: true, +) +class RemoteAssetEntity extends Table + with DriftDefaultsMixin, AssetEntityMixin { + const RemoteAssetEntity(); + + TextColumn get id => text()(); + + TextColumn get checksum => text()(); + + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + DateTimeColumn get localDateTime => dateTime().nullable()(); + + TextColumn get thumbHash => text().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + IntColumn get visibility => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart new file mode 100644 index 000000000..e3fe52170 Binary files /dev/null and b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 955b2267d..b0c1e6e86 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -78,7 +78,7 @@ class User { class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); - BlobColumn get id => blob()(); + TextColumn get id => text()(); TextColumn get name => text()(); BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index 474746a79..32be96951 100644 Binary files a/mobile/lib/infrastructure/entities/user.entity.drift.dart and b/mobile/lib/infrastructure/entities/user.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ebbfeebad..302a9ffce 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserMetadataEntity extends Table with DriftDefaultsMixin { const UserMetadataEntity(); - BlobColumn get userId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get preferences => text().map(userPreferenceConverter)(); @override diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart index 9829fd1ac..95ab63ebf 100644 Binary files a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart and b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 17fcad76b..4ad60276a 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; 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_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.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_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -36,6 +38,8 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumEntity, LocalAssetEntity, LocalAlbumAssetEntity, + RemoteAssetEntity, + RemoteExifEntity, ], ) class Drift extends $Drift implements IDatabaseRepository { diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 6611eb5c9..d1bda9365 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.drift.dart and b/mobile/lib/infrastructure/repositories/db.repository.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index c69122335..2349f35df 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -105,6 +106,7 @@ class SyncApiRepository implements ISyncApiRepository { stopwatch.stop(); _logger .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); + DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } List _parseLines(List lines) { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 5ad9a369d..804f66c5b 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,13 @@ import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; -import 'package:immich_mobile/extensions/string_extensions.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/partner.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/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; +import 'package:openapi/api.dart' hide AssetVisibility; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -22,7 +23,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository for (final user in data) { batch.delete( _db.userEntity, - UserEntityCompanion(id: Value(user.userId.toUuidByte())), + UserEntityCompanion(id: Value(user.userId)), ); } }); @@ -44,7 +45,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.userEntity, - companion.copyWith(id: Value(user.id.toUuidByte())), + companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion), ); } @@ -63,8 +64,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.delete( _db.partnerEntity, PartnerEntityCompanion( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), ); } @@ -86,8 +87,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.partnerEntity, companion.copyWith( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), onConflict: DoUpdate((_) => companion), ); @@ -99,36 +100,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } - // Assets - @override - Future updateAssetsV1(Iterable data) async { - debugPrint("updateAssetsV1 - ${data.length}"); - } - @override Future deleteAssetsV1(Iterable data) async { - debugPrint("deleteAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deleteAssetsV1', e, s); + rethrow; + } } - // Partner Assets @override - Future updatePartnerAssetsV1(Iterable data) async { - debugPrint("updatePartnerAssetsV1 - ${data.length}"); + Future updateAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsV1', e, s); + rethrow; + } } @override Future deletePartnerAssetsV1(Iterable data) async { - debugPrint("deletePartnerAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deletePartnerAssetsV1', e, s); + rethrow; + } + } + + @override + Future updatePartnerAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsV1', e, s); + rethrow; + } } - // EXIF @override Future updateAssetsExifV1(Iterable data) async { - debugPrint("updateAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsExifV1', e, s); + rethrow; + } } @override Future updatePartnerAssetsExifV1(Iterable data) async { - debugPrint("updatePartnerAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); + rethrow; + } } + + Future _updateAssetsV1(Iterable data) => + _db.batch((batch) { + for (final asset in data) { + final companion = RemoteAssetEntityCompanion( + name: Value(asset.originalFileName), + type: Value(asset.type.toAssetType()), + createdAt: Value.absentIfNull(asset.fileCreatedAt), + updatedAt: Value.absentIfNull(asset.fileModifiedAt), + durationInSeconds: const Value(0), + checksum: Value(asset.checksum), + isFavorite: Value(asset.isFavorite), + ownerId: Value(asset.ownerId), + localDateTime: Value(asset.localDateTime), + thumbHash: Value(asset.thumbhash), + deletedAt: Value(asset.deletedAt), + visibility: Value(asset.visibility.toAssetVisibility()), + ); + + batch.insert( + _db.remoteAssetEntity, + companion.copyWith(id: Value(asset.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + + Future _deleteAssetsV1(Iterable assets) => + _db.batch((batch) { + for (final asset in assets) { + batch.delete( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(id: Value(asset.assetId)), + ); + } + }); + + Future _updateAssetExifV1(Iterable data) => + _db.batch((batch) { + for (final exif in data) { + final companion = RemoteExifEntityCompanion( + city: Value(exif.city), + state: Value(exif.state), + country: Value(exif.country), + dateTimeOriginal: Value(exif.dateTimeOriginal), + description: Value(exif.description), + height: Value(exif.exifImageHeight), + width: Value(exif.exifImageWidth), + exposureTime: Value(exif.exposureTime), + fNumber: Value(exif.fNumber), + fileSize: Value(exif.fileSizeInByte), + focalLength: Value(exif.focalLength), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), + iso: Value(exif.iso), + make: Value(exif.make), + model: Value(exif.model), + orientation: Value(exif.orientation), + timeZone: Value(exif.timeZone), + rating: Value(exif.rating), + projectionType: Value(exif.projectionType), + ); + + batch.insert( + _db.remoteExifEntity, + companion.copyWith(assetId: Value(exif.assetId)), + onConflict: DoUpdate((_) => companion), + ); + } + }); +} + +extension on SyncAssetV1TypeEnum { + AssetType toAssetType() => switch (this) { + SyncAssetV1TypeEnum.IMAGE => AssetType.image, + SyncAssetV1TypeEnum.VIDEO => AssetType.video, + SyncAssetV1TypeEnum.AUDIO => AssetType.audio, + SyncAssetV1TypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown SyncAssetV1TypeEnum value: $this'), + }; +} + +extension on SyncAssetV1VisibilityEnum { + AssetVisibility toAssetVisibility() => switch (this) { + SyncAssetV1VisibilityEnum.timeline => AssetVisibility.timeline, + SyncAssetV1VisibilityEnum.hidden => AssetVisibility.hidden, + SyncAssetV1VisibilityEnum.archive => AssetVisibility.archive, + SyncAssetV1VisibilityEnum.locked => AssetVisibility.locked, + _ => throw Exception('Unknown SyncAssetV1VisibilityEnum value: $this'), + }; } diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index da0bea157..3ff0b12b9 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -53,11 +53,38 @@ final _features = [ await db.localAlbumAssetEntity.deleteAll(); }, ), + _Feature( + name: 'Clear Remote Data', + icon: Icons.delete_sweep_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.remoteAssetEntity.deleteAll(); + await db.remoteExifEntity.deleteAll(); + }, + ), _Feature( name: 'Local Media Summary', icon: Icons.table_chart_rounded, onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), ), + _Feature( + name: 'Remote Media Summary', + icon: Icons.summarize_rounded, + onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), + ), + _Feature( + name: 'Reset Sqlite', + icon: Icons.table_view_rounded, + onTap: (_, ref) async { + final drift = ref.read(driftProvider); + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + final migrator = drift.createMigrator(); + for (final entity in drift.allSchemaEntities) { + await migrator.drop(entity); + await migrator.create(entity); + } + }, + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/dev/local_media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart similarity index 76% rename from mobile/lib/presentation/pages/dev/local_media_stat.page.dart rename to mobile/lib/presentation/pages/dev/media_stat.page.dart index b42cae84f..5debeff31 100644 --- a/mobile/lib/presentation/pages/dev/local_media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer-single-widget-per-file + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -8,7 +10,40 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -final _stats = [ +class _Stat { + const _Stat({required this.name, required this.load}); + + final String name; + final Future Function(Drift _) load; +} + +class _Summary extends StatelessWidget { + final String name; + final Future countFuture; + + const _Summary({required this.name, required this.countFuture}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: countFuture, + builder: (ctx, snapshot) { + final Widget subtitle; + + if (snapshot.connectionState == ConnectionState.waiting) { + subtitle = const CircularProgressIndicator(); + } else if (snapshot.hasError) { + subtitle = const Icon(Icons.error_rounded); + } else { + subtitle = Text('${snapshot.data ?? 0}'); + } + return ListTile(title: Text(name), trailing: subtitle); + }, + ); + } +} + +final _localStats = [ _Stat( name: 'Local Assets', load: (db) => db.managers.localAssetEntity.count(), @@ -36,11 +71,11 @@ class LocalMediaSummaryPage extends StatelessWidget { slivers: [ SliverList.builder( itemBuilder: (_, index) { - final stat = _stats[index]; + final stat = _localStats[index]; final countFuture = stat.load(db); return _Summary(name: stat.name, countFuture: countFuture); }, - itemCount: _stats.length, + itemCount: _localStats.length, ), SliverToBoxAdapter( child: Column( @@ -90,36 +125,43 @@ class LocalMediaSummaryPage extends StatelessWidget { } } -// ignore: prefer-single-widget-per-file -class _Summary extends StatelessWidget { - final String name; - final Future countFuture; +final _remoteStats = [ + _Stat( + name: 'Remote Assets', + load: (db) => db.managers.remoteAssetEntity.count(), + ), + _Stat( + name: 'Exif Entities', + load: (db) => db.managers.remoteExifEntity.count(), + ), +]; - const _Summary({required this.name, required this.countFuture}); +@RoutePage() +class RemoteMediaSummaryPage extends StatelessWidget { + const RemoteMediaSummaryPage({super.key}); @override Widget build(BuildContext context) { - return FutureBuilder( - future: countFuture, - builder: (ctx, snapshot) { - final Widget subtitle; + return Scaffold( + appBar: AppBar(title: const Text('Remote Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); - if (snapshot.connectionState == ConnectionState.waiting) { - subtitle = const CircularProgressIndicator(); - } else if (snapshot.hasError) { - subtitle = const Icon(Icons.error_rounded); - } else { - subtitle = Text('${snapshot.data ?? 0}'); - } - return ListTile(title: Text(name), trailing: subtitle); - }, + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _remoteStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _remoteStats.length, + ), + ], + ); + }, + ), ); } } - -class _Stat { - const _Stat({required this.name, required this.load}); - - final String name; - final Future Function(Drift _) load; -} diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index f9e82e163..01d2684fa 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -8,17 +9,22 @@ 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/interfaces/auth.interface.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( - (ref) => AuthRepository(ref.watch(dbProvider)), + (ref) => + AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)), ); class AuthRepository extends DatabaseRepository implements IAuthRepository { - AuthRepository(super.db); + final Drift _drift; + + AuthRepository(super.db, {required Drift drift}) : _drift = drift; @override Future clearLocalData() { @@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index a6e1d89ff..1f14aaa5b 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -64,7 +64,7 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/local_media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -326,5 +326,9 @@ class AppRouter extends RootStackRouter { page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: RemoteMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 57fb8cef8..0c57949f0 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1356,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteMediaSummaryPage] +class RemoteMediaSummaryRoute extends PageRouteInfo { + const RemoteMediaSummaryRoute({List? children}) + : super(RemoteMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'RemoteMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RemoteMediaSummaryPage(); + }, + ); +} + /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { diff --git a/mobile/makefile b/mobile/makefile index b797a6592..ec0d08f08 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -21,7 +21,7 @@ create_splash: build_release_android: flutter build appbundle -migrations: +migration: dart run drift_dev make-migrations translation: