diff --git a/mobile-v2/lib/domain/entities/asset.entity.dart b/mobile-v2/lib/domain/entities/asset.entity.dart index 2daf326207..e2cf15f725 100644 --- a/mobile-v2/lib/domain/entities/asset.entity.dart +++ b/mobile-v2/lib/domain/entities/asset.entity.dart @@ -1,11 +1,11 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset.model.dart'; -class LocalAsset extends Table { - const LocalAsset(); +class Asset extends Table { + const Asset(); IntColumn get id => integer().autoIncrement()(); - TextColumn get localId => text().unique()(); + TextColumn get name => text()(); TextColumn get checksum => text().unique()(); IntColumn get height => integer()(); diff --git a/mobile-v2/lib/domain/entities/local_asset.entity.dart b/mobile-v2/lib/domain/entities/local_asset.entity.dart new file mode 100644 index 0000000000..aa2643962f --- /dev/null +++ b/mobile-v2/lib/domain/entities/local_asset.entity.dart @@ -0,0 +1,8 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/entities/asset.entity.dart'; + +class LocalAsset extends Asset { + const LocalAsset(); + + TextColumn get localId => text().unique()(); +} diff --git a/mobile-v2/lib/domain/entities/remote_asset.entity.dart b/mobile-v2/lib/domain/entities/remote_asset.entity.dart new file mode 100644 index 0000000000..4cf596742b --- /dev/null +++ b/mobile-v2/lib/domain/entities/remote_asset.entity.dart @@ -0,0 +1,8 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/entities/asset.entity.dart'; + +class RemoteAsset extends Asset { + const RemoteAsset(); + + TextColumn get remoteId => text().unique()(); +} diff --git a/mobile-v2/lib/domain/models/asset.model.dart b/mobile-v2/lib/domain/models/asset.model.dart index 211e0ea1cd..cda67c4059 100644 --- a/mobile-v2/lib/domain/models/asset.model.dart +++ b/mobile-v2/lib/domain/models/asset.model.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - enum AssetType { // do not change this order! other, @@ -8,10 +6,8 @@ enum AssetType { audio, } -@immutable -class LocalAsset { +class Asset { final int id; - final String localId; final String name; final String checksum; final int height; @@ -22,9 +18,8 @@ class LocalAsset { final int duration; final bool isLivePhoto; - const LocalAsset({ + const Asset({ required this.id, - required this.localId, required this.name, required this.checksum, required this.height, @@ -36,16 +31,36 @@ class LocalAsset { required this.isLivePhoto, }); - @override - String toString() { - return 'LocalAsset(id: $id, localId: $localId, name: $name, checksum: $checksum, height: $height, width: $width, type: $type, createdTime: $createdTime, modifiedTime: $modifiedTime, duration: $duration, isLivePhoto: $isLivePhoto)'; + Asset copyWith({ + int? id, + String? name, + String? checksum, + int? height, + int? width, + AssetType? type, + 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, + width: width ?? this.width, + type: type ?? this.type, + createdTime: createdTime ?? this.createdTime, + modifiedTime: modifiedTime ?? this.modifiedTime, + duration: duration ?? this.duration, + isLivePhoto: isLivePhoto ?? this.isLivePhoto, + ); } - String toJSON() { - return """ + @override + String toString() => """ { "id": $id, - "localId": "$localId", "name": "$name", "checksum": "$checksum", "height": $height, @@ -56,19 +71,26 @@ class LocalAsset { "duration": "$duration", "isLivePhoto": "$isLivePhoto", }"""; - } @override - bool operator ==(covariant LocalAsset other) { + bool operator ==(covariant Asset other) { if (identical(this, other)) return true; - return other.hashCode == hashCode; + return other.id == id && + 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; } @override int get hashCode { return id.hashCode ^ - localId.hashCode ^ name.hashCode ^ checksum.hashCode ^ height.hashCode ^ diff --git a/mobile-v2/lib/domain/models/local_asset.model.dart b/mobile-v2/lib/domain/models/local_asset.model.dart new file mode 100644 index 0000000000..4962bc5a08 --- /dev/null +++ b/mobile-v2/lib/domain/models/local_asset.model.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; + +@immutable +class LocalAsset extends Asset { + final String localId; + + const LocalAsset({ + required this.localId, + required super.id, + required super.name, + required super.checksum, + required super.height, + required super.width, + required super.type, + 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, + "type": "$type", + "createdTime": "$createdTime", + "modifiedTime": "$modifiedTime", + "duration": "$duration", + "isLivePhoto": "$isLivePhoto", +}"""; + + @override + bool operator ==(covariant LocalAsset other) { + if (identical(this, other)) return true; + + return super == (other) && other.localId == localId; + } + + @override + int get hashCode => super.hashCode ^ localId.hashCode; + + @override + LocalAsset copyWith({ + int? id, + String? localId, + String? name, + String? checksum, + int? height, + int? width, + AssetType? type, + 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, + height: height ?? this.height, + width: width ?? this.width, + type: type ?? this.type, + createdTime: createdTime ?? this.createdTime, + modifiedTime: modifiedTime ?? this.modifiedTime, + duration: duration ?? this.duration, + isLivePhoto: isLivePhoto ?? this.isLivePhoto, + ); + } +} diff --git a/mobile-v2/lib/domain/models/remote_asset.model.dart b/mobile-v2/lib/domain/models/remote_asset.model.dart new file mode 100644 index 0000000000..f30c0b5440 --- /dev/null +++ b/mobile-v2/lib/domain/models/remote_asset.model.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/asset.model.dart'; + +@immutable +class RemoteAsset extends Asset { + final String remoteId; + + const RemoteAsset({ + required this.remoteId, + required super.id, + required super.name, + required super.checksum, + required super.height, + required super.width, + required super.type, + required super.createdTime, + required super.modifiedTime, + required super.duration, + required super.isLivePhoto, + }); + + @override + String toString() => """ +{ + "id": $id, + "remoteId": "$remoteId", + "name": "$name", + "checksum": "$checksum", + "height": $height, + "width": $width, + "type": "$type", + "createdTime": "$createdTime", + "modifiedTime": "$modifiedTime", + "duration": "$duration", + "isLivePhoto": "$isLivePhoto", +}"""; + + @override + bool operator ==(covariant RemoteAsset other) { + if (identical(this, other)) return true; + + return super == (other) && other.remoteId == remoteId; + } + + @override + int get hashCode => super.hashCode ^ remoteId.hashCode; + + @override + RemoteAsset copyWith({ + int? id, + String? remoteId, + String? name, + String? checksum, + int? height, + int? width, + AssetType? type, + DateTime? createdTime, + DateTime? modifiedTime, + int? duration, + bool? isLivePhoto, + }) { + return RemoteAsset( + id: id ?? this.id, + remoteId: remoteId ?? this.remoteId, + name: name ?? this.name, + checksum: checksum ?? this.checksum, + height: height ?? this.height, + width: width ?? this.width, + type: type ?? this.type, + createdTime: createdTime ?? this.createdTime, + modifiedTime: modifiedTime ?? this.modifiedTime, + duration: duration ?? this.duration, + isLivePhoto: isLivePhoto ?? this.isLivePhoto, + ); + } +} diff --git a/mobile-v2/lib/domain/models/server-info/server_config.model.dart b/mobile-v2/lib/domain/models/server-info/server_config.model.dart index 7592ce0098..a03addb4f7 100644 --- a/mobile-v2/lib/domain/models/server-info/server_config.model.dart +++ b/mobile-v2/lib/domain/models/server-info/server_config.model.dart @@ -1,4 +1,4 @@ -import 'package:openapi/openapi.dart'; +import 'package:openapi/api.dart'; class ServerConfig { final String? oauthButtonText; diff --git a/mobile-v2/lib/domain/models/server-info/server_features.model.dart b/mobile-v2/lib/domain/models/server-info/server_features.model.dart index cccaa3fbe5..a5e0ecd9ee 100644 --- a/mobile-v2/lib/domain/models/server-info/server_features.model.dart +++ b/mobile-v2/lib/domain/models/server-info/server_features.model.dart @@ -1,4 +1,4 @@ -import 'package:openapi/openapi.dart'; +import 'package:openapi/api.dart'; class ServerFeatures { final bool hasPasswordLogin; diff --git a/mobile-v2/lib/domain/models/user.model.dart b/mobile-v2/lib/domain/models/user.model.dart index ec1c794947..7ddf942731 100644 --- a/mobile-v2/lib/domain/models/user.model.dart +++ b/mobile-v2/lib/domain/models/user.model.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:openapi/openapi.dart' as api; +import 'package:openapi/api.dart' as api; class User { const User({ diff --git a/mobile-v2/lib/domain/repositories/database.repository.dart b/mobile-v2/lib/domain/repositories/database.repository.dart index 4a5f4a6fc2..de9f7ab792 100644 --- a/mobile-v2/lib/domain/repositories/database.repository.dart +++ b/mobile-v2/lib/domain/repositories/database.repository.dart @@ -6,8 +6,9 @@ import 'package:drift/native.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/entities/album.entity.dart'; -import 'package:immich_mobile/domain/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/entities/local_asset.entity.dart'; 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'; @@ -18,7 +19,7 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; import 'database.repository.drift.dart'; -@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User]) +@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset]) class DriftDatabaseRepository extends $DriftDatabaseRepository implements IDatabaseRepository { DriftDatabaseRepository() : super(_openConnection()); diff --git a/mobile-v2/lib/domain/repositories/log.repository.dart b/mobile-v2/lib/domain/repositories/log.repository.dart index c834a19fab..59efeb0c70 100644 --- a/mobile-v2/lib/domain/repositories/log.repository.dart +++ b/mobile-v2/lib/domain/repositories/log.repository.dart @@ -18,32 +18,28 @@ class LogDriftRepository implements ILogRepository { } @override - Future truncateLogs({int limit = 250}) { - return db.transaction(() async { - final totalCount = await db.managers.logs.count(); - if (totalCount > limit) { - final rowsToDelete = totalCount - limit; - await db.managers.logs - .orderBy((o) => o.createdAt.desc()) - .limit(rowsToDelete) - .delete(); - } - }); + Future truncateLogs({int limit = 250}) async { + final totalCount = await db.managers.logs.count(); + if (totalCount > limit) { + final rowsToDelete = totalCount - limit; + await db.managers.logs + .orderBy((o) => o.createdAt.desc()) + .limit(rowsToDelete) + .delete(); + } } @override FutureOr add(LogMessage log) async { try { - await db.transaction(() async { - await db.into(db.logs).insert(LogsCompanion.insert( - content: log.content, - level: log.level, - createdAt: Value(log.createdAt), - error: Value(log.error), - logger: Value(log.logger), - stack: Value(log.stack), - )); - }); + await db.into(db.logs).insert(LogsCompanion.insert( + content: log.content, + level: log.level, + createdAt: Value(log.createdAt), + error: Value(log.error), + logger: Value(log.logger), + stack: Value(log.stack), + )); return true; } catch (e) { debugPrint("Error while adding a log to the DB - $e"); diff --git a/mobile-v2/lib/domain/repositories/store.repository.dart b/mobile-v2/lib/domain/repositories/store.repository.dart index 54e314e2f8..64e22d7a9d 100644 --- a/mobile-v2/lib/domain/repositories/store.repository.dart +++ b/mobile-v2/lib/domain/repositories/store.repository.dart @@ -32,16 +32,14 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr set(StoreKey key, T value) async { try { - await db.transaction(() async { - final storeValue = key.converter.toPrimitive(value); - final intValue = (key.type == int) ? storeValue as int : null; - final stringValue = (key.type == String) ? storeValue as String : null; - await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert( - id: Value(key.id), - intValue: Value(intValue), - stringValue: Value(stringValue), - )); - }); + final storeValue = key.converter.toPrimitive(value); + final intValue = (key.type == int) ? storeValue as int : null; + final stringValue = (key.type == String) ? storeValue as String : null; + await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert( + id: Value(key.id), + intValue: Value(intValue), + stringValue: Value(stringValue), + )); return true; } catch (e, s) { log.severe("Cannot set store value - ${key.name}; id - ${key.id}", e, s); @@ -51,9 +49,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr delete(StoreKey key) async { - return await db.transaction(() async { - await db.managers.store.filter((s) => s.id.equals(key.id)).delete(); - }); + await db.managers.store.filter((s) => s.id.equals(key.id)).delete(); } @override @@ -66,9 +62,8 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr clearStore() async { - return await db.transaction(() async { - await db.managers.store.delete(); - }); + await db.managers.store.delete(); + ; } FutureOr _getValueFromStoreData( diff --git a/mobile-v2/lib/domain/repositories/user.repository.dart b/mobile-v2/lib/domain/repositories/user.repository.dart index 36af9e1034..1e39513d3d 100644 --- a/mobile-v2/lib/domain/repositories/user.repository.dart +++ b/mobile-v2/lib/domain/repositories/user.repository.dart @@ -23,8 +23,8 @@ class UserDriftRepository with LogContext implements IUserRepository { @override FutureOr insertUser(User user) async { try { - return await db.transaction(() async { - await db.into(db.user).insertOnConflictUpdate(UserCompanion.insert( + await db.into(db.user).insertOnConflictUpdate( + UserCompanion.insert( id: user.id, name: user.name, email: user.email, @@ -36,9 +36,9 @@ class UserDriftRepository with LogContext implements IUserRepository { quotaSizeInBytes: Value(user.quotaSizeInBytes), quotaUsageInBytes: Value(user.quotaSizeInBytes), updatedAt: Value(user.updatedAt), - )); - return true; - }); + ), + ); + return true; } catch (e, s) { log.severe("Cannot insert User into table - $user", e, s); return false; diff --git a/mobile-v2/lib/domain/services/login.service.dart b/mobile-v2/lib/domain/services/login.service.dart index 2570d2ffd2..7c77241512 100644 --- a/mobile-v2/lib/domain/services/login.service.dart +++ b/mobile-v2/lib/domain/services/login.service.dart @@ -1,26 +1,28 @@ import 'dart:convert'; +import 'dart:io'; -import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:http/http.dart'; import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; -import 'package:openapi/openapi.dart'; +import 'package:openapi/api.dart'; class LoginService with LogContext { const LoginService(); - Future isEndpointAvailable(Uri uri, {Dio? dio}) async { + Future isEndpointAvailable(Uri uri, {ImmichApiClient? client}) async { String baseUrl = uri.toString(); if (!baseUrl.endsWith('/api')) { baseUrl += '/api'; } - final serverAPI = - Openapi(dio: dio, basePathOverride: baseUrl, interceptors: []) - .getServerApi(); + final serverAPI = client?.getServerApi() ?? + ImmichApiClient(endpoint: baseUrl).getServerApi(); try { - await serverAPI.pingServer(validateStatus: (status) => status == 200); + await serverAPI.pingServer(); } catch (e) { log.severe("Exception occured while validating endpoint", e); return false; @@ -28,25 +30,24 @@ class LoginService with LogContext { return true; } - Future resolveEndpoint(Uri uri, {Dio? dio}) async { - final d = dio ?? Dio(); + Future resolveEndpoint(Uri uri, {Client? client}) async { String baseUrl = uri.toString(); + final d = client ?? ImmichApiClient(endpoint: baseUrl).client; try { // Check for well-known endpoint final res = await d.get( - "$baseUrl/.well-known/immich", - options: Options( - headers: {"Accept": "application/json"}, - validateStatus: (status) => status == 200, - ), + Uri.parse("$baseUrl/.well-known/immich"), + headers: {"Accept": "application/json"}, ); - final data = jsonDecode(res.data); - final endpoint = data['api']['endpoint'].toString(); + if (res.statusCode == HttpStatus.ok) { + final data = await compute(jsonDecode, res.body); + final endpoint = data['api']['endpoint'].toString(); - // Full URL is relative to base - return endpoint.startsWith('/') ? "$baseUrl$endpoint" : endpoint; + // Full URL is relative to base + return endpoint.startsWith('/') ? "$baseUrl$endpoint" : endpoint; + } } catch (e) { log.fine("Could not locate /.well-known/immich at $baseUrl", e); } @@ -57,14 +58,12 @@ class LoginService with LogContext { Future passwordLogin(String email, String password) async { try { - final loginResponse = await di().getAuthenticationApi().login( - loginCredentialDto: LoginCredentialDto((builder) { - builder.email = email; - builder.password = password; - }), - ); + final loginResponse = + await di().getAuthenticationApi().login( + LoginCredentialDto(email: email, password: password), + ); - return loginResponse.data?.accessToken; + return loginResponse?.accessToken; } catch (e, s) { log.severe("Exception occured while performing password login", e, s); } @@ -74,16 +73,14 @@ class LoginService with LogContext { Future oAuthLogin() async { const String oAuthCallbackSchema = 'app.immich'; - final oAuthApi = di().getOAuthApi(); + final oAuthApi = di().getOAuthApi(); try { final oAuthUrl = await oAuthApi.startOAuth( - oAuthConfigDto: OAuthConfigDto((builder) { - builder.redirectUri = "$oAuthCallbackSchema:/"; - }), + OAuthConfigDto(redirectUri: "$oAuthCallbackSchema:/"), ); - final oAuthUrlRes = oAuthUrl.data?.url; + final oAuthUrlRes = oAuthUrl?.url; if (oAuthUrlRes == null) { log.severe( "oAuth Server URL not available. Kindly ensure oAuth login is enabled in the server", @@ -97,12 +94,10 @@ class LoginService with LogContext { ); final loginResponse = await oAuthApi.finishOAuth( - oAuthCallbackDto: OAuthCallbackDto((builder) { - builder.url = oAuthCallbackUrl; - }), + OAuthCallbackDto(url: oAuthCallbackUrl), ); - return loginResponse.data?.accessToken; + return loginResponse?.accessToken; } catch (e) { log.severe("Exception occured while performing oauth login", e); } diff --git a/mobile-v2/lib/domain/services/server_info.service.dart b/mobile-v2/lib/domain/services/server_info.service.dart index 5803975f85..0ff11d226c 100644 --- a/mobile-v2/lib/domain/services/server_info.service.dart +++ b/mobile-v2/lib/domain/services/server_info.service.dart @@ -1,10 +1,11 @@ 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/openapi.dart'; +import 'package:openapi/api.dart'; class ServerInfoService with LogContext { - final Openapi _api; + final ImmichApiClient _api; ServerApi get _serverInfo => _api.getServerApi(); @@ -12,8 +13,7 @@ class ServerInfoService with LogContext { Future getServerFeatures() async { try { - final response = await _serverInfo.getServerFeatures(); - final dto = response.data; + final dto = await _serverInfo.getServerFeatures(); if (dto != null) { return ServerFeatures.fromDto(dto); } @@ -25,8 +25,7 @@ class ServerInfoService with LogContext { Future getServerConfig() async { try { - final response = await _serverInfo.getServerConfig(); - final dto = response.data; + final dto = await _serverInfo.getServerConfig(); if (dto != null) { return ServerConfig.fromDto(dto); } diff --git a/mobile-v2/lib/domain/services/user.service.dart b/mobile-v2/lib/domain/services/user.service.dart index 799c5e38a7..ab58a018d5 100644 --- a/mobile-v2/lib/domain/services/user.service.dart +++ b/mobile-v2/lib/domain/services/user.service.dart @@ -1,9 +1,10 @@ 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/openapi.dart'; +import 'package:openapi/api.dart'; class UserService with LogContext { - final Openapi _api; + final ImmichApiClient _api; UsersApi get _userApi => _api.getUsersApi(); @@ -11,15 +12,14 @@ class UserService with LogContext { Future getMyUser() async { try { - final response = await _userApi.getMyUser(); - final dto = response.data; - if (dto == null) { + final userDto = await _userApi.getMyUser(); + if (userDto == null) { log.severe("Cannot fetch my user."); return null; } - final preferences = await _userApi.getMyPreferences(); - return User.fromAdminDto(dto, preferences.data); + final preferencesDto = await _userApi.getMyPreferences(); + return User.fromAdminDto(userDto, preferencesDto); } catch (e, s) { log.severe("Error while fetching server features", e, s); } diff --git a/mobile-v2/lib/presentation/modules/common/states/current_user.state.dart b/mobile-v2/lib/presentation/modules/common/states/current_user.state.dart index c04c0969e1..19eab03e5f 100644 --- a/mobile-v2/lib/presentation/modules/common/states/current_user.state.dart +++ b/mobile-v2/lib/presentation/modules/common/states/current_user.state.dart @@ -3,4 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart'; class CurrentUserCubit extends Cubit { CurrentUserCubit(super.initialState); + + void updateUser(User user) => emit(user); } diff --git a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart index 518ff96b35..d54cae4103 100644 --- a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart +++ b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; @@ -11,10 +10,9 @@ 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/login/models/login_page.model.dart'; import 'package:immich_mobile/service_locator.dart'; -import 'package:immich_mobile/utils/immich_auth_interceptor.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/snackbar_manager.dart'; -import 'package:openapi/openapi.dart'; class LoginPageCubit extends Cubit with LogContext { LoginPageCubit() : super(LoginPageState.reset()); @@ -66,7 +64,7 @@ class LoginPageCubit extends Cubit with LogContext { url = await loginService.resolveEndpoint(uri); di().set(StoreKey.serverEndpoint, url); - await ServiceLocator.registerPostValidationServices(url); + ServiceLocator.registerPostValidationServices(url); // Fetch server features await di().getFeatures(); @@ -92,6 +90,9 @@ class LoginPageCubit extends Cubit with LogContext { } await _postLogin(accessToken); + } catch (e, s) { + SnackbarManager.showError(t.login.error.error_login); + log.severe("Cannot perform password login", e, s); } finally { emit(state.copyWith(isValidationInProgress: false)); } @@ -109,6 +110,9 @@ class LoginPageCubit extends Cubit with LogContext { } await _postLogin(accessToken); + } catch (e, s) { + SnackbarManager.showError(t.login.error.error_login_oauth); + log.severe("Cannot perform oauth login", e, s); } finally { emit(state.copyWith(isValidationInProgress: false)); } @@ -118,12 +122,7 @@ class LoginPageCubit extends Cubit with LogContext { await di().set(StoreKey.accessToken, accessToken); /// Set token to interceptor - final interceptor = di() - .dio - .interceptors - .firstWhereOrNull((i) => i is ImmichAuthInterceptor) - as ImmichAuthInterceptor?; - interceptor?.setAccessToken(accessToken); + await di().init(accessToken: accessToken); final user = await di().getMyUser(); if (user == null) { @@ -137,7 +136,7 @@ class LoginPageCubit extends Cubit with LogContext { emit(state.copyWith( isValidationInProgress: false, - isServerValidated: true, + isLoginSuccessful: true, )); } diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index e70571d0e1..51bc0b41ba 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -1,8 +1,3 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; @@ -15,12 +10,12 @@ 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/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'; import 'package:immich_mobile/presentation/modules/theme/states/app_theme.state.dart'; import 'package:immich_mobile/presentation/router/router.dart'; -import 'package:immich_mobile/utils/immich_auth_interceptor.dart'; -import 'package:openapi/openapi.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; final di = GetIt.I; @@ -55,47 +50,24 @@ class ServiceLocator { di.registerLazySingleton(() => AppThemeCubit(di())); } - static FutureOr registerPostValidationServices(String endpoint) async { - if (di.isRegistered()) { - return; - } - - final deviceInfo = DeviceInfoPlugin(); - final String deviceModel; - if (Platform.isIOS) { - deviceModel = (await deviceInfo.iosInfo).utsname.machine; - } else { - deviceModel = (await deviceInfo.androidInfo).model; - } + static void registerPostValidationServices(String endpoint) { + di.registerSingleton(ImmichApiClient(endpoint: endpoint)); // ====== DOMAIN - - di.registerSingleton( - Openapi( - dio: Dio( - BaseOptions( - baseUrl: endpoint, - connectTimeout: const Duration(milliseconds: 5000), - receiveTimeout: const Duration(milliseconds: 3000), - headers: { - 'deviceModel': deviceModel, - 'deviceType': Platform.operatingSystem, - }, - ), - ), - interceptors: [ImmichAuthInterceptor()], - ), - ); + di.registerFactory(() => UserService(di())); di.registerFactory(() => ServerInfoService(di())); // ====== PRESENTATION - di.registerLazySingleton( () => ServerFeatureConfigCubit(di()), ); } static void registerCurrentUser(User user) { - di.registerSingleton(CurrentUserCubit(user)); + if (di.isRegistered()) { + di().updateUser(user); + } else { + di.registerSingleton(CurrentUserCubit(user)); + } } } diff --git a/mobile-v2/lib/utils/constants/assets.gen.dart b/mobile-v2/lib/utils/constants/assets.gen.dart deleted file mode 100644 index 83cc4fdb00..0000000000 --- a/mobile-v2/lib/utils/constants/assets.gen.dart +++ /dev/null @@ -1,117 +0,0 @@ -/// GENERATED CODE - DO NOT MODIFY BY HAND -/// ***************************************************** -/// FlutterGen -/// ***************************************************** - -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use - -import 'package:flutter/widgets.dart'; - -class $AssetsImagesGen { - const $AssetsImagesGen(); - - /// File path: assets/images/immich-logo.png - AssetGenImage get immichLogo => - const AssetGenImage('assets/images/immich-logo.png'); - - /// File path: assets/images/immich-text-dark.png - AssetGenImage get immichTextDark => - const AssetGenImage('assets/images/immich-text-dark.png'); - - /// File path: assets/images/immich-text-light.png - AssetGenImage get immichTextLight => - const AssetGenImage('assets/images/immich-text-light.png'); - - /// List of all assets - List get values => - [immichLogo, immichTextDark, immichTextLight]; -} - -class Assets { - Assets._(); - - static const $AssetsImagesGen images = $AssetsImagesGen(); -} - -class AssetGenImage { - const AssetGenImage( - this._assetName, { - this.size, - this.flavors = const {}, - }); - - final String _assetName; - - final Size? size; - final Set flavors; - - Image image({ - Key? key, - AssetBundle? bundle, - ImageFrameBuilder? frameBuilder, - ImageErrorWidgetBuilder? errorBuilder, - String? semanticLabel, - bool excludeFromSemantics = false, - double? scale, - double? width, - double? height, - Color? color, - Animation? opacity, - BlendMode? colorBlendMode, - BoxFit? fit, - AlignmentGeometry alignment = Alignment.center, - ImageRepeat repeat = ImageRepeat.noRepeat, - Rect? centerSlice, - bool matchTextDirection = false, - bool gaplessPlayback = false, - bool isAntiAlias = false, - String? package, - FilterQuality filterQuality = FilterQuality.low, - int? cacheWidth, - int? cacheHeight, - }) { - return Image.asset( - _assetName, - key: key, - bundle: bundle, - frameBuilder: frameBuilder, - errorBuilder: errorBuilder, - semanticLabel: semanticLabel, - excludeFromSemantics: excludeFromSemantics, - scale: scale, - width: width, - height: height, - color: color, - opacity: opacity, - colorBlendMode: colorBlendMode, - fit: fit, - alignment: alignment, - repeat: repeat, - centerSlice: centerSlice, - matchTextDirection: matchTextDirection, - gaplessPlayback: gaplessPlayback, - isAntiAlias: isAntiAlias, - package: package, - filterQuality: filterQuality, - cacheWidth: cacheWidth, - cacheHeight: cacheHeight, - ); - } - - ImageProvider provider({ - AssetBundle? bundle, - String? package, - }) { - return AssetImage( - _assetName, - bundle: bundle, - package: package, - ); - } - - String get path => _assetName; - - String get keyName => _assetName; -} diff --git a/mobile-v2/lib/utils/constants/globals.dart b/mobile-v2/lib/utils/constants/globals.dart index 29799ce86f..27eef57d8e 100644 --- a/mobile-v2/lib/utils/constants/globals.dart +++ b/mobile-v2/lib/utils/constants/globals.dart @@ -3,5 +3,11 @@ import 'package:flutter/material.dart'; /// Log messages stored in the DB const int kLogMessageLimit = 500; +/// Headers +// Auth header +const String kImmichHeaderAuthKey = "x-immich-user-token"; +const String kImmichHeaderDeviceModel = "deviceModel"; +const String kImmichHeaderDeviceType = "deviceType"; + /// Global ScaffoldMessengerKey to show snackbars final GlobalKey kScafMessengerKey = GlobalKey(); diff --git a/mobile-v2/lib/utils/immich_api_client.dart b/mobile-v2/lib/utils/immich_api_client.dart new file mode 100644 index 0000000000..1e3b3167ff --- /dev/null +++ b/mobile-v2/lib/utils/immich_api_client.dart @@ -0,0 +1,92 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:http/http.dart'; +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/presentation/router/router.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/constants/globals.dart'; +import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; +import 'package:openapi/api.dart'; + +class ImmichApiClient extends ApiClient with LogContext { + ImmichApiClient({required String endpoint}) : super(basePath: endpoint); + + Future init({String? accessToken}) async { + final token = + accessToken ?? (await di().get(StoreKey.accessToken)); + + if (token != null) { + addDefaultHeader(kImmichHeaderAuthKey, token); + } + + final deviceInfo = DeviceInfoPlugin(); + final String deviceModel; + if (Platform.isIOS) { + deviceModel = (await deviceInfo.iosInfo).utsname.machine; + } else { + deviceModel = (await deviceInfo.androidInfo).model; + } + + addDefaultHeader(kImmichHeaderDeviceModel, deviceModel); + addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem); + } + + @override + Future invokeAPI( + String path, + String method, + List queryParams, + Object? body, + Map headerParams, + Map formParams, + String? contentType, + ) async { + final res = await super.invokeAPI( + path, + method, + queryParams, + body, + headerParams, + formParams, + contentType, + ); + + if (res.statusCode == HttpStatus.unauthorized) { + log.severe("Token invalid. Redirecting to login route"); + await di().replaceAll([const LoginRoute()]); + throw ApiException(res.statusCode, "Unauthorized"); + } + + return res; + } + + // ignore: avoid-dynamic + static dynamic _patchDto(dynamic value, String targetType) { + switch (targetType) { + case 'UserPreferencesResponseDto': + if (value is Map) { + if (value['rating'] == null) { + value['rating'] = RatingResponse().toJson(); + } + } + } + } + + // ignore: avoid-dynamic + static dynamic fromJson( + // ignore: avoid-dynamic + dynamic value, + String targetType, { + bool growable = false, + }) { + _patchDto(value, targetType); + return ApiClient.fromJson(value, targetType, growable: growable); + } + + UsersApi getUsersApi() => UsersApi(this); + ServerApi getServerApi() => ServerApi(this); + AuthenticationApi getAuthenticationApi() => AuthenticationApi(this); + OAuthApi getOAuthApi() => OAuthApi(this); +} diff --git a/mobile-v2/lib/utils/immich_auth_interceptor.dart b/mobile-v2/lib/utils/immich_auth_interceptor.dart deleted file mode 100644 index 81d8da2959..0000000000 --- a/mobile-v2/lib/utils/immich_auth_interceptor.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:immich_mobile/presentation/router/router.dart'; -import 'package:immich_mobile/service_locator.dart'; -import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; - -class ImmichAuthInterceptor extends Interceptor with LogContext { - String? _accessToken; - - void setAccessToken(String token) => _accessToken = token; - - @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - if (_accessToken != null) { - options.headers["x-immich-user-token"] = _accessToken; - } - - handler.next(options); - } - - @override - void onResponse(Response response, ResponseInterceptorHandler handler) { - if (response.statusCode == 401) { - log.severe("Token expired. Logging user out"); - di().replaceAll([const LoginRoute()]); - return; - } - handler.next(response); - } -} diff --git a/mobile-v2/lib/utils/log_manager.dart b/mobile-v2/lib/utils/log_manager.dart index f907c85e8b..2dab6667c0 100644 --- a/mobile-v2/lib/utils/log_manager.dart +++ b/mobile-v2/lib/utils/log_manager.dart @@ -14,7 +14,6 @@ import 'package:logging/logging.dart'; class LogManager { LogManager._(); static final LogManager _instance = LogManager._(); - // ignore: match-getter-setter-field-names static LogManager get I => _instance; diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index acfc59d9b9..ec7a233e70 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -262,22 +262,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - dio: - dependency: "direct main" - description: - name: dio - sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" - url: "https://pub.dev" - source: hosted - version: "5.6.0" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.dev" - source: hosted - version: "2.0.0" drift: dependency: "direct main" description: @@ -451,7 +435,7 @@ packages: source: hosted version: "2.0.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 @@ -618,22 +602,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - one_of: - dependency: transitive - description: - name: one_of - sha256: "25fe0fcf181e761c6fcd604caf9d5fdf952321be17584ba81c72c06bdaa511f0" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - one_of_serializer: - dependency: transitive - description: - name: one_of_serializer - sha256: "3f3dfb5c1578ba3afef1cb47fcc49e585e797af3f2b6c2cc7ed90aad0c5e7b83" - url: "https://pub.dev" - source: hosted - version: "1.5.0" openapi: dependency: "direct main" description: @@ -801,14 +769,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" recase: dependency: transitive description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index 559703d0fa..35d7d75e4b 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -16,14 +16,14 @@ dependencies: # OS specific path path_provider: ^2.1.4 path: ^1.9.0 - # Bloc + # State handling flutter_bloc: ^8.1.6 # Database drift: ^2.20.0 sqlite3: ^2.4.6 sqlite3_flutter_libs: ^0.5.24 # Network - dio: ^5.6.0 + http: ^1.2.2 # Route handling auto_route: ^9.2.2 # Logging @@ -55,6 +55,11 @@ dependencies: openapi: path: openapi +dependency_overrides: + # openapi uses an older version of http for backward compatibility. New versions do not have + # a breaking change so it is safer to override it and use the latest version for the app + http: ^1.2.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index b0d47b719e..01ebc79652 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -3,6 +3,7 @@ OPENAPI_GENERATOR_VERSION=v7.8.0 # usage: ./bin/generate-open-api.sh + function dart { rm -rf ../mobile/openapi cd ./templates/mobile/serialization/native @@ -23,11 +24,20 @@ function dart { function dartDio { rm -rf ../mobile-v2/openapi - npx --yes @openapitools/openapi-generator-cli generate -g dart-dio -i ./immich-openapi-specs.json -o ../mobile-v2/openapi --global-property skipFormModel=false --global-property models,apis,supportingFiles,apiTests=false,apiDocs=false,modelTests=false,modelDocs=false + cd ./templates/mobile/serialization/native + wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache + patch --no-backup-if-mismatch -u native_class.mustache > ../mobile-v2/openapi/lib/openapi.dart } function typescript {