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:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/domain/models/asset/asset.model.dart';
class Asset extends Table {
const Asset();
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get checksum => text().unique()();
IntColumn get height => integer()();
IntColumn get width => integer()();
IntColumn get height => integer().nullable()();
IntColumn get width => integer().nullable()();
IntColumn get type => intEnum<AssetType>()();
DateTimeColumn get createdTime => dateTime()();
DateTimeColumn get modifiedTime =>
dateTime().withDefault(currentDateAndTime)();
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 {
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 {
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 {
/// Fetches all logs
FutureOr<List<LogMessage>> fetchLogs();
FutureOr<List<LogMessage>> fetchAll();
/// Inserts a new log into the DB
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 {
/// Fetches user
FutureOr<User?> getUser(String userId);
FutureOr<User?> fetch(String userId);
/// Insert user
FutureOr<bool> insertUser(User user);
FutureOr<bool> add(User user);
}

View File

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

View File

@ -1,5 +1,5 @@
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
class LocalAsset extends Asset {
@ -7,7 +7,6 @@ class LocalAsset extends Asset {
const LocalAsset({
required this.localId,
required super.id,
required super.name,
required super.checksum,
required super.height,
@ -16,23 +15,20 @@ class LocalAsset extends Asset {
required super.createdTime,
required super.modifiedTime,
required super.duration,
required super.isLivePhoto,
});
@override
String toString() => """
{
"id": $id,
"localId": "$localId",
"name": "$name",
"checksum": "$checksum",
"height": $height,
"width": $width,
"height": ${height ?? "-"},
"width": ${width ?? "-"},
"type": "$type",
"createdTime": "$createdTime",
"modifiedTime": "$modifiedTime",
"duration": "$duration",
"isLivePhoto": "$isLivePhoto",
}""";
@override
@ -47,7 +43,6 @@ class LocalAsset extends Asset {
@override
LocalAsset copyWith({
int? id,
String? localId,
String? name,
String? checksum,
@ -57,10 +52,8 @@ class LocalAsset extends Asset {
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
bool? isLivePhoto,
}) {
return LocalAsset(
id: id ?? this.id,
localId: localId ?? this.localId,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
@ -70,7 +63,6 @@ class LocalAsset extends Asset {
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
);
}
}

View File

@ -1,13 +1,15 @@
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
class RemoteAsset extends Asset {
final String remoteId;
final String? livePhotoVideoId;
const RemoteAsset({
required this.remoteId,
required super.id,
required super.name,
required super.checksum,
required super.height,
@ -16,23 +18,22 @@ class RemoteAsset extends Asset {
required super.createdTime,
required super.modifiedTime,
required super.duration,
required super.isLivePhoto,
this.livePhotoVideoId,
});
@override
String toString() => """
{
"id": $id,
"remoteId": "$remoteId",
"name": "$name",
"checksum": "$checksum",
"height": $height,
"width": $width,
"height": ${height ?? "-"},
"width": ${width ?? "-"},
"type": "$type",
"createdTime": "$createdTime",
"modifiedTime": "$modifiedTime",
"duration": "$duration",
"isLivePhoto": "$isLivePhoto",
"livePhotoVideoId": "${livePhotoVideoId ?? "-"}",
}""";
@override
@ -47,7 +48,6 @@ class RemoteAsset extends Asset {
@override
RemoteAsset copyWith({
int? id,
String? remoteId,
String? name,
String? checksum,
@ -57,10 +57,9 @@ class RemoteAsset extends Asset {
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
bool? isLivePhoto,
String? livePhotoVideoId,
}) {
return RemoteAsset(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
@ -70,7 +69,27 @@ class RemoteAsset extends Asset {
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
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/native.dart';
// ignore: depend_on_referenced_packages
import 'package:drift_dev/api/migrations.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/entities/album.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/store.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';
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset])
class DriftDatabaseRepository extends $DriftDatabaseRepository
implements IDatabaseRepository<GeneratedDatabase> {
DriftDatabaseRepository() : super(_openConnection());
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;
class DriftDatabaseRepository extends $DriftDatabaseRepository {
DriftDatabaseRepository([QueryExecutor? executor])
: super(executor ?? driftDatabase(name: 'db'));
@override
int get schemaVersion => 1;
@override
// ignore: no-empty-block
void migrateDB() {
// Migrations are handled automatically using the migrator field
}
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),

View File

@ -13,8 +13,8 @@ class LogDriftRepository implements ILogRepository {
const LogDriftRepository(this.db);
@override
Future<List<LogMessage>> fetchLogs() async {
return await db.managers.logs.map((l) => l.toModel()).get();
Future<List<LogMessage>> fetchAll() async {
return await db.managers.logs.map(_toModel).get();
}
@override
@ -82,15 +82,13 @@ class LogDriftRepository implements ILogRepository {
}
}
extension _LogToLogMessage on Log {
LogMessage toModel() {
return LogMessage(
content: content,
createdAt: createdAt,
level: level,
error: error,
logger: logger,
stack: stack,
);
}
LogMessage _toModel(Log log) {
return LogMessage(
content: log.content,
createdAt: log.createdAt,
level: log.level,
error: log.error,
logger: log.logger,
stack: log.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
FutureOr<void> clearStore() async {
await db.managers.store.delete();
;
}
FutureOr<T?> _getValueFromStoreData<T, U>(

View File

@ -13,15 +13,15 @@ class UserDriftRepository with LogContext implements IUserRepository {
const UserDriftRepository(this.db);
@override
FutureOr<User?> getUser(String userId) async {
FutureOr<User?> fetch(String userId) async {
return await db.managers.user
.filter((f) => f.id.equals(userId))
.map((u) => u.toModel())
.map(_toModel)
.getSingleOrNull();
}
@override
FutureOr<bool> insertUser(User user) async {
FutureOr<bool> add(User user) async {
try {
await db.into(db.user).insertOnConflictUpdate(
UserCompanion.insert(
@ -46,20 +46,18 @@ class UserDriftRepository with LogContext implements IUserRepository {
}
}
extension _UserDataToUser on UserData {
User toModel() {
return User(
id: id,
email: email,
avatarColor: avatarColor,
inTimeline: inTimeline,
isAdmin: isAdmin,
memoryEnabled: memoryEnabled,
name: name,
profileImagePath: profileImagePath,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
updatedAt: updatedAt,
);
}
User _toModel(UserData user) {
return User(
id: user.id,
email: user.email,
avatarColor: user.avatarColor,
inTimeline: user.inTimeline,
isAdmin: user.isAdmin,
memoryEnabled: user.memoryEnabled,
name: user.name,
profileImagePath: user.profileImagePath,
quotaSizeInBytes: user.quotaSizeInBytes,
quotaUsageInBytes: user.quotaUsageInBytes,
updatedAt: user.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_features.model.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart';
class ServerInfoService with LogContext {
final ImmichApiClient _api;
final ServerApi _serverInfo;
ServerApi get _serverInfo => _api.getServerApi();
ServerInfoService(this._api);
const ServerInfoService(this._serverInfo);
Future<ServerFeatures?> getServerFeatures() async {
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/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart';
class UserService with LogContext {
final ImmichApiClient _api;
final UsersApi _userApi;
UsersApi get _userApi => _api.getUsersApi();
UserService(this._api);
const UserService(this._userApi);
Future<User?> getMyUser() async {
try {
@ -21,7 +18,7 @@ class UserService with LogContext {
final preferencesDto = await _userApi.getMyPreferences();
return User.fromAdminDto(userDto, preferencesDto);
} catch (e, s) {
log.severe("Error while fetching server features", e, s);
log.severe("Error while fetching my user", e, s);
}
return null;
}

View File

@ -50,7 +50,7 @@ class StoreUserConverter extends IStoreConverter<User, String> {
@override
Future<User?> fromPrimitive(String value) async {
return await di<IUserRepository>().getUser(value);
return await di<IUserRepository>().fetch(value);
}
@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/service_locator.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() {
WidgetsFlutterBinding.ensureInitialized();
@ -13,5 +15,11 @@ void main() {
// Init localization
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());
}

View File

@ -1,6 +1,8 @@
import 'package:auto_route/auto_route.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()
class HomePage extends StatelessWidget {
@ -10,8 +12,9 @@ class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () => context.router.navigate(const SettingsRoute()),
child: const Text('Settings'),
onPressed: () => di<SyncService>()
.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/models/store.model.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/i18n/strings.g.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);
ServiceLocator.registerPostValidationServices(url);
ServiceLocator.registerPostGlobalStates();
// Fetch server features
await di<ServerFeatureConfigCubit>().getFeatures();
@ -132,7 +134,9 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
// Register 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(
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
return PopScope(
canPop: tabsRouter.activeIndex == 0,
onPopInvoked: (didPop) =>
onPopInvokedWithResult: (didPop, _) =>
!didPop ? tabsRouter.setActiveIndex(0) : null,
child: _TabControllerAdaptiveScaffold(
body: (ctxx) => child,

View File

@ -1,15 +1,18 @@
import 'package:get_it/get_it.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/user.interface.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/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/user.repository.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/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/presentation/modules/common/states/current_user.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();
static void configureServices() {
// Register DB
di.registerSingleton<DriftDatabaseRepository>(DriftDatabaseRepository());
_registerPreValidationServices();
_registerRepositories();
_registerPreGlobalStates();
}
static void _registerPreValidationServices() {
// ====== DOMAIN
static void configureServicesForIsolate({
required DriftDatabaseRepository database,
}) {
di.registerSingleton<DriftDatabaseRepository>(database);
_registerRepositories();
}
// Init store
static void _registerRepositories() {
/// Repositories
di.registerFactory<IStoreRepository>(() => StoreDriftRepository(di()));
// Logs
di.registerFactory<ILogRepository>(() => LogDriftRepository(di()));
// App Settings
di.registerFactory<AppSettingService>(() => AppSettingService(di()));
// User Repo
di.registerFactory<IUserRepository>(() => UserDriftRepository(di()));
// Login Service
di.registerFactory<IRemoteAssetRepository>(
() => RemoteAssetDriftRepository(di()),
);
/// Services
di.registerFactory<LoginService>(() => const LoginService());
}
// ====== PRESENTATION
// App router
static void _registerPreGlobalStates() {
di.registerSingleton<AppRouter>(AppRouter());
// Global states
di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di()));
}
static void registerPostValidationServices(String 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
di.registerFactory<UserService>(() => UserService(di()));
di.registerFactory<ServerInfoService>(() => ServerInfoService(di()));
// ====== PRESENTATION
static void registerPostGlobalStates() {
di.registerLazySingleton<ServerFeatureConfigCubit>(
() => ServerFeatureConfigCubit(di()),
);

View File

@ -3,6 +3,9 @@ import 'package:flutter/material.dart';
/// Log messages stored in the DB
const int kLogMessageLimit = 500;
/// Chunked asset sync size
const int kFullSyncChunkSize = 10000;
/// Headers
// Auth header
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 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.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: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 {
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 {
final token =
accessToken ?? (await di<IStoreRepository>().get(StoreKey.accessToken));
@ -33,6 +47,15 @@ class ImmichApiClient extends ApiClient with LogContext {
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
Future<Response> invokeAPI(
String path,
@ -85,8 +108,35 @@ class ImmichApiClient extends ApiClient with LogContext {
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);
ServerApi getServerApi() => ServerApi(this);
AuthenticationApi getAuthenticationApi() => AuthenticationApi(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}');
if (record.error != null && record.stackTrace != null) {
debugPrint('${record.error}');
debugPrintStack(stackTrace: record.stackTrace);
}
return true;
}());

View File

@ -278,6 +278,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

View File

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