mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00:00
feat(mobile): remote asset & exif sync (#18756)
* feat(mobile): remote asset & exif sync * add visibility and update constraints * chore: generate drifts * update ids to be strings * clear remote entities on logout * reset sqlite button --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
edae9c2d3d
commit
b4a798c39f
26 changed files with 388 additions and 75 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.
|
|
@ -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 ?? "<NA>"},
|
||||
localId: ${localId ?? "<NA>"},
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ class SyncStreamService {
|
|||
Iterable<dynamic> 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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Column> get primaryKey => {assetId};
|
||||
}
|
||||
|
|
|
|||
BIN
mobile/lib/infrastructure/entities/exif.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/exif.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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))();
|
||||
|
||||
|
|
|
|||
Binary file not shown.
35
mobile/lib/infrastructure/entities/remote_asset.entity.dart
Normal file
35
mobile/lib/infrastructure/entities/remote_asset.entity.dart
Normal file
|
|
@ -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<AssetVisibility>()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
BIN
mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -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()();
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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<SyncEvent> _parseLines(List<String> lines) {
|
||||
|
|
|
|||
|
|
@ -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<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||
debugPrint("updateAssetsV1 - ${data.length}");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> 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<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||
debugPrint("updatePartnerAssetsV1 - ${data.length}");
|
||||
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||
try {
|
||||
await _updateAssetsV1(data);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error while processing updateAssetsV1', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
|
||||
debugPrint("deletePartnerAssetsV1 - ${data.length}");
|
||||
try {
|
||||
await _deleteAssetsV1(data);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error while processing deletePartnerAssetsV1', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
|
||||
try {
|
||||
await _updateAssetsV1(data);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error while processing updatePartnerAssetsV1', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// EXIF
|
||||
@override
|
||||
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
||||
debugPrint("updateAssetsExifV1 - ${data.length}");
|
||||
try {
|
||||
await _updateAssetExifV1(data);
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error while processing updateAssetsExifV1', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
|
||||
debugPrint("updatePartnerAssetsExifV1 - ${data.length}");
|
||||
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) {
|
||||
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<void> _deleteAssetsV1(Iterable<SyncAssetDeleteV1> assets) =>
|
||||
_db.batch((batch) {
|
||||
for (final asset in assets) {
|
||||
batch.delete(
|
||||
_db.remoteAssetEntity,
|
||||
RemoteAssetEntityCompanion(id: Value(asset.assetId)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Future<void> _updateAssetExifV1(Iterable<SyncAssetExifV1> 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'),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<int> Function(Drift _) load;
|
||||
}
|
||||
|
||||
class _Summary extends StatelessWidget {
|
||||
final String name;
|
||||
final Future<int> countFuture;
|
||||
|
||||
const _Summary({required this.name, required this.countFuture});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<int>(
|
||||
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<int> 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<int>(
|
||||
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<int> Function(Drift _) load;
|
||||
}
|
||||
|
|
@ -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<IAuthRepository>(
|
||||
(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<void> 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(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo<void> {
|
|||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RemoteMediaSummaryPage]
|
||||
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
||||
const RemoteMediaSummaryRoute({List<PageRouteInfo>? 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<SearchRouteArgs> {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ create_splash:
|
|||
build_release_android:
|
||||
flutter build appbundle
|
||||
|
||||
migrations:
|
||||
migration:
|
||||
dart run drift_dev make-migrations
|
||||
|
||||
translation:
|
||||
|
|
|
|||
Loading…
Reference in a new issue