add full sync

This commit is contained in:
shenlong-tanwen 2024-09-02 02:16:47 +05:30
parent 877c3b028b
commit e81b61c98b
30 changed files with 333 additions and 179 deletions

View File

@ -1,19 +1,15 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/asset/asset.model.dart';
class Asset extends Table { class Asset extends Table {
const Asset(); const Asset();
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get checksum => text().unique()(); TextColumn get checksum => text().unique()();
IntColumn get height => integer()(); IntColumn get height => integer().nullable()();
IntColumn get width => integer()(); IntColumn get width => integer().nullable()();
IntColumn get type => intEnum<AssetType>()(); IntColumn get type => intEnum<AssetType>()();
DateTimeColumn get createdTime => dateTime()(); DateTimeColumn get createdTime => dateTime()();
DateTimeColumn get modifiedTime => DateTimeColumn get modifiedTime =>
dateTime().withDefault(currentDateAndTime)(); dateTime().withDefault(currentDateAndTime)();
IntColumn get duration => integer().withDefault(const Constant(0))(); IntColumn get duration => integer().withDefault(const Constant(0))();
BoolColumn get isLivePhoto => boolean().withDefault(const Constant(false))();
} }

View File

@ -4,5 +4,8 @@ import 'package:immich_mobile/domain/entities/asset.entity.dart';
class LocalAsset extends Asset { class LocalAsset extends Asset {
const LocalAsset(); const LocalAsset();
TextColumn get localId => text().unique()(); TextColumn get localId => text()();
@override
Set<Column> get primaryKey => {localId};
} }

View File

@ -4,5 +4,9 @@ import 'package:immich_mobile/domain/entities/asset.entity.dart';
class RemoteAsset extends Asset { class RemoteAsset extends Asset {
const RemoteAsset(); const RemoteAsset();
TextColumn get remoteId => text().unique()(); TextColumn get remoteId => text()();
TextColumn get livePhotoVideoId => text().nullable()();
@override
Set<Column> get primaryKey => {remoteId};
} }

View File

@ -1,10 +0,0 @@
abstract class IDatabaseRepository<T> {
/// Current version of the DB to aid with migration
int get schemaVersion;
/// Initializes the DB and returns the corresponding object
T init();
/// Check and migrate the DB to the latest schema
void migrateDB();
}

View File

@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/log.model.dart';
abstract class ILogRepository { abstract class ILogRepository {
/// Fetches all logs /// Fetches all logs
FutureOr<List<LogMessage>> fetchLogs(); FutureOr<List<LogMessage>> fetchAll();
/// Inserts a new log into the DB /// Inserts a new log into the DB
FutureOr<bool> add(LogMessage log); FutureOr<bool> add(LogMessage log);

View File

@ -0,0 +1,6 @@
import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart';
abstract class IRemoteAssetRepository {
/// Batch insert asset
Future<bool> addAll(Iterable<RemoteAsset> assets);
}

View File

@ -4,8 +4,8 @@ import 'package:immich_mobile/domain/models/user.model.dart';
abstract class IUserRepository { abstract class IUserRepository {
/// Fetches user /// Fetches user
FutureOr<User?> getUser(String userId); FutureOr<User?> fetch(String userId);
/// Insert user /// Insert user
FutureOr<bool> insertUser(User user); FutureOr<bool> add(User user);
} }

View File

@ -7,32 +7,27 @@ enum AssetType {
} }
class Asset { class Asset {
final int id;
final String name; final String name;
final String checksum; final String checksum;
final int height; final int? height;
final int width; final int? width;
final AssetType type; final AssetType type;
final DateTime createdTime; final DateTime createdTime;
final DateTime modifiedTime; final DateTime modifiedTime;
final int duration; final int duration;
final bool isLivePhoto;
const Asset({ const Asset({
required this.id,
required this.name, required this.name,
required this.checksum, required this.checksum,
required this.height, this.height,
required this.width, this.width,
required this.type, required this.type,
required this.createdTime, required this.createdTime,
required this.modifiedTime, required this.modifiedTime,
required this.duration, required this.duration,
required this.isLivePhoto,
}); });
Asset copyWith({ Asset copyWith({
int? id,
String? name, String? name,
String? checksum, String? checksum,
int? height, int? height,
@ -41,10 +36,8 @@ class Asset {
DateTime? createdTime, DateTime? createdTime,
DateTime? modifiedTime, DateTime? modifiedTime,
int? duration, int? duration,
bool? isLivePhoto,
}) { }) {
return Asset( return Asset(
id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
height: height ?? this.height, height: height ?? this.height,
@ -53,52 +46,45 @@ class Asset {
createdTime: createdTime ?? this.createdTime, createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime, modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration, duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
); );
} }
@override @override
String toString() => """ String toString() => """
{ {
"id": $id,
"name": "$name", "name": "$name",
"checksum": "$checksum", "checksum": "$checksum",
"height": $height, "height": ${height ?? "-"},
"width": $width, "width": ${width ?? "-"},
"type": "$type", "type": "$type",
"createdTime": "$createdTime", "createdTime": "$createdTime",
"modifiedTime": "$modifiedTime", "modifiedTime": "$modifiedTime",
"duration": "$duration", "duration": "$duration",
"isLivePhoto": "$isLivePhoto",
}"""; }""";
@override @override
bool operator ==(covariant Asset other) { bool operator ==(covariant Asset other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.id == id && return other.name == name &&
other.name == name &&
other.checksum == checksum && other.checksum == checksum &&
other.height == height && other.height == height &&
other.width == width && other.width == width &&
other.type == type && other.type == type &&
other.createdTime == createdTime && other.createdTime == createdTime &&
other.modifiedTime == modifiedTime && other.modifiedTime == modifiedTime &&
other.duration == duration && other.duration == duration;
other.isLivePhoto == isLivePhoto;
} }
@override @override
int get hashCode { int get hashCode {
return id.hashCode ^ return name.hashCode ^
name.hashCode ^
checksum.hashCode ^ checksum.hashCode ^
height.hashCode ^ height.hashCode ^
width.hashCode ^ width.hashCode ^
type.hashCode ^ type.hashCode ^
createdTime.hashCode ^ createdTime.hashCode ^
modifiedTime.hashCode ^ modifiedTime.hashCode ^
duration.hashCode ^ duration.hashCode;
isLivePhoto.hashCode;
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/asset/asset.model.dart';
@immutable @immutable
class LocalAsset extends Asset { class LocalAsset extends Asset {
@ -7,7 +7,6 @@ class LocalAsset extends Asset {
const LocalAsset({ const LocalAsset({
required this.localId, required this.localId,
required super.id,
required super.name, required super.name,
required super.checksum, required super.checksum,
required super.height, required super.height,
@ -16,23 +15,20 @@ class LocalAsset extends Asset {
required super.createdTime, required super.createdTime,
required super.modifiedTime, required super.modifiedTime,
required super.duration, required super.duration,
required super.isLivePhoto,
}); });
@override @override
String toString() => """ String toString() => """
{ {
"id": $id,
"localId": "$localId", "localId": "$localId",
"name": "$name", "name": "$name",
"checksum": "$checksum", "checksum": "$checksum",
"height": $height, "height": ${height ?? "-"},
"width": $width, "width": ${width ?? "-"},
"type": "$type", "type": "$type",
"createdTime": "$createdTime", "createdTime": "$createdTime",
"modifiedTime": "$modifiedTime", "modifiedTime": "$modifiedTime",
"duration": "$duration", "duration": "$duration",
"isLivePhoto": "$isLivePhoto",
}"""; }""";
@override @override
@ -47,7 +43,6 @@ class LocalAsset extends Asset {
@override @override
LocalAsset copyWith({ LocalAsset copyWith({
int? id,
String? localId, String? localId,
String? name, String? name,
String? checksum, String? checksum,
@ -57,10 +52,8 @@ class LocalAsset extends Asset {
DateTime? createdTime, DateTime? createdTime,
DateTime? modifiedTime, DateTime? modifiedTime,
int? duration, int? duration,
bool? isLivePhoto,
}) { }) {
return LocalAsset( return LocalAsset(
id: id ?? this.id,
localId: localId ?? this.localId, localId: localId ?? this.localId,
name: name ?? this.name, name: name ?? this.name,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
@ -70,7 +63,6 @@ class LocalAsset extends Asset {
createdTime: createdTime ?? this.createdTime, createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime, modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration, duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
); );
} }
} }

View File

@ -1,13 +1,15 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/asset/asset.model.dart';
import 'package:immich_mobile/utils/extensions/string.extension.dart';
import 'package:openapi/api.dart';
@immutable @immutable
class RemoteAsset extends Asset { class RemoteAsset extends Asset {
final String remoteId; final String remoteId;
final String? livePhotoVideoId;
const RemoteAsset({ const RemoteAsset({
required this.remoteId, required this.remoteId,
required super.id,
required super.name, required super.name,
required super.checksum, required super.checksum,
required super.height, required super.height,
@ -16,23 +18,22 @@ class RemoteAsset extends Asset {
required super.createdTime, required super.createdTime,
required super.modifiedTime, required super.modifiedTime,
required super.duration, required super.duration,
required super.isLivePhoto, this.livePhotoVideoId,
}); });
@override @override
String toString() => """ String toString() => """
{ {
"id": $id,
"remoteId": "$remoteId", "remoteId": "$remoteId",
"name": "$name", "name": "$name",
"checksum": "$checksum", "checksum": "$checksum",
"height": $height, "height": ${height ?? "-"},
"width": $width, "width": ${width ?? "-"},
"type": "$type", "type": "$type",
"createdTime": "$createdTime", "createdTime": "$createdTime",
"modifiedTime": "$modifiedTime", "modifiedTime": "$modifiedTime",
"duration": "$duration", "duration": "$duration",
"isLivePhoto": "$isLivePhoto", "livePhotoVideoId": "${livePhotoVideoId ?? "-"}",
}"""; }""";
@override @override
@ -47,7 +48,6 @@ class RemoteAsset extends Asset {
@override @override
RemoteAsset copyWith({ RemoteAsset copyWith({
int? id,
String? remoteId, String? remoteId,
String? name, String? name,
String? checksum, String? checksum,
@ -57,10 +57,9 @@ class RemoteAsset extends Asset {
DateTime? createdTime, DateTime? createdTime,
DateTime? modifiedTime, DateTime? modifiedTime,
int? duration, int? duration,
bool? isLivePhoto, String? livePhotoVideoId,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId, remoteId: remoteId ?? this.remoteId,
name: name ?? this.name, name: name ?? this.name,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
@ -70,7 +69,27 @@ class RemoteAsset extends Asset {
createdTime: createdTime ?? this.createdTime, createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime, modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration, duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
); );
} }
factory RemoteAsset.fromDto(AssetResponseDto dto) => RemoteAsset(
remoteId: dto.id,
createdTime: dto.fileCreatedAt,
duration: dto.duration.tryParseInt() ?? 0,
height: dto.exifInfo?.exifImageHeight?.toInt(),
width: dto.exifInfo?.exifImageWidth?.toInt(),
checksum: dto.checksum,
name: dto.originalFileName,
livePhotoVideoId: dto.livePhotoVideoId,
modifiedTime: dto.fileModifiedAt,
type: _toAssetType(dto.type),
);
} }
AssetType _toAssetType(AssetTypeEnum type) => switch (type) {
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
_ => AssetType.other,
};

View File

@ -1,9 +1,7 @@
import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/native.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:drift_dev/api/migrations.dart'; import 'package:drift_dev/api/migrations.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/entities/album.entity.dart'; import 'package:immich_mobile/domain/entities/album.entity.dart';
import 'package:immich_mobile/domain/entities/local_asset.entity.dart'; import 'package:immich_mobile/domain/entities/local_asset.entity.dart';
@ -11,54 +9,17 @@ import 'package:immich_mobile/domain/entities/log.entity.dart';
import 'package:immich_mobile/domain/entities/remote_asset.entity.dart'; import 'package:immich_mobile/domain/entities/remote_asset.entity.dart';
import 'package:immich_mobile/domain/entities/store.entity.dart'; import 'package:immich_mobile/domain/entities/store.entity.dart';
import 'package:immich_mobile/domain/entities/user.entity.dart'; import 'package:immich_mobile/domain/entities/user.entity.dart';
import 'package:immich_mobile/domain/interfaces/database.interface.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'database.repository.drift.dart'; import 'database.repository.drift.dart';
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset]) @DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset])
class DriftDatabaseRepository extends $DriftDatabaseRepository class DriftDatabaseRepository extends $DriftDatabaseRepository {
implements IDatabaseRepository<GeneratedDatabase> { DriftDatabaseRepository([QueryExecutor? executor])
DriftDatabaseRepository() : super(_openConnection()); : super(executor ?? driftDatabase(name: 'db'));
static LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
// Work around limitations on old Android versions
// https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#problems-on-android-6
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
// Make sqlite3 pick a more suitable location for temporary files - the
// one from the system may be inaccessible due to sandboxing.
// https://github.com/simolus3/moor/issues/876#issuecomment-710013503
final cachebase = (await getTemporaryDirectory()).path;
// We can't access /tmp on Android, which sqlite3 would try by default.
// Explicitly tell it about the correct temporary directory.
sqlite3.tempDirectory = cachebase;
return NativeDatabase.createInBackground(file);
});
}
@override
GeneratedDatabase init() => this;
@override @override
int get schemaVersion => 1; int get schemaVersion => 1;
@override
// ignore: no-empty-block
void migrateDB() {
// Migrations are handled automatically using the migrator field
}
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(), onCreate: (m) => m.createAll(),

View File

@ -13,8 +13,8 @@ class LogDriftRepository implements ILogRepository {
const LogDriftRepository(this.db); const LogDriftRepository(this.db);
@override @override
Future<List<LogMessage>> fetchLogs() async { Future<List<LogMessage>> fetchAll() async {
return await db.managers.logs.map((l) => l.toModel()).get(); return await db.managers.logs.map(_toModel).get();
} }
@override @override
@ -82,15 +82,13 @@ class LogDriftRepository implements ILogRepository {
} }
} }
extension _LogToLogMessage on Log { LogMessage _toModel(Log log) {
LogMessage toModel() { return LogMessage(
return LogMessage( content: log.content,
content: content, createdAt: log.createdAt,
createdAt: createdAt, level: log.level,
level: level, error: log.error,
error: error, logger: log.logger,
logger: logger, stack: log.stack,
stack: stack, );
);
}
} }

View File

@ -0,0 +1,44 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart';
import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
class RemoteAssetDriftRepository
with LogContext
implements IRemoteAssetRepository {
final DriftDatabaseRepository _db;
const RemoteAssetDriftRepository(this._db);
@override
Future<bool> addAll(Iterable<RemoteAsset> assets) async {
try {
await _db.batch((batch) => batch.insertAllOnConflictUpdate(
_db.remoteAsset,
assets.map(_toEntity),
));
return true;
} catch (e, s) {
log.severe("Cannot insert remote assets into table", e, s);
return false;
}
}
}
RemoteAssetCompanion _toEntity(RemoteAsset asset) {
return RemoteAssetCompanion.insert(
name: asset.name,
checksum: asset.checksum,
height: Value(asset.height),
width: Value(asset.width),
type: asset.type,
createdTime: asset.createdTime,
remoteId: asset.remoteId,
duration: Value(asset.duration),
modifiedTime: Value(asset.modifiedTime),
livePhotoVideoId: Value(asset.livePhotoVideoId),
);
}

View File

@ -63,7 +63,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
@override @override
FutureOr<void> clearStore() async { FutureOr<void> clearStore() async {
await db.managers.store.delete(); await db.managers.store.delete();
;
} }
FutureOr<T?> _getValueFromStoreData<T, U>( FutureOr<T?> _getValueFromStoreData<T, U>(

View File

@ -13,15 +13,15 @@ class UserDriftRepository with LogContext implements IUserRepository {
const UserDriftRepository(this.db); const UserDriftRepository(this.db);
@override @override
FutureOr<User?> getUser(String userId) async { FutureOr<User?> fetch(String userId) async {
return await db.managers.user return await db.managers.user
.filter((f) => f.id.equals(userId)) .filter((f) => f.id.equals(userId))
.map((u) => u.toModel()) .map(_toModel)
.getSingleOrNull(); .getSingleOrNull();
} }
@override @override
FutureOr<bool> insertUser(User user) async { FutureOr<bool> add(User user) async {
try { try {
await db.into(db.user).insertOnConflictUpdate( await db.into(db.user).insertOnConflictUpdate(
UserCompanion.insert( UserCompanion.insert(
@ -46,20 +46,18 @@ class UserDriftRepository with LogContext implements IUserRepository {
} }
} }
extension _UserDataToUser on UserData { User _toModel(UserData user) {
User toModel() { return User(
return User( id: user.id,
id: id, email: user.email,
email: email, avatarColor: user.avatarColor,
avatarColor: avatarColor, inTimeline: user.inTimeline,
inTimeline: inTimeline, isAdmin: user.isAdmin,
isAdmin: isAdmin, memoryEnabled: user.memoryEnabled,
memoryEnabled: memoryEnabled, name: user.name,
name: name, profileImagePath: user.profileImagePath,
profileImagePath: profileImagePath, quotaSizeInBytes: user.quotaSizeInBytes,
quotaSizeInBytes: quotaSizeInBytes, quotaUsageInBytes: user.quotaUsageInBytes,
quotaUsageInBytes: quotaUsageInBytes, updatedAt: user.updatedAt,
updatedAt: updatedAt, );
);
}
} }

View File

@ -1,15 +1,12 @@
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart'; import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart'; import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ServerInfoService with LogContext { class ServerInfoService with LogContext {
final ImmichApiClient _api; final ServerApi _serverInfo;
ServerApi get _serverInfo => _api.getServerApi(); const ServerInfoService(this._serverInfo);
ServerInfoService(this._api);
Future<ServerFeatures?> getServerFeatures() async { Future<ServerFeatures?> getServerFeatures() async {
try { try {

View File

@ -0,0 +1,71 @@
import 'package:drift/isolate.dart';
import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart';
import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/constants/globals.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/log_manager.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class SyncService with LogContext {
final ImmichApiClient _appClient;
final DriftDatabaseRepository _db;
SyncService(this._appClient, this._db);
Future<bool> doFullSyncForUserDrift(
User user, {
DateTime? updatedUtil,
int? limit,
}) async {
final clientData = _appClient.clientData;
try {
await _db.computeWithDatabase(
connect: (connection) => DriftDatabaseRepository(connection),
computation: (database) async {
ServiceLocator.configureServicesForIsolate(database: database);
LogManager.I.init();
final logger = Logger("SyncService <Isolate>");
final syncClient =
ImmichApiClient.clientData(clientData).getSyncApi();
final chunkSize = limit ?? kFullSyncChunkSize;
final updatedTill = updatedUtil ?? DateTime.now().toUtc();
updatedUtil ??= DateTime.now().toUtc();
String? lastAssetId;
while (true) {
logger.info(
"Requesting more chunks from lastId - ${lastAssetId ?? "<initial_fetch>"}",
);
final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto(
limit: chunkSize,
updatedUntil: updatedTill,
lastId: lastAssetId,
userId: user.id,
));
if (assets == null) {
break;
}
await di<IRemoteAssetRepository>()
.addAll(assets.map(RemoteAsset.fromDto));
lastAssetId = assets.lastOrNull?.id;
if (assets.length != chunkSize) break;
}
return true;
},
);
} catch (e, s) {
log.severe("Error performing full sync for user - ${user.name}", e, s);
}
return false;
}
}

View File

@ -1,14 +1,11 @@
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class UserService with LogContext { class UserService with LogContext {
final ImmichApiClient _api; final UsersApi _userApi;
UsersApi get _userApi => _api.getUsersApi(); const UserService(this._userApi);
UserService(this._api);
Future<User?> getMyUser() async { Future<User?> getMyUser() async {
try { try {
@ -21,7 +18,7 @@ class UserService with LogContext {
final preferencesDto = await _userApi.getMyPreferences(); final preferencesDto = await _userApi.getMyPreferences();
return User.fromAdminDto(userDto, preferencesDto); return User.fromAdminDto(userDto, preferencesDto);
} catch (e, s) { } catch (e, s) {
log.severe("Error while fetching server features", e, s); log.severe("Error while fetching my user", e, s);
} }
return null; return null;
} }

View File

@ -50,7 +50,7 @@ class StoreUserConverter extends IStoreConverter<User, String> {
@override @override
Future<User?> fromPrimitive(String value) async { Future<User?> fromPrimitive(String value) async {
return await di<IUserRepository>().getUser(value); return await di<IUserRepository>().fetch(value);
} }
@override @override

View File

@ -3,6 +3,8 @@ import 'package:immich_mobile/i18n/strings.g.dart';
import 'package:immich_mobile/immich_app.dart'; import 'package:immich_mobile/immich_app.dart';
import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/log_manager.dart'; import 'package:immich_mobile/utils/log_manager.dart';
// ignore: depend_on_referenced_packages
import 'package:stack_trace/stack_trace.dart' as stack_trace;
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -13,5 +15,11 @@ void main() {
// Init localization // Init localization
LocaleSettings.useDeviceLocale(); LocaleSettings.useDeviceLocale();
FlutterError.demangleStackTrace = (StackTrace stack) {
if (stack is stack_trace.Trace) return stack.vmTrace;
if (stack is stack_trace.Chain) return stack.toTrace().vmTrace;
return stack;
};
runApp(const ImmichApp()); runApp(const ImmichApp());
} }

View File

@ -1,6 +1,8 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/presentation/router/router.dart'; import 'package:immich_mobile/domain/services/sync.service.dart';
import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart';
import 'package:immich_mobile/service_locator.dart';
@RoutePage() @RoutePage()
class HomePage extends StatelessWidget { class HomePage extends StatelessWidget {
@ -10,8 +12,9 @@ class HomePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: () => context.router.navigate(const SettingsRoute()), onPressed: () => di<SyncService>()
child: const Text('Settings'), .doFullSyncForUserDrift(di<CurrentUserCubit>().state),
child: const Text('Sync'),
), ),
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/domain/services/login.service.dart';
import 'package:immich_mobile/domain/services/sync.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/i18n/strings.g.dart'; import 'package:immich_mobile/i18n/strings.g.dart';
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
@ -65,6 +66,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
di<IStoreRepository>().set(StoreKey.serverEndpoint, url); di<IStoreRepository>().set(StoreKey.serverEndpoint, url);
ServiceLocator.registerPostValidationServices(url); ServiceLocator.registerPostValidationServices(url);
ServiceLocator.registerPostGlobalStates();
// Fetch server features // Fetch server features
await di<ServerFeatureConfigCubit>().getFeatures(); await di<ServerFeatureConfigCubit>().getFeatures();
@ -132,7 +134,9 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
// Register user // Register user
ServiceLocator.registerCurrentUser(user); ServiceLocator.registerCurrentUser(user);
await di<IUserRepository>().insertUser(user); await di<IUserRepository>().add(user);
// Sync assets in background
unawaited(di<SyncService>().doFullSyncForUserDrift(user));
emit(state.copyWith( emit(state.copyWith(
isValidationInProgress: false, isValidationInProgress: false,

View File

@ -23,7 +23,7 @@ class TabControllerPage extends StatelessWidget {
// Pop-back to photos tab or if already in photos tab, close the app // Pop-back to photos tab or if already in photos tab, close the app
return PopScope( return PopScope(
canPop: tabsRouter.activeIndex == 0, canPop: tabsRouter.activeIndex == 0,
onPopInvoked: (didPop) => onPopInvokedWithResult: (didPop, _) =>
!didPop ? tabsRouter.setActiveIndex(0) : null, !didPop ? tabsRouter.setActiveIndex(0) : null,
child: _TabControllerAdaptiveScaffold( child: _TabControllerAdaptiveScaffold(
body: (ctxx) => child, body: (ctxx) => child,

View File

@ -1,15 +1,18 @@
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/domain/repositories/log.repository.dart'; import 'package:immich_mobile/domain/repositories/log.repository.dart';
import 'package:immich_mobile/domain/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/domain/repositories/store.repository.dart'; import 'package:immich_mobile/domain/repositories/store.repository.dart';
import 'package:immich_mobile/domain/repositories/user.repository.dart'; import 'package:immich_mobile/domain/repositories/user.repository.dart';
import 'package:immich_mobile/domain/services/app_setting.service.dart'; import 'package:immich_mobile/domain/services/app_setting.service.dart';
import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/domain/services/login.service.dart';
import 'package:immich_mobile/domain/services/server_info.service.dart'; import 'package:immich_mobile/domain/services/server_info.service.dart';
import 'package:immich_mobile/domain/services/sync.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart';
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
@ -23,41 +26,49 @@ class ServiceLocator {
const ServiceLocator._internal(); const ServiceLocator._internal();
static void configureServices() { static void configureServices() {
// Register DB
di.registerSingleton<DriftDatabaseRepository>(DriftDatabaseRepository()); di.registerSingleton<DriftDatabaseRepository>(DriftDatabaseRepository());
_registerPreValidationServices(); _registerRepositories();
_registerPreGlobalStates();
} }
static void _registerPreValidationServices() { static void configureServicesForIsolate({
// ====== DOMAIN required DriftDatabaseRepository database,
}) {
di.registerSingleton<DriftDatabaseRepository>(database);
_registerRepositories();
}
// Init store static void _registerRepositories() {
/// Repositories
di.registerFactory<IStoreRepository>(() => StoreDriftRepository(di())); di.registerFactory<IStoreRepository>(() => StoreDriftRepository(di()));
// Logs
di.registerFactory<ILogRepository>(() => LogDriftRepository(di())); di.registerFactory<ILogRepository>(() => LogDriftRepository(di()));
// App Settings
di.registerFactory<AppSettingService>(() => AppSettingService(di())); di.registerFactory<AppSettingService>(() => AppSettingService(di()));
// User Repo
di.registerFactory<IUserRepository>(() => UserDriftRepository(di())); di.registerFactory<IUserRepository>(() => UserDriftRepository(di()));
// Login Service di.registerFactory<IRemoteAssetRepository>(
() => RemoteAssetDriftRepository(di()),
);
/// Services
di.registerFactory<LoginService>(() => const LoginService()); di.registerFactory<LoginService>(() => const LoginService());
}
// ====== PRESENTATION static void _registerPreGlobalStates() {
// App router
di.registerSingleton<AppRouter>(AppRouter()); di.registerSingleton<AppRouter>(AppRouter());
// Global states
di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di())); di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di()));
} }
static void registerPostValidationServices(String endpoint) { static void registerPostValidationServices(String endpoint) {
di.registerSingleton<ImmichApiClient>(ImmichApiClient(endpoint: endpoint)); di.registerSingleton<ImmichApiClient>(ImmichApiClient(endpoint: endpoint));
di.registerFactory<UserService>(() => UserService(
di<ImmichApiClient>().getUsersApi(),
));
di.registerFactory<ServerInfoService>(() => ServerInfoService(
di<ImmichApiClient>().getServerApi(),
));
di.registerFactory<SyncService>(() => SyncService(di(), di()));
}
// ====== DOMAIN static void registerPostGlobalStates() {
di.registerFactory<UserService>(() => UserService(di()));
di.registerFactory<ServerInfoService>(() => ServerInfoService(di()));
// ====== PRESENTATION
di.registerLazySingleton<ServerFeatureConfigCubit>( di.registerLazySingleton<ServerFeatureConfigCubit>(
() => ServerFeatureConfigCubit(di()), () => ServerFeatureConfigCubit(di()),
); );

View File

@ -3,6 +3,9 @@ import 'package:flutter/material.dart';
/// Log messages stored in the DB /// Log messages stored in the DB
const int kLogMessageLimit = 500; const int kLogMessageLimit = 500;
/// Chunked asset sync size
const int kFullSyncChunkSize = 10000;
/// Headers /// Headers
// Auth header // Auth header
const String kImmichHeaderAuthKey = "x-immich-user-token"; const String kImmichHeaderAuthKey = "x-immich-user-token";

View File

@ -0,0 +1,3 @@
extension StringNumberUtils on String {
int? tryParseInt() => int.tryParse(this);
}

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
@ -10,9 +12,21 @@ import 'package:immich_mobile/utils/constants/globals.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@immutable
class ImmichApiClientData {
final String endpoint;
final Map<String, String> headersMap;
const ImmichApiClientData({required this.endpoint, required this.headersMap});
}
class ImmichApiClient extends ApiClient with LogContext { class ImmichApiClient extends ApiClient with LogContext {
ImmichApiClient({required String endpoint}) : super(basePath: endpoint); ImmichApiClient({required String endpoint}) : super(basePath: endpoint);
/// Used to recreate the client in Isolates
ImmichApiClientData get clientData =>
ImmichApiClientData(endpoint: basePath, headersMap: defaultHeaderMap);
Future<void> init({String? accessToken}) async { Future<void> init({String? accessToken}) async {
final token = final token =
accessToken ?? (await di<IStoreRepository>().get(StoreKey.accessToken)); accessToken ?? (await di<IStoreRepository>().get(StoreKey.accessToken));
@ -33,6 +47,15 @@ class ImmichApiClient extends ApiClient with LogContext {
addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem); addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem);
} }
factory ImmichApiClient.clientData(ImmichApiClientData data) {
final client = ImmichApiClient(endpoint: data.endpoint);
for (final entry in data.headersMap.entries) {
client.addDefaultHeader(entry.key, entry.value);
}
return client;
}
@override @override
Future<Response> invokeAPI( Future<Response> invokeAPI(
String path, String path,
@ -85,8 +108,35 @@ class ImmichApiClient extends ApiClient with LogContext {
return ApiClient.fromJson(value, targetType, growable: growable); return ApiClient.fromJson(value, targetType, growable: growable);
} }
@override
// ignore: avoid-dynamic
Future<dynamic> deserializeAsync(
String value,
String targetType, {
bool growable = false,
}) =>
deserialize(value, targetType, growable: growable);
@override
// ignore: avoid-dynamic
Future<dynamic> deserialize(
String value,
String targetType, {
bool growable = false,
}) async {
targetType = targetType.replaceAll(' ', '');
return targetType == 'String'
? value
: fromJson(
await compute((String j) => json.decode(j), value),
targetType,
growable: growable,
);
}
UsersApi getUsersApi() => UsersApi(this); UsersApi getUsersApi() => UsersApi(this);
ServerApi getServerApi() => ServerApi(this); ServerApi getServerApi() => ServerApi(this);
AuthenticationApi getAuthenticationApi() => AuthenticationApi(this); AuthenticationApi getAuthenticationApi() => AuthenticationApi(this);
OAuthApi getOAuthApi() => OAuthApi(this); OAuthApi getOAuthApi() => OAuthApi(this);
SyncApi getSyncApi() => SyncApi(this);
} }

View File

@ -27,6 +27,7 @@ class LogManager {
debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
if (record.error != null && record.stackTrace != null) { if (record.error != null && record.stackTrace != null) {
debugPrint('${record.error}'); debugPrint('${record.error}');
debugPrintStack(stackTrace: record.stackTrace);
} }
return true; return true;
}()); }());

View File

@ -278,6 +278,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.20.1" version: "2.20.1"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: c670c947fe17ad149678a43fdbbfdb69321f0c83d315043e34e8ad2729e11f49
url: "https://pub.dev"
source: hosted
version: "0.2.0"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -20,6 +20,7 @@ dependencies:
flutter_bloc: ^8.1.6 flutter_bloc: ^8.1.6
# Database # Database
drift: ^2.20.0 drift: ^2.20.0
drift_flutter: ^0.2.0
sqlite3: ^2.4.6 sqlite3: ^2.4.6
sqlite3_flutter_libs: ^0.5.24 sqlite3_flutter_libs: ^0.5.24
# Network # Network