diff --git a/Makefile b/Makefile
index 0899d82d24..7c03604dd2 100644
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,9 @@ open-api:
open-api-dart:
cd ./open-api && bash ./bin/generate-open-api.sh dart
+open-api-dart-dio:
+ cd ./open-api && bash ./bin/generate-open-api.sh dart-dio
+
open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
diff --git a/mobile-v2/.gitignore b/mobile-v2/.gitignore
index 4e51a5b910..7846f3208f 100644
--- a/mobile-v2/.gitignore
+++ b/mobile-v2/.gitignore
@@ -2,6 +2,8 @@
*.g.dart
*.gr.dart
*.drift.dart
+*.gen.dart
+openapi/*
# Miscellaneous
*.class
diff --git a/mobile-v2/analysis_options.yaml b/mobile-v2/analysis_options.yaml
index 20b96f7e00..a74bafe91a 100644
--- a/mobile-v2/analysis_options.yaml
+++ b/mobile-v2/analysis_options.yaml
@@ -18,6 +18,5 @@ dart_code_metrics:
- avoid-passing-self-as-argument:
exclude:
- lib/domain/repositories/**
- - prefer-single-widget-per-file:
- ignore-private-widgets: true
+ - prefer-single-widget-per-file: false
- prefer-correct-callback-field-name: false
diff --git a/mobile-v2/android/app/build.gradle b/mobile-v2/android/app/build.gradle
index 3c931bca9d..d355694032 100644
--- a/mobile-v2/android/app/build.gradle
+++ b/mobile-v2/android/app/build.gradle
@@ -24,7 +24,7 @@ if (flutterVersionName == null) {
android {
namespace "com.alextran.immich"
- compileSdk flutter.compileSdkVersion
+ compileSdkVersion 34
ndkVersion flutter.ndkVersion
compileOptions {
@@ -42,8 +42,8 @@ android {
defaultConfig {
applicationId "com.alextran.immich"
- minSdkVersion flutter.minSdkVersion
- targetSdkVersion flutter.targetSdkVersion
+ minSdkVersion 26
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/mobile-v2/android/app/src/main/AndroidManifest.xml b/mobile-v2/android/app/src/main/AndroidManifest.xml
index 51d1691704..bc753bd1f3 100644
--- a/mobile-v2/android/app/src/main/AndroidManifest.xml
+++ b/mobile-v2/android/app/src/main/AndroidManifest.xml
@@ -11,10 +11,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
-
+
+
+
+
+
diff --git a/mobile-v2/assets/i18n/strings.i18n.json b/mobile-v2/assets/i18n/strings.i18n.json
index b61d5cd17d..5534412991 100644
--- a/mobile-v2/assets/i18n/strings.i18n.json
+++ b/mobile-v2/assets/i18n/strings.i18n.json
@@ -4,5 +4,29 @@
"search": "Search",
"sharing": "Sharing",
"library": "Library"
+ },
+ "settings": {
+ "sections": {
+ "about": "About",
+ "general": "General",
+ "advance": "Advance"
+ }
+ },
+ "login": {
+ "error": {
+ "empty_server_url": "Kindly provide a server URL",
+ "invalid_server_url": "Invalid URL",
+ "server_not_reachable": "Server is not reachable"
+ },
+ "label": {
+ "email": "Email",
+ "password": "Password",
+ "endpoint": "Server Endpoint URL",
+ "login_button": "Login",
+ "back_button": "Back",
+ "next_button": "Next",
+ "oauth_button": "OAuth",
+ "login_disabled": "Login Disabled"
+ }
}
}
\ No newline at end of file
diff --git a/mobile-v2/assets/images/immich-logo.png b/mobile-v2/assets/images/immich-logo.png
new file mode 100644
index 0000000000..37c53aac5e
Binary files /dev/null and b/mobile-v2/assets/images/immich-logo.png differ
diff --git a/mobile-v2/assets/images/immich-text-dark.png b/mobile-v2/assets/images/immich-text-dark.png
new file mode 100644
index 0000000000..215687af8f
Binary files /dev/null and b/mobile-v2/assets/images/immich-text-dark.png differ
diff --git a/mobile-v2/assets/images/immich-text-light.png b/mobile-v2/assets/images/immich-text-light.png
new file mode 100644
index 0000000000..478158d39c
Binary files /dev/null and b/mobile-v2/assets/images/immich-text-light.png differ
diff --git a/mobile-v2/ios/Podfile.lock b/mobile-v2/ios/Podfile.lock
index 4e5e25366a..8b2068a35c 100644
--- a/mobile-v2/ios/Podfile.lock
+++ b/mobile-v2/ios/Podfile.lock
@@ -17,7 +17,7 @@ PODS:
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- - sqlite3 (~> 3.45.1)
+ - "sqlite3 (~> 3.45.3+1)"
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
@@ -44,10 +44,10 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
- photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
+ photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a
- sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
+ sqlite3_flutter_libs: 9bfe005308998aeca155330bbc2ea6dddf834a3b
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
diff --git a/mobile-v2/ios/Runner/Info.plist b/mobile-v2/ios/Runner/Info.plist
index 46292be13c..6a253c326a 100644
--- a/mobile-v2/ios/Runner/Info.plist
+++ b/mobile-v2/ios/Runner/Info.plist
@@ -51,5 +51,11 @@
en
+
+ LSApplicationQueriesSchemes
+
+ https
+
+
diff --git a/mobile-v2/lib/domain/entities/album.entity.dart b/mobile-v2/lib/domain/entities/album.entity.dart
new file mode 100644
index 0000000000..64422c172a
--- /dev/null
+++ b/mobile-v2/lib/domain/entities/album.entity.dart
@@ -0,0 +1,11 @@
+import 'package:drift/drift.dart';
+
+class LocalAlbum extends Table {
+ const LocalAlbum();
+
+ IntColumn get id => integer().autoIncrement()();
+ TextColumn get localId => text()();
+ TextColumn get name => text()();
+ DateTimeColumn get modifiedTime =>
+ dateTime().withDefault(currentDateAndTime)();
+}
diff --git a/mobile-v2/lib/domain/entities/asset.entity.dart b/mobile-v2/lib/domain/entities/asset.entity.dart
new file mode 100644
index 0000000000..ba4961d3c6
--- /dev/null
+++ b/mobile-v2/lib/domain/entities/asset.entity.dart
@@ -0,0 +1,19 @@
+import 'package:drift/drift.dart';
+import 'package:immich_mobile/domain/models/asset.model.dart';
+
+class LocalAsset extends Table {
+ const LocalAsset();
+
+ IntColumn get id => integer().autoIncrement()();
+ TextColumn get localId => text()();
+ TextColumn get name => text()();
+ TextColumn get checksum => text()();
+ IntColumn get height => integer()();
+ IntColumn get width => integer()();
+ IntColumn get type => intEnum()();
+ 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))();
+}
diff --git a/mobile-v2/lib/domain/interfaces/store.interface.dart b/mobile-v2/lib/domain/interfaces/store.interface.dart
index e283826151..323c58138a 100644
--- a/mobile-v2/lib/domain/interfaces/store.interface.dart
+++ b/mobile-v2/lib/domain/interfaces/store.interface.dart
@@ -2,14 +2,24 @@ import 'dart:async';
import 'package:immich_mobile/domain/models/store.model.dart';
-abstract class IStoreRepository {
- FutureOr getValue(StoreKey key);
+abstract class IStoreConverter {
+ const IStoreConverter();
- FutureOr setValue(StoreKey key, T value);
+ /// Converts the value T to the primitive type U supported by the Store
+ U toPrimitive(T value);
+
+ /// Converts the value back to T? from the primitive type U from the Store
+ T? fromPrimitive(U value);
+}
+
+abstract class IStoreRepository {
+ FutureOr getValue(StoreKey key);
+
+ FutureOr setValue(StoreKey key, T value);
FutureOr deleteValue(StoreKey key);
- Stream watchValue(StoreKey key);
+ Stream watchValue(StoreKey key);
Stream> watchStore();
diff --git a/mobile-v2/lib/domain/models/album.model.dart b/mobile-v2/lib/domain/models/album.model.dart
new file mode 100644
index 0000000000..d8131e5d4f
--- /dev/null
+++ b/mobile-v2/lib/domain/models/album.model.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/foundation.dart';
+
+@immutable
+class LocalAlbum {
+ final int id;
+ final String localId;
+ final String name;
+ final DateTime modifiedTime;
+
+ const LocalAlbum({
+ required this.id,
+ required this.localId,
+ required this.name,
+ required this.modifiedTime,
+ });
+
+ @override
+ bool operator ==(covariant LocalAlbum other) {
+ if (identical(this, other)) return true;
+
+ return other.hashCode == hashCode;
+ }
+
+ @override
+ int get hashCode {
+ return id.hashCode ^
+ localId.hashCode ^
+ name.hashCode ^
+ modifiedTime.hashCode;
+ }
+}
diff --git a/mobile-v2/lib/domain/models/app_setting.model.dart b/mobile-v2/lib/domain/models/app_setting.model.dart
index 65972f55db..52afd68032 100644
--- a/mobile-v2/lib/domain/models/app_setting.model.dart
+++ b/mobile-v2/lib/domain/models/app_setting.model.dart
@@ -1,10 +1,15 @@
+import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
-enum AppSettings {
- appTheme(StoreKey.appTheme, 10);
+enum AppSetting {
+ appTheme(StoreKey.appTheme, AppTheme.blue),
+ themeMode(StoreKey.themeMode, ThemeMode.system),
+ darkMode(StoreKey.darkMode, false);
- const AppSettings(this.storeKey, this.defaultValue);
+ const AppSetting(this.storeKey, this.defaultValue);
- final StoreKey storeKey;
+ // ignore: avoid-dynamic
+ final StoreKey storeKey;
final T defaultValue;
}
diff --git a/mobile-v2/lib/domain/models/asset.model.dart b/mobile-v2/lib/domain/models/asset.model.dart
new file mode 100644
index 0000000000..211e0ea1cd
--- /dev/null
+++ b/mobile-v2/lib/domain/models/asset.model.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/foundation.dart';
+
+enum AssetType {
+ // do not change this order!
+ other,
+ image,
+ video,
+ audio,
+}
+
+@immutable
+class LocalAsset {
+ final int id;
+ final String localId;
+ final String name;
+ final String checksum;
+ final int height;
+ final int width;
+ final AssetType type;
+ final DateTime createdTime;
+ final DateTime modifiedTime;
+ final int duration;
+ final bool isLivePhoto;
+
+ const LocalAsset({
+ required this.id,
+ required this.localId,
+ required this.name,
+ required this.checksum,
+ required this.height,
+ required this.width,
+ required this.type,
+ required this.createdTime,
+ required this.modifiedTime,
+ required this.duration,
+ 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)';
+ }
+
+ String toJSON() {
+ return """
+{
+ "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 other.hashCode == hashCode;
+ }
+
+ @override
+ int get hashCode {
+ return id.hashCode ^
+ localId.hashCode ^
+ name.hashCode ^
+ checksum.hashCode ^
+ height.hashCode ^
+ width.hashCode ^
+ type.hashCode ^
+ createdTime.hashCode ^
+ modifiedTime.hashCode ^
+ duration.hashCode ^
+ isLivePhoto.hashCode;
+ }
+}
diff --git a/mobile-v2/lib/domain/models/log.model.dart b/mobile-v2/lib/domain/models/log.model.dart
index 2b1a120b5a..d6731ea449 100644
--- a/mobile-v2/lib/domain/models/log.model.dart
+++ b/mobile-v2/lib/domain/models/log.model.dart
@@ -1,7 +1,9 @@
+import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
/// Log levels according to dart logging [Level]
enum LogLevel {
+ // do not change this order!
all,
finest,
finer,
@@ -20,6 +22,7 @@ extension LevelExtension on Level {
LogLevel.info;
}
+@immutable
class LogMessage {
final int id;
final String content;
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
new file mode 100644
index 0000000000..7592ce0098
--- /dev/null
+++ b/mobile-v2/lib/domain/models/server-info/server_config.model.dart
@@ -0,0 +1,34 @@
+import 'package:openapi/openapi.dart';
+
+class ServerConfig {
+ final String? oauthButtonText;
+
+ const ServerConfig({this.oauthButtonText});
+
+ ServerConfig copyWith({String? oauthButtonText}) {
+ return ServerConfig(
+ oauthButtonText: oauthButtonText ?? this.oauthButtonText,
+ );
+ }
+
+ factory ServerConfig.fromDto(ServerConfigDto dto) => ServerConfig(
+ oauthButtonText:
+ dto.oauthButtonText.isEmpty ? null : dto.oauthButtonText,
+ );
+
+ const ServerConfig.reset() : oauthButtonText = null;
+
+ @override
+ String toString() =>
+ 'ServerConfig(oauthButtonText: ${oauthButtonText ?? ''})';
+
+ @override
+ bool operator ==(covariant ServerConfig other) {
+ if (identical(this, other)) return true;
+
+ return other.oauthButtonText == oauthButtonText;
+ }
+
+ @override
+ int get hashCode => oauthButtonText.hashCode;
+}
diff --git a/mobile-v2/lib/domain/models/server-info/server_feature_config.model.dart b/mobile-v2/lib/domain/models/server-info/server_feature_config.model.dart
new file mode 100644
index 0000000000..63d47d1710
--- /dev/null
+++ b/mobile-v2/lib/domain/models/server-info/server_feature_config.model.dart
@@ -0,0 +1,37 @@
+import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
+import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
+
+class ServerFeatureConfig {
+ final ServerFeatures features;
+ final ServerConfig config;
+
+ const ServerFeatureConfig({required this.features, required this.config});
+
+ ServerFeatureConfig copyWith({
+ ServerFeatures? features,
+ ServerConfig? config,
+ }) {
+ return ServerFeatureConfig(
+ features: features ?? this.features,
+ config: config ?? this.config,
+ );
+ }
+
+ const ServerFeatureConfig.reset()
+ : features = const ServerFeatures.reset(),
+ config = const ServerConfig.reset();
+
+ @override
+ String toString() =>
+ 'ServerFeatureConfig(features: $features, config: $config)';
+
+ @override
+ bool operator ==(covariant ServerFeatureConfig other) {
+ if (identical(this, other)) return true;
+
+ return other.features == features && other.config == config;
+ }
+
+ @override
+ int get hashCode => features.hashCode ^ config.hashCode;
+}
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
new file mode 100644
index 0000000000..cccaa3fbe5
--- /dev/null
+++ b/mobile-v2/lib/domain/models/server-info/server_features.model.dart
@@ -0,0 +1,42 @@
+import 'package:openapi/openapi.dart';
+
+class ServerFeatures {
+ final bool hasPasswordLogin;
+ final bool hasOAuthLogin;
+
+ const ServerFeatures({
+ required this.hasPasswordLogin,
+ required this.hasOAuthLogin,
+ });
+
+ ServerFeatures copyWith({bool? hasPasswordLogin, bool? hasOAuthLogin}) {
+ return ServerFeatures(
+ hasPasswordLogin: hasPasswordLogin ?? this.hasPasswordLogin,
+ hasOAuthLogin: hasOAuthLogin ?? this.hasOAuthLogin,
+ );
+ }
+
+ factory ServerFeatures.fromDto(ServerFeaturesDto dto) => ServerFeatures(
+ hasPasswordLogin: dto.passwordLogin,
+ hasOAuthLogin: dto.oauth,
+ );
+
+ const ServerFeatures.reset()
+ : hasPasswordLogin = true,
+ hasOAuthLogin = false;
+
+ @override
+ String toString() =>
+ 'ServerFeatures(hasPasswordLogin: $hasPasswordLogin, hasOAuthLogin: $hasOAuthLogin)';
+
+ @override
+ bool operator ==(covariant ServerFeatures other) {
+ if (identical(this, other)) return true;
+
+ return other.hasPasswordLogin == hasPasswordLogin &&
+ other.hasOAuthLogin == hasOAuthLogin;
+ }
+
+ @override
+ int get hashCode => hasPasswordLogin.hashCode ^ hasOAuthLogin.hashCode;
+}
diff --git a/mobile-v2/lib/domain/models/store.model.dart b/mobile-v2/lib/domain/models/store.model.dart
index 5f456fffec..8807a4326f 100644
--- a/mobile-v2/lib/domain/models/store.model.dart
+++ b/mobile-v2/lib/domain/models/store.model.dart
@@ -1,19 +1,14 @@
-/// Key for each possible value in the `Store`.
-/// Defines the data type for each value
-enum StoreKey {
- appTheme(1000, type: int);
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/domain/interfaces/store.interface.dart';
+import 'package:immich_mobile/domain/utils/store_converters.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
- const StoreKey(this.id, {required this.type});
+@immutable
+class StoreValue {
final int id;
- final Type type;
-}
+ final T? value;
-class StoreValue {
- final int id;
- final int? intValue;
- final String? stringValue;
-
- const StoreValue({required this.id, this.intValue, this.stringValue});
+ const StoreValue({required this.id, this.value});
@override
bool operator ==(covariant StoreValue other) {
@@ -23,45 +18,33 @@ class StoreValue {
}
@override
- int get hashCode => id.hashCode ^ intValue.hashCode ^ stringValue.hashCode;
-
- T? extract(Type type) {
- switch (type) {
- case const (int):
- return intValue as T?;
- case const (bool):
- return intValue == null ? null : (intValue! == 1) as T;
- case const (DateTime):
- return intValue == null
- ? null
- : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
- case const (String):
- return stringValue as T?;
- default:
- throw UnsupportedError("Unknown Store Key type");
- }
- }
-
- static StoreValue of(StoreKey key, T? value) {
- int? i;
- String? s;
-
- switch (key.type) {
- case const (int):
- i = value as int?;
- break;
- case const (bool):
- i = value == null ? null : (value == true ? 1 : 0);
- break;
- case const (DateTime):
- i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
- break;
- case const (String):
- s = value as String?;
- break;
- default:
- throw UnsupportedError("Unknown Store Key type");
- }
- return StoreValue(id: key.id, intValue: i, stringValue: s);
- }
+ int get hashCode => id.hashCode ^ value.hashCode;
+}
+
+/// Key for each possible value in the `Store`.
+/// Also stores the converter to convert the value to and from the store and the type of value stored in the Store
+enum StoreKey {
+ serverEndpoint(
+ 0,
+ converter: StorePrimitiveConverter(),
+ type: String,
+ ),
+ appTheme(
+ 1000,
+ converter: StoreEnumConverter(AppTheme.values),
+ type: int,
+ ),
+ themeMode(
+ 1001,
+ converter: StoreEnumConverter(ThemeMode.values),
+ type: int,
+ ),
+ darkMode(1002, converter: StoreBooleanConverter(), type: int);
+
+ const StoreKey(this.id, {required this.converter, required this.type});
+ final int id;
+
+ /// Type is also stored here easily fetch it during runtime
+ final Type type;
+ final IStoreConverter converter;
}
diff --git a/mobile-v2/lib/domain/repositories/database.repository.dart b/mobile-v2/lib/domain/repositories/database.repository.dart
index 066955a9c4..d0d0edb205 100644
--- a/mobile-v2/lib/domain/repositories/database.repository.dart
+++ b/mobile-v2/lib/domain/repositories/database.repository.dart
@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.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/log.entity.dart';
import 'package:immich_mobile/domain/entities/store.entity.dart';
import 'package:immich_mobile/domain/interfaces/database.interface.dart';
@@ -12,7 +14,7 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'database.repository.drift.dart';
-@DriftDatabase(tables: [Logs, Store])
+@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset])
class DriftDatabaseRepository extends $DriftDatabaseRepository
implements IDatabaseRepository {
DriftDatabaseRepository() : super(_openConnection());
diff --git a/mobile-v2/lib/domain/repositories/store.repository.dart b/mobile-v2/lib/domain/repositories/store.repository.dart
index 1987a2afc9..5a98e7b970 100644
--- a/mobile-v2/lib/domain/repositories/store.repository.dart
+++ b/mobile-v2/lib/domain/repositories/store.repository.dart
@@ -1,66 +1,89 @@
import 'dart:async';
+import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/store.entity.drift.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
+import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
-class StoreDriftRepository implements IStoreRepository {
+class StoreDriftRepository with LogContext implements IStoreRepository {
final DriftDatabaseRepository db;
const StoreDriftRepository(this.db);
@override
- FutureOr getValue(StoreKey key) async {
- final value = await db.managers.store
+ FutureOr getValue(StoreKey key) async {
+ final storeData = await db.managers.store
.filter((s) => s.id.equals(key.id))
.getSingleOrNull();
- return value?.toModel().extract(key.type);
+ return _getValueFromStoreData(key, storeData);
}
@override
- FutureOr setValue(StoreKey key, T value) {
- return db.transaction(() async {
- final storeValue = StoreValue.of(key, value);
- await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert(
- id: Value(storeValue.id),
- intValue: Value(storeValue.intValue),
- stringValue: Value(storeValue.stringValue),
- ));
- });
+ FutureOr setValue(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),
+ ));
+ });
+ return true;
+ } catch (e, s) {
+ log.severe("Cannot set store value - ${key.name}; id - ${key.id}", e, s);
+ return false;
+ }
}
@override
- FutureOr deleteValue(StoreKey key) {
- return db.transaction(() async {
+ FutureOr deleteValue(StoreKey key) async {
+ return await db.transaction(() async {
await db.managers.store.filter((s) => s.id.equals(key.id)).delete();
});
}
@override
Stream> watchStore() {
- return (db.select(db.store).map((s) => s.toModel())).watch();
+ return (db.select(db.store).map((s) {
+ final key = StoreKey.values.firstWhereOrNull((e) => e.id == s.id);
+ if (key != null) {
+ final value = _getValueFromStoreData(key, s);
+ return StoreValue(id: s.id, value: value);
+ }
+ return StoreValue(id: s.id, value: null);
+ })).watch();
}
@override
- Stream watchValue(StoreKey key) {
+ Stream watchValue(StoreKey key) {
return db.managers.store
.filter((s) => s.id.equals(key.id))
.watchSingleOrNull()
- .map((e) => e?.toModel().extract(key.type));
+ .map((e) => _getValueFromStoreData(key, e));
}
@override
- FutureOr clearStore() {
- return db.transaction(() async {
+ FutureOr clearStore() async {
+ return await db.transaction(() async {
await db.managers.store.delete();
});
}
-}
-extension _StoreDataToStoreValue on StoreData {
- StoreValue toModel() {
- return StoreValue(id: id, intValue: intValue, stringValue: stringValue);
+ T? _getValueFromStoreData(StoreKey key, StoreData? data) {
+ final primitive = switch (key.type) {
+ const (int) => data?.intValue,
+ const (String) => data?.stringValue,
+ _ => null,
+ } as U?;
+ if (primitive != null) {
+ return key.converter.fromPrimitive(primitive);
+ }
+ return null;
}
}
diff --git a/mobile-v2/lib/domain/services/app_setting.service.dart b/mobile-v2/lib/domain/services/app_setting.service.dart
index c1e5ba5eff..e73120c1c1 100644
--- a/mobile-v2/lib/domain/services/app_setting.service.dart
+++ b/mobile-v2/lib/domain/services/app_setting.service.dart
@@ -1,22 +1,22 @@
import 'package:immich_mobile/domain/models/app_setting.model.dart';
import 'package:immich_mobile/domain/store_manager.dart';
-class AppSettingsService {
+class AppSettingService {
final StoreManager store;
- const AppSettingsService(this.store);
+ const AppSettingService(this.store);
- T getSetting(AppSettings setting) {
+ T getSetting(AppSetting setting) {
return store.get(setting.storeKey, setting.defaultValue);
}
- void setSetting(AppSettings setting, T value) {
- store.put(setting.storeKey, value);
+ Future setSetting(AppSetting setting, T value) async {
+ return await store.put(setting.storeKey, value);
}
- Stream watchSetting(AppSettings setting) {
+ Stream watchSetting(AppSetting setting) {
return store
- .watch(setting.storeKey)
+ .watch(setting.storeKey)
.map((value) => value ?? setting.defaultValue);
}
}
diff --git a/mobile-v2/lib/domain/services/login.service.dart b/mobile-v2/lib/domain/services/login.service.dart
new file mode 100644
index 0000000000..3d6476603b
--- /dev/null
+++ b/mobile-v2/lib/domain/services/login.service.dart
@@ -0,0 +1,55 @@
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
+import 'package:openapi/openapi.dart';
+
+class LoginService with LogContext {
+ const LoginService();
+
+ Future isEndpointAvailable(Uri uri, {Dio? dio}) async {
+ String baseUrl = uri.toString();
+
+ if (!baseUrl.endsWith('/api')) {
+ baseUrl += '/api';
+ }
+
+ final serverAPI =
+ Openapi(dio: dio, basePathOverride: baseUrl, interceptors: [])
+ .getServerInfoApi();
+ try {
+ await serverAPI.pingServer(validateStatus: (status) => status == 200);
+ } catch (e) {
+ log.severe("Exception occured while validating endpoint", e);
+ return false;
+ }
+ return true;
+ }
+
+ Future resolveEndpoint(Uri uri, {Dio? dio}) async {
+ final d = dio ?? Dio();
+ String baseUrl = uri.toString();
+
+ 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,
+ ),
+ );
+
+ final data = jsonDecode(res.data);
+ final endpoint = data['api']['endpoint'].toString();
+
+ // 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);
+ }
+
+ // No well-known, return the baseUrl
+ return baseUrl;
+ }
+}
diff --git a/mobile-v2/lib/domain/services/server_info.service.dart b/mobile-v2/lib/domain/services/server_info.service.dart
new file mode 100644
index 0000000000..bf32191aee
--- /dev/null
+++ b/mobile-v2/lib/domain/services/server_info.service.dart
@@ -0,0 +1,38 @@
+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/mixins/log_context.mixin.dart';
+import 'package:openapi/openapi.dart';
+
+class ServerInfoService with LogContext {
+ final Openapi _api;
+
+ ServerInfoApi get _serverInfo => _api.getServerInfoApi();
+
+ ServerInfoService(this._api);
+
+ Future getServerFeatures() async {
+ try {
+ final response = await _serverInfo.getServerFeatures();
+ final dto = response.data;
+ if (dto != null) {
+ return ServerFeatures.fromDto(dto);
+ }
+ } catch (e, s) {
+ log.severe("Error while fetching server features", e, s);
+ }
+ return null;
+ }
+
+ Future getServerConfig() async {
+ try {
+ final response = await _serverInfo.getServerConfig();
+ final dto = response.data;
+ if (dto != null) {
+ return ServerConfig.fromDto(dto);
+ }
+ } catch (e, s) {
+ log.severe("Error while fetching server config", e, s);
+ }
+ return null;
+ }
+}
diff --git a/mobile-v2/lib/domain/store_manager.dart b/mobile-v2/lib/domain/store_manager.dart
index 3960be6198..1671623737 100644
--- a/mobile-v2/lib/domain/store_manager.dart
+++ b/mobile-v2/lib/domain/store_manager.dart
@@ -1,8 +1,8 @@
import 'dart:async';
-import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
+import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
class StoreKeyNotFoundException implements Exception {
@@ -13,39 +13,32 @@ class StoreKeyNotFoundException implements Exception {
String toString() => "Key '${key.name}' not found in Store";
}
-/// Key-value store for individual items enumerated in StoreKey.
-/// Supports String, int and JSON-serializable Objects
-/// Can be used concurrently from multiple isolates
+/// Key-value cache for individual items enumerated in StoreKey.
class StoreManager with LogContext {
late final IStoreRepository _db;
- // This cannot be final or else dart would bite when we access the field in the factory method
- StreamSubscription? _subscription;
+ late final StreamSubscription _subscription;
final Map _cache = {};
- StoreManager._internal();
- static final StoreManager _instance = StoreManager._internal();
-
- factory StoreManager(IStoreRepository db) {
- if (_instance._subscription == null) {
- _instance._db = db;
- _instance._populateCache();
- _instance._subscription =
- _instance._db.watchStore().listen(_instance._onChangeListener);
- }
- return _instance;
+ StoreManager(IStoreRepository db) {
+ _db = db;
+ _subscription = _db.watchStore().listen(_onChangeListener);
+ _populateCache();
}
void dispose() {
- _subscription?.cancel();
+ _subscription.cancel();
}
FutureOr _populateCache() async {
for (StoreKey key in StoreKey.values) {
- final StoreValue? value = await _db.getValue(key);
+ final value = await _db.getValue(key);
if (value != null) {
_cache[key.id] = value;
}
}
+
+ /// Signal ready once the cache is populated
+ di.signalReady(this);
}
/// clears all values from this store (cache and DB), only for testing!
@@ -55,11 +48,11 @@ class StoreManager with LogContext {
}
/// Returns the stored value for the given key (possibly null)
- T? tryGet(StoreKey key) => _cache[key.id] as T?;
+ T? tryGet(StoreKey key) => _cache[key.id] as T?;
/// Returns the stored value for the given key or if null the [defaultValue]
/// Throws a [StoreKeyNotFoundException] if both are null
- T get(StoreKey key, [T? defaultValue]) {
+ T get(StoreKey key, [T? defaultValue]) {
final value = _cache[key.id] ?? defaultValue;
if (value == null) {
throw StoreKeyNotFoundException(key);
@@ -68,17 +61,17 @@ class StoreManager with LogContext {
}
/// Watches a specific key for changes
- Stream watch(StoreKey key) => _db.watchValue(key);
+ Stream watch(StoreKey key) => _db.watchValue(key);
/// Stores the value synchronously in the cache and asynchronously in the DB
- FutureOr put(StoreKey key, T value) async {
- if (_cache[key.id] == value) return Future.value();
+ FutureOr put(StoreKey key, T value) async {
+ if (_cache[key.id] == value) return Future.value(true);
_cache[key.id] = value;
return await _db.setValue(key, value);
}
/// Removes the value synchronously from the cache and asynchronously from the DB
- Future delete(StoreKey key) async {
+ Future delete(StoreKey key) async {
if (_cache[key.id] == null) return Future.value();
_cache.remove(key.id);
return await _db.deleteValue(key);
@@ -87,12 +80,9 @@ class StoreManager with LogContext {
/// Updates the state in cache if a value is updated in any isolate
void _onChangeListener(List? data) {
if (data != null) {
- for (StoreValue value in data) {
- final key = StoreKey.values.firstWhereOrNull((e) => e.id == value.id);
- if (key != null) {
- _cache[value.id] = value.extract(key.type);
- } else {
- log.warning("No key available for value Id - ${value.id}");
+ for (StoreValue storeValue in data) {
+ if (storeValue.value != null) {
+ _cache[storeValue.id] = storeValue.value;
}
}
}
diff --git a/mobile-v2/lib/domain/utils/store_converters.dart b/mobile-v2/lib/domain/utils/store_converters.dart
new file mode 100644
index 0000000000..29d151553e
--- /dev/null
+++ b/mobile-v2/lib/domain/utils/store_converters.dart
@@ -0,0 +1,33 @@
+import 'package:immich_mobile/domain/interfaces/store.interface.dart';
+
+class StoreEnumConverter extends IStoreConverter {
+ const StoreEnumConverter(this.values);
+
+ final List values;
+
+ @override
+ T? fromPrimitive(int value) => values.elementAtOrNull(value);
+
+ @override
+ int toPrimitive(T value) => value.index;
+}
+
+class StoreBooleanConverter extends IStoreConverter {
+ const StoreBooleanConverter();
+
+ @override
+ bool fromPrimitive(int value) => value != 0;
+
+ @override
+ int toPrimitive(bool value) => value ? 1 : 0;
+}
+
+class StorePrimitiveConverter extends IStoreConverter {
+ const StorePrimitiveConverter();
+
+ @override
+ T fromPrimitive(T value) => value;
+
+ @override
+ T toPrimitive(T value) => value;
+}
diff --git a/mobile-v2/lib/immich_app.dart b/mobile-v2/lib/immich_app.dart
index 14be0e8790..d207cacb02 100644
--- a/mobile-v2/lib/immich_app.dart
+++ b/mobile-v2/lib/immich_app.dart
@@ -1,18 +1,16 @@
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
+import 'package:immich_mobile/presentation/modules/theme/states/app_theme.state.dart';
+import 'package:immich_mobile/presentation/modules/theme/widgets/app_theme_builder.widget.dart';
import 'package:immich_mobile/presentation/router/router.dart';
-import 'package:watch_it/watch_it.dart';
+import 'package:immich_mobile/service_locator.dart';
+import 'package:immich_mobile/utils/constants/globals.dart';
class ImmichApp extends StatefulWidget {
- final ThemeData lightTheme;
- final ThemeData darkTheme;
-
- const ImmichApp({
- required this.lightTheme,
- required this.darkTheme,
- super.key,
- });
+ const ImmichApp({super.key});
@override
State createState() => _ImmichAppState();
@@ -21,15 +19,23 @@ class ImmichApp extends StatefulWidget {
class _ImmichAppState extends State with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
- final router = di();
-
- return MaterialApp.router(
- locale: TranslationProvider.of(context).flutterLocale,
- supportedLocales: AppLocaleUtils.supportedLocales,
- localizationsDelegates: GlobalMaterialLocalizations.delegates,
- theme: widget.lightTheme,
- darkTheme: widget.darkTheme,
- routerConfig: router.config(),
+ return TranslationProvider(
+ child: BlocBuilder(
+ bloc: di(),
+ builder: (_, appTheme) => AppThemeBuilder(
+ theme: appTheme,
+ builder: (ctx, lightTheme, darkTheme) => MaterialApp.router(
+ debugShowCheckedModeBanner: false,
+ locale: TranslationProvider.of(ctx).flutterLocale,
+ supportedLocales: AppLocaleUtils.supportedLocales,
+ localizationsDelegates: GlobalMaterialLocalizations.delegates,
+ theme: lightTheme,
+ darkTheme: darkTheme,
+ routerConfig: di().config(),
+ scaffoldMessengerKey: kScafMessengerKey,
+ ),
+ ),
+ ),
);
}
}
diff --git a/mobile-v2/lib/main.dart b/mobile-v2/lib/main.dart
index 65c7a2474a..886d686945 100644
--- a/mobile-v2/lib/main.dart
+++ b/mobile-v2/lib/main.dart
@@ -1,34 +1,14 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/i18n/strings.g.dart';
import 'package:immich_mobile/immich_app.dart';
-import 'package:immich_mobile/presentation/theme/states/app_theme.state.dart';
-import 'package:immich_mobile/presentation/theme/widgets/app_theme_builder.dart';
import 'package:immich_mobile/service_locator.dart';
-import 'package:watch_it/watch_it.dart';
void main() {
- // Ensure the bindings are initialized
WidgetsFlutterBinding.ensureInitialized();
// DI Injection
ServiceLocator.configureServices();
// Init localization
LocaleSettings.useDeviceLocale();
- runApp(const MainWidget());
-}
-
-class MainWidget extends StatelessWidget with WatchItMixin {
- const MainWidget({super.key});
-
- @override
- Widget build(BuildContext context) {
- final appTheme = watchIt().value;
-
- return TranslationProvider(
- child: AppThemeBuilder(
- theme: appTheme,
- builder: (lightTheme, darkTheme) =>
- ImmichApp(lightTheme: lightTheme, darkTheme: darkTheme),
- ),
- );
- }
+
+ runApp(const ImmichApp());
}
diff --git a/mobile-v2/lib/presentation/components/common/gap.widget.dart b/mobile-v2/lib/presentation/components/common/gap.widget.dart
new file mode 100644
index 0000000000..8bc3755ce1
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/common/gap.widget.dart
@@ -0,0 +1,19 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/utils/constants/size_constants.dart';
+
+@immutable
+class SizedGap extends SizedBox {
+ const SizedGap({super.key, super.height, super.width});
+
+ // Widgets to be used in Column
+ const SizedGap.sh({super.key}) : super(height: SizeConstants.s);
+ const SizedGap.mh({super.key}) : super(height: SizeConstants.m);
+ const SizedGap.lh({super.key}) : super(height: SizeConstants.l);
+ const SizedGap.xlh({super.key}) : super(height: SizeConstants.xl);
+
+ // Widgets to be used in Row
+ const SizedGap.sw({super.key}) : super(width: SizeConstants.s);
+ const SizedGap.mw({super.key}) : super(width: SizeConstants.m);
+ const SizedGap.lw({super.key}) : super(width: SizeConstants.l);
+ const SizedGap.xlw({super.key}) : super(width: SizeConstants.xl);
+}
diff --git a/mobile-v2/lib/presentation/components/common/loading_indaticator.widget.dart b/mobile-v2/lib/presentation/components/common/loading_indaticator.widget.dart
new file mode 100644
index 0000000000..b1c2756b20
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/common/loading_indaticator.widget.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+
+class ImLoadingIndicator extends StatelessWidget {
+ const ImLoadingIndicator({super.key, this.dimension, this.strokeWidth});
+
+ /// The size of the indicator with a default of 24
+ final double? dimension;
+
+ /// The width of the indicator with a default of 2
+ final double? strokeWidth;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: dimension ?? 24,
+ height: dimension ?? 24,
+ child: FittedBox(
+ child: CircularProgressIndicator(strokeWidth: strokeWidth ?? 2),
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/image/immich_logo.widget.dart b/mobile-v2/lib/presentation/components/image/immich_logo.widget.dart
new file mode 100644
index 0000000000..37e3675652
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/image/immich_logo.widget.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/widgets.dart';
+import 'package:immich_mobile/utils/constants/assets.gen.dart';
+import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
+
+class ImLogo extends StatelessWidget {
+ const ImLogo({
+ this.width,
+ this.filterQuality = FilterQuality.high,
+ super.key,
+ });
+
+ /// The width of the image.
+ final double? width;
+
+ /// The rendering quality
+ final FilterQuality filterQuality;
+
+ @override
+ Widget build(BuildContext context) {
+ return Image(
+ width: width,
+ filterQuality: filterQuality,
+ semanticLabel: 'Immich Logo',
+ image: Assets.images.immichLogo.provider(),
+ isAntiAlias: true,
+ );
+ }
+}
+
+class ImLogoText extends StatelessWidget {
+ const ImLogoText({
+ super.key,
+ this.fontSize = 48,
+ this.filterQuality = FilterQuality.high,
+ });
+
+ final double fontSize;
+
+ /// The rendering quality
+ final FilterQuality filterQuality;
+
+ @override
+ Widget build(BuildContext context) {
+ return Image(
+ semanticLabel: 'Immich Logo Text',
+ image: (context.isDarkTheme
+ ? Assets.images.immichTextDark.provider
+ : Assets.images.immichTextLight.provider)(),
+ width: fontSize * 4,
+ filterQuality: FilterQuality.high,
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/input/filled_button.widget.dart b/mobile-v2/lib/presentation/components/input/filled_button.widget.dart
new file mode 100644
index 0000000000..a36ee08e18
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/input/filled_button.widget.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+
+class ImFilledButton extends StatelessWidget {
+ const ImFilledButton({
+ super.key,
+ this.icon,
+ this.onPressed,
+ this.isDisabled = false,
+ required this.label,
+ }) : _tonal = false;
+
+ const ImFilledButton.tonal({
+ super.key,
+ this.icon,
+ this.onPressed,
+ this.isDisabled = false,
+ required this.label,
+ }) : _tonal = true;
+
+ /// Internal flag to switch between filled and tonal variant
+ final bool _tonal;
+
+ /// Should disable the button
+ final bool isDisabled;
+
+ /// Icon to display if [withIcon] is true
+ final IconData? icon;
+
+ /// Action to perform on Button press
+ final VoidCallback? onPressed;
+
+ /// Label to be displayed in the button
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ if (_tonal) {
+ if (icon != null) {
+ return FilledButton.tonalIcon(
+ onPressed: isDisabled ? null : onPressed,
+ icon: Icon(icon),
+ label: Text(label),
+ );
+ }
+
+ return FilledButton.tonal(
+ onPressed: isDisabled ? null : onPressed,
+ child: Text(label),
+ );
+ }
+
+ if (icon != null) {
+ return FilledButton.icon(
+ onPressed: isDisabled ? null : onPressed,
+ icon: Icon(icon),
+ label: Text(label),
+ );
+ }
+
+ return FilledButton(
+ onPressed: isDisabled ? null : onPressed,
+ child: Text(label),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/input/password_form_field.widget.dart b/mobile-v2/lib/presentation/components/input/password_form_field.widget.dart
new file mode 100644
index 0000000000..ca32c9c3d4
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/input/password_form_field.widget.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/components/input/text_form_field.widget.dart';
+import 'package:material_symbols_icons/symbols.dart';
+
+class ImPasswordFormField extends StatefulWidget {
+ const ImPasswordFormField({
+ super.key,
+ this.controller,
+ this.onChanged,
+ this.focusNode,
+ this.label,
+ this.hint,
+ this.textInputAction,
+ this.isDisabled = false,
+ });
+
+ /// The [TextEditingController] passed to the underlying [TextFormField]
+ final TextEditingController? controller;
+
+ /// Optional callback to receive changes
+ final void Function(String?)? onChanged;
+
+ /// The [FocusNode] passed to the underlying [TextFormField]
+ final FocusNode? focusNode;
+
+ /// Translation Key used as label
+ final String? label;
+
+ /// Translation key used as hint
+ final String? hint;
+
+ /// Type of the following action - go, next, enter, etc.
+ final TextInputAction? textInputAction;
+
+ /// Flag to disable the [TextFormField]
+ final bool isDisabled;
+
+ @override
+ State createState() => _ImPasswordFormFieldState();
+}
+
+class _ImPasswordFormFieldState extends State {
+ final showPassword = ValueNotifier(false);
+
+ @override
+ void dispose() {
+ showPassword.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder(
+ valueListenable: showPassword,
+ builder: (_, showPass, child) => ImTextFormField(
+ controller: widget.controller,
+ onChanged: widget.onChanged,
+ shouldObscure: !showPass,
+ hint: widget.hint,
+ label: widget.label,
+ focusNode: widget.focusNode,
+ suffixIcon: IconButton(
+ onPressed: () => showPassword.value = !showPassword.value,
+ icon: Icon(
+ showPassword.value
+ ? Symbols.visibility_off_rounded
+ : Symbols.visibility_rounded,
+ ),
+ ),
+ autoFillHints: const [AutofillHints.password],
+ keyboardType: TextInputType.visiblePassword,
+ textInputAction: widget.textInputAction,
+ isDisabled: widget.isDisabled,
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/input/switch_list.widget.dart b/mobile-v2/lib/presentation/components/input/switch_list.widget.dart
new file mode 100644
index 0000000000..11380d7b8e
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/input/switch_list.widget.dart
@@ -0,0 +1,62 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/domain/models/app_setting.model.dart';
+import 'package:immich_mobile/domain/services/app_setting.service.dart';
+import 'package:immich_mobile/service_locator.dart';
+
+class ImSwitchListTile extends StatefulWidget {
+ const ImSwitchListTile(
+ this.setting, {
+ super.key,
+ this.fromAppSetting,
+ this.toAppSetting,
+ }) : assert(T == bool || (fromAppSetting != null && toAppSetting != null),
+ "Setting is not a boolean and a from / to App setting is not provided");
+
+ final AppSetting setting;
+
+ /// Converts the type T to a boolean to use in a switch
+ final bool Function(T value)? fromAppSetting;
+
+ /// Converts the boolean back to the type T to be stored in the app setting. Return null to not update the DB but to
+ /// retain the previous value
+ final T? Function(bool state)? toAppSetting;
+
+ @override
+ State createState() => _ImSwitchListTileState();
+}
+
+class _ImSwitchListTileState extends State> {
+ // Actual switch list state
+ late bool isEnabled;
+ final AppSettingService _appSettingService = di();
+
+ Future set(bool enabled) async {
+ if (isEnabled == enabled) return;
+
+ final value = T != bool ? widget.toAppSetting!(enabled) : enabled as T;
+ if (value != null &&
+ await _appSettingService.setSetting(widget.setting, value) &&
+ context.mounted) {
+ setState(() {
+ isEnabled = enabled;
+ });
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ final value = _appSettingService.getSetting(widget.setting);
+ isEnabled = T != bool ? widget.fromAppSetting!(value) : value as bool;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SwitchListTile(
+ value: isEnabled,
+ onChanged: (value) => set(value),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/input/text_button.widget.dart b/mobile-v2/lib/presentation/components/input/text_button.widget.dart
new file mode 100644
index 0000000000..6206fe50cb
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/input/text_button.widget.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+
+class ImTextButton extends StatelessWidget {
+ const ImTextButton({
+ super.key,
+ this.icon,
+ this.onPressed,
+ this.isDisabled = false,
+ required this.label,
+ });
+
+ /// Icon to display if [withIcon] is true
+ final IconData? icon;
+
+ /// Flag to disable the button
+ final bool isDisabled;
+
+ /// Action to perform on Button press
+ final VoidCallback? onPressed;
+
+ /// Label to be displayed in the button
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ if (icon != null) {
+ return TextButton.icon(
+ onPressed: isDisabled ? null : onPressed,
+ icon: Icon(icon),
+ label: Text(label),
+ );
+ }
+
+ return TextButton(onPressed: onPressed, child: Text(label));
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/input/text_form_field.widget.dart b/mobile-v2/lib/presentation/components/input/text_form_field.widget.dart
new file mode 100644
index 0000000000..039ad08015
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/input/text_form_field.widget.dart
@@ -0,0 +1,86 @@
+import 'package:flutter/material.dart';
+
+class ImTextFormField extends StatelessWidget {
+ const ImTextFormField({
+ super.key,
+ this.controller,
+ this.focusNode,
+ this.onChanged,
+ this.validator,
+ this.shouldObscure = false,
+ this.suffixIcon,
+ this.label,
+ this.hint,
+ this.autoFillHints,
+ this.keyboardType,
+ this.textInputAction,
+ this.isDisabled = false,
+ this.onSubmitted,
+ }) : assert(
+ onSubmitted == null ||
+ textInputAction == TextInputAction.next ||
+ textInputAction == TextInputAction.previous,
+ "onSubmitted provided when textInputAction is not next or pervious",
+ );
+
+ /// The [TextEditingController] passed to the underlying [TextFormField]
+ final TextEditingController? controller;
+
+ /// The [FocusNode] passed to the underlying [TextFormField]
+ final FocusNode? focusNode;
+
+ /// Optional callback to validate input
+ final String? Function(String?)? validator;
+
+ /// Optional callback to receive changes
+ final void Function(String?)? onChanged;
+
+ /// Optional flag to obscure texts
+ final bool shouldObscure;
+
+ /// Icon Widget to display in the suffix
+ final Widget? suffixIcon;
+
+ /// Translation Key used as label
+ final String? label;
+
+ /// Translation key used as hint
+ final String? hint;
+
+ /// Hints used by the auto-fill service
+ final List? autoFillHints;
+
+ /// Type of keyboard - Numberic / Alphanum
+ final TextInputType? keyboardType;
+
+ /// Type of the following action - go, next, enter, etc.
+ final TextInputAction? textInputAction;
+
+ /// Flag to disable the [TextFormField]
+ final bool isDisabled;
+
+ /// Called on [TextInputAction.next] or [TextInputAction.previous]
+ final void Function(String)? onSubmitted;
+
+ @override
+ Widget build(BuildContext context) {
+ return TextFormField(
+ controller: controller,
+ onChanged: onChanged,
+ focusNode: focusNode,
+ obscureText: shouldObscure,
+ validator: validator,
+ decoration: InputDecoration(
+ labelText: label,
+ hintText: hint,
+ suffixIcon: suffixIcon,
+ ),
+ autofillHints: autoFillHints,
+ keyboardType: keyboardType,
+ textInputAction: textInputAction,
+ readOnly: isDisabled,
+ onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
+ onFieldSubmitted: onSubmitted,
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/scaffold/adaptive_route_primary_appbar.widget.dart b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_primary_appbar.widget.dart
new file mode 100644
index 0000000000..1629168a7e
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_primary_appbar.widget.dart
@@ -0,0 +1,17 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+class ImAdaptiveRoutePrimaryAppBar extends StatelessWidget
+ implements PreferredSizeWidget {
+ const ImAdaptiveRoutePrimaryAppBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBar(
+ leading: BackButton(onPressed: () => context.router.root.maybePop()),
+ );
+ }
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+}
diff --git a/mobile-v2/lib/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart
new file mode 100644
index 0000000000..ccb347a7eb
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart
@@ -0,0 +1,20 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
+
+class ImAdaptiveRouteSecondaryAppBar extends StatelessWidget
+ implements PreferredSizeWidget {
+ const ImAdaptiveRouteSecondaryAppBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBar(
+ leading: context.isTablet
+ ? CloseButton(onPressed: () => context.maybePop())
+ : BackButton(onPressed: () => context.maybePop()),
+ );
+ }
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+}
diff --git a/mobile-v2/lib/presentation/components/scaffold/adaptive_route_wrapper.widget.dart b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_wrapper.widget.dart
new file mode 100644
index 0000000000..2c594cdaf0
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/scaffold/adaptive_route_wrapper.widget.dart
@@ -0,0 +1,37 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_scaffold_body.widget.dart';
+import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
+
+class ImAdaptiveRouteWrapper extends StatelessWidget {
+ const ImAdaptiveRouteWrapper({
+ super.key,
+ required this.primaryRoute,
+ required this.primaryBody,
+ this.bodyRatio,
+ });
+
+ /// Builder to build the primary body
+ final Widget Function(BuildContext?) primaryBody;
+
+ /// Primary route name to not render it twice in landscape
+ final String primaryRoute;
+
+ /// Ratio of primaryBody:secondaryBody
+ final double? bodyRatio;
+
+ @override
+ Widget build(BuildContext context) {
+ return AutoRouter(builder: (ctx, child) {
+ if (ctx.isTablet) {
+ return ImAdaptiveScaffoldBody(
+ primaryBody: primaryBody,
+ secondaryBody:
+ ctx.topRoute.name != primaryRoute ? (_) => child : null,
+ bodyRatio: bodyRatio,
+ );
+ }
+ return ImAdaptiveScaffoldBody(primaryBody: (_) => child);
+ });
+ }
+}
diff --git a/mobile-v2/lib/presentation/components/scaffold/adaptive_scaffold_body.widget.dart b/mobile-v2/lib/presentation/components/scaffold/adaptive_scaffold_body.widget.dart
new file mode 100644
index 0000000000..0f874f94ad
--- /dev/null
+++ b/mobile-v2/lib/presentation/components/scaffold/adaptive_scaffold_body.widget.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+
+class ImAdaptiveScaffoldBody extends StatelessWidget {
+ const ImAdaptiveScaffoldBody({
+ super.key,
+ required this.primaryBody,
+ this.secondaryBody,
+ this.bodyRatio,
+ });
+
+ /// Builder to build the primary body
+ final Widget Function(BuildContext?) primaryBody;
+
+ /// Builder to build the secondary body
+ final Widget Function(BuildContext?)? secondaryBody;
+
+ /// Ratio of primaryBody:secondaryBody
+ final double? bodyRatio;
+
+ @override
+ Widget build(BuildContext context) {
+ return AdaptiveLayout(
+ internalAnimations: false,
+ transitionDuration: const Duration(milliseconds: 300),
+ bodyRatio: bodyRatio,
+ body: SlotLayout(
+ config: {
+ Breakpoints.standard: SlotLayout.from(
+ key: const Key('ImAdaptiveScaffold Body Standard'),
+ builder: primaryBody,
+ ),
+ },
+ ),
+ secondaryBody: SlotLayout(
+ config: {
+ /// No secondary body in mobile layouts
+ Breakpoints.small: SlotLayoutConfig.empty(),
+ Breakpoints.mediumAndUp: SlotLayout.from(
+ key: const Key('ImAdaptiveScaffold Secondary Body Medium'),
+ builder: secondaryBody,
+ ),
+ },
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/common/states/server_info/server_feature_config.state.dart b/mobile-v2/lib/presentation/modules/common/states/server_info/server_feature_config.state.dart
new file mode 100644
index 0000000000..a4c595872c
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/common/states/server_info/server_feature_config.state.dart
@@ -0,0 +1,27 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/domain/models/server-info/server_feature_config.model.dart';
+import 'package:immich_mobile/domain/services/server_info.service.dart';
+
+class ServerFeatureConfigCubit extends Cubit {
+ final ServerInfoService _serverInfoService;
+
+ ServerFeatureConfigCubit(this._serverInfoService)
+ : super(const ServerFeatureConfig.reset());
+
+ Future getFeatures() async =>
+ await Future.wait([_getFeatures(), _getConfig()]);
+
+ Future _getFeatures() async {
+ final features = await _serverInfoService.getServerFeatures();
+ if (features != null) {
+ emit(state.copyWith(features: features));
+ }
+ }
+
+ Future _getConfig() async {
+ final config = await _serverInfoService.getServerConfig();
+ if (config != null) {
+ emit(state.copyWith(config: config));
+ }
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart
index 687031f170..df15cd414d 100644
--- a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart
+++ b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart
@@ -1,5 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
@RoutePage()
class HomePage extends StatelessWidget {
@@ -7,6 +8,11 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container();
+ return Center(
+ child: ElevatedButton(
+ onPressed: () => context.router.navigate(const SettingsRoute()),
+ child: const Text('Settings'),
+ ),
+ );
}
}
diff --git a/mobile-v2/lib/presentation/modules/login/models/login_page.model.dart b/mobile-v2/lib/presentation/modules/login/models/login_page.model.dart
new file mode 100644
index 0000000000..67f7ba94a6
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/login/models/login_page.model.dart
@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+
+@immutable
+class LoginPageState {
+ final bool isServerValidated;
+ final bool isValidationInProgress;
+
+ const LoginPageState({
+ required this.isServerValidated,
+ required this.isValidationInProgress,
+ });
+
+ factory LoginPageState.reset() {
+ return const LoginPageState(
+ isServerValidated: false,
+ isValidationInProgress: false,
+ );
+ }
+
+ LoginPageState copyWith({
+ bool? isServerValidated,
+ bool? isValidationInProgress,
+ }) {
+ return LoginPageState(
+ isServerValidated: isServerValidated ?? this.isServerValidated,
+ isValidationInProgress:
+ isValidationInProgress ?? this.isValidationInProgress,
+ );
+ }
+
+ @override
+ String toString() =>
+ 'LoginPageState(isServerValidated: $isServerValidated, isValidationInProgress: $isValidationInProgress)';
+
+ @override
+ bool operator ==(covariant LoginPageState other) {
+ if (identical(this, other)) return true;
+
+ return other.isServerValidated == isServerValidated &&
+ other.isValidationInProgress == isValidationInProgress;
+ }
+
+ @override
+ int get hashCode =>
+ isServerValidated.hashCode ^ isValidationInProgress.hashCode;
+}
diff --git a/mobile-v2/lib/presentation/modules/login/pages/login.page.dart b/mobile-v2/lib/presentation/modules/login/pages/login.page.dart
new file mode 100644
index 0000000000..1fd23f7891
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/login/pages/login.page.dart
@@ -0,0 +1,187 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
+import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_scaffold_body.widget.dart';
+import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
+import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
+import 'package:immich_mobile/presentation/modules/login/widgets/login_form.widget.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:immich_mobile/utils/constants/size_constants.dart';
+import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+@RoutePage()
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _animationController;
+ final TextEditingController _serverUrlController = TextEditingController();
+ final TextEditingController _emailController = TextEditingController();
+ final TextEditingController _passwordController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ _animationController = AnimationController(
+ duration: const Duration(seconds: 60),
+ vsync: this,
+ )..repeat();
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ _serverUrlController.dispose();
+ _emailController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ void _populateDemoCredentials() {
+ _serverUrlController.text = 'https://demo.immich.app';
+ _emailController.text = 'demo@immich.app';
+ _passwordController.text = 'demo';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final PreferredSizeWidget? appBar;
+ late final Widget primaryBody;
+ late final Widget secondaryBody;
+
+ Widget rotatingLogo = GestureDetector(
+ onDoubleTap: _populateDemoCredentials,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ RotationTransition(
+ turns: _animationController,
+ child: const ImLogo(width: 100),
+ ),
+ const SizedGap.lh(),
+ const ImLogoText(),
+ ],
+ ),
+ ),
+ );
+
+ final Widget form = FractionallySizedBox(
+ widthFactor: 0.8,
+ child: LoginForm(
+ serverUrlController: _serverUrlController,
+ emailController: _emailController,
+ passwordController: _passwordController,
+ ),
+ );
+
+ final Widget bottom = Padding(
+ padding: const EdgeInsets.only(bottom: SizeConstants.s),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FutureBuilder(
+ future: PackageInfo.fromPlatform(),
+ builder: (_, snap) => DefaultTextStyle.merge(
+ style: TextStyle(color: context.theme.colorScheme.outline),
+ child: Text(snap.data?.version ?? ''),
+ ),
+ ),
+ TextButton(
+ onPressed: () => context.navigateRoot(const LogsRoute()),
+ child: const Text('Logs'),
+ ),
+ ],
+ ),
+ );
+
+ final serverUrl = BlocSelector(
+ selector: (state) => state.isServerValidated,
+ builder: (_, isValidated) => isValidated
+ ? Padding(
+ padding: const EdgeInsets.only(bottom: SizeConstants.m),
+ child: DefaultTextStyle.merge(
+ style: TextStyle(
+ color: context.theme.primaryColor,
+ fontWeight: FontWeight.w500,
+ ),
+ child: InkWell(
+ onTap: () => launchUrl(Uri.parse(_serverUrlController.text)),
+ child: Text(
+ _serverUrlController.text,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ )
+ : const SizedBox.shrink(),
+ );
+
+ const PreferredSizeWidget topBar = _MobileAppBar();
+
+ if (context.isTablet) {
+ appBar = null;
+ primaryBody = Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [rotatingLogo, const SizedGap.mh(), serverUrl],
+ ),
+ );
+ secondaryBody = Column(
+ children: [topBar, Expanded(child: Center(child: form)), bottom],
+ );
+ } else {
+ appBar = topBar;
+ primaryBody = Center(
+ child: Column(children: [
+ Expanded(child: rotatingLogo),
+ serverUrl,
+ Expanded(flex: 2, child: form),
+ bottom,
+ ]),
+ );
+ }
+
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ appBar: appBar,
+ body: SafeArea(
+ child: ImAdaptiveScaffoldBody(
+ primaryBody: (_) => primaryBody,
+ secondaryBody: (_) => secondaryBody,
+ ),
+ ),
+ );
+ }
+}
+
+class _MobileAppBar extends StatelessWidget implements PreferredSizeWidget {
+ const _MobileAppBar();
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBar(
+ automaticallyImplyLeading: false,
+ scrolledUnderElevation: 0.0,
+ actions: [
+ IconButton(
+ onPressed: () => context.navigateRoot(const SettingsRoute()),
+ icon: const Icon(Symbols.settings),
+ ),
+ ],
+ );
+ }
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+}
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
new file mode 100644
index 0000000000..cf5f324313
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart
@@ -0,0 +1,93 @@
+import 'dart:async';
+
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/domain/models/store.model.dart';
+import 'package:immich_mobile/domain/services/login.service.dart';
+import 'package:immich_mobile/domain/store_manager.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/login/models/login_page.model.dart';
+import 'package:immich_mobile/service_locator.dart';
+import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
+import 'package:immich_mobile/utils/snackbar_manager.dart';
+
+class LoginPageCubit extends Cubit with LogContext {
+ LoginPageCubit() : super(LoginPageState.reset());
+
+ String _appendSchema(String url) {
+ // Add schema if none is set
+ url =
+ url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url";
+ // Remove trailing slash(es)
+ url = url.trimRight().replaceFirst(RegExp(r"/+$"), "");
+ return url;
+ }
+
+ String? validateServerUrl(String? url) {
+ if (url == null || url.isEmpty) {
+ return t.login.error.empty_server_url;
+ }
+
+ url = _appendSchema(url);
+
+ final uri = Uri.tryParse(url);
+ if (uri == null ||
+ !uri.isAbsolute ||
+ uri.host.isEmpty ||
+ !uri.scheme.startsWith("http")) {
+ return t.login.error.invalid_server_url;
+ }
+
+ return null;
+ }
+
+ Future validateServer(String url) async {
+ url = _appendSchema(url);
+
+ final LoginService loginService = di();
+
+ try {
+ // parse instead of tryParse since the method expects a valid well formed URI
+ final uri = Uri.parse(url);
+ emit(state.copyWith(isValidationInProgress: true));
+
+ // Check if the endpoint is available
+ if (!await loginService.isEndpointAvailable(uri)) {
+ SnackbarManager.showError(t.login.error.server_not_reachable);
+ return;
+ }
+
+ // Check for /.well-known/immich
+ url = await loginService.resolveEndpoint(uri);
+
+ di().put(StoreKey.serverEndpoint, url);
+ ServiceLocator.registerPostValidationServices(url);
+
+ // Fetch server features
+ await di().getFeatures();
+
+ emit(state.copyWith(isServerValidated: true));
+ } finally {
+ emit(state.copyWith(isValidationInProgress: false));
+ }
+ }
+
+ Future passwordLogin({
+ required String email,
+ required String password,
+ }) async {
+ emit(state.copyWith(isValidationInProgress: true));
+
+ final url = di().get(StoreKey.serverEndpoint);
+ }
+
+ Future oAuthLogin() async {
+ emit(state.copyWith(isValidationInProgress: true));
+
+ final url = di().get(StoreKey.serverEndpoint);
+ }
+
+ void resetServerValidation() {
+ emit(LoginPageState.reset());
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart b/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart
new file mode 100644
index 0000000000..dd3efc6092
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart
@@ -0,0 +1,187 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/domain/models/server-info/server_feature_config.model.dart';
+import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
+import 'package:immich_mobile/presentation/components/common/loading_indaticator.widget.dart';
+import 'package:immich_mobile/presentation/components/input/filled_button.widget.dart';
+import 'package:immich_mobile/presentation/components/input/password_form_field.widget.dart';
+import 'package:immich_mobile/presentation/components/input/text_button.widget.dart';
+import 'package:immich_mobile/presentation/components/input/text_form_field.widget.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/presentation/modules/login/states/login_page.state.dart';
+import 'package:immich_mobile/service_locator.dart';
+import 'package:material_symbols_icons/symbols.dart';
+
+class LoginForm extends StatelessWidget {
+ final TextEditingController serverUrlController;
+ final TextEditingController emailController;
+ final TextEditingController passwordController;
+
+ const LoginForm({
+ super.key,
+ required this.serverUrlController,
+ required this.emailController,
+ required this.passwordController,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocSelector(
+ selector: (model) => model.isServerValidated,
+ builder: (_, isServerValidated) => AnimatedSwitcher(
+ duration: Durations.medium1,
+ child: SingleChildScrollView(
+ child: isServerValidated
+ ? _CredentialsPage(
+ emailController: emailController,
+ passwordController: passwordController,
+ )
+ : _ServerPage(controller: serverUrlController),
+ ),
+ layoutBuilder: (current, previous) =>
+ current ?? (previous.lastOrNull ?? const SizedBox.shrink()),
+ ),
+ );
+ }
+}
+
+class _ServerPage extends StatelessWidget {
+ final TextEditingController controller;
+ final GlobalKey _formKey = GlobalKey();
+
+ _ServerPage({required this.controller});
+
+ Future _validateForm(BuildContext context) async {
+ if (_formKey.currentState?.validate() == true) {
+ await context.read().validateServer(controller.text);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Form(
+ key: _formKey,
+ child: BlocSelector(
+ selector: (model) => model.isValidationInProgress,
+ builder: (_, isValidationInProgress) => Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ImTextFormField(
+ controller: controller,
+ label: context.t.login.label.endpoint,
+ validator: context.read().validateServerUrl,
+ autoFillHints: const [AutofillHints.url],
+ keyboardType: TextInputType.url,
+ textInputAction: TextInputAction.go,
+ isDisabled: isValidationInProgress,
+ ),
+ const SizedGap.mh(),
+ ImFilledButton(
+ label: context.t.login.label.next_button,
+ icon: Symbols.arrow_forward_rounded,
+ onPressed: () => unawaited(_validateForm(context)),
+ isDisabled: isValidationInProgress,
+ ),
+ const SizedGap.mh(),
+ if (isValidationInProgress) const ImLoadingIndicator(),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _CredentialsPage extends StatefulWidget {
+ final TextEditingController emailController;
+ final TextEditingController passwordController;
+
+ const _CredentialsPage({
+ required this.emailController,
+ required this.passwordController,
+ });
+
+ @override
+ State<_CredentialsPage> createState() => _CredentialsPageState();
+}
+
+class _CredentialsPageState extends State<_CredentialsPage> {
+ final passwordFocusNode = FocusNode();
+
+ @override
+ void dispose() {
+ passwordFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocSelector(
+ selector: (model) => model.isValidationInProgress,
+ builder: (_, isValidationInProgress) => isValidationInProgress
+ ? const ImLoadingIndicator()
+ : BlocBuilder(
+ bloc: di(),
+ builder: (_, state) => Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (state.features.hasPasswordLogin) ...[
+ ImTextFormField(
+ label: context.t.login.label.email,
+ isDisabled: isValidationInProgress,
+ textInputAction: TextInputAction.next,
+ onSubmitted: (_) => passwordFocusNode.requestFocus(),
+ ),
+ const SizedGap.mh(),
+ ImPasswordFormField(
+ label: context.t.login.label.password,
+ focusNode: passwordFocusNode,
+ isDisabled: isValidationInProgress,
+ textInputAction: TextInputAction.go,
+ ),
+ const SizedGap.mh(),
+ ImFilledButton(
+ label: context.t.login.label.login_button,
+ icon: Symbols.login_rounded,
+ onPressed: () =>
+ context.read().passwordLogin(
+ email: widget.emailController.text,
+ password: widget.passwordController.text,
+ ),
+ ),
+ // Divider when both password and oAuth login is enabled
+ if (state.features.hasOAuthLogin) const Divider(),
+ ],
+ if (state.features.hasOAuthLogin)
+ ImFilledButton(
+ label: state.config.oauthButtonText ??
+ context.t.login.label.oauth_button,
+ icon: Symbols.pin_rounded,
+ onPressed: () => unawaited(
+ context.read().oAuthLogin(),
+ ),
+ ),
+ if (!state.features.hasPasswordLogin &&
+ !state.features.hasOAuthLogin)
+ ImFilledButton(
+ label: context.t.login.label.login_disabled,
+ isDisabled: true,
+ ),
+ const SizedGap.sh(),
+ ImTextButton(
+ label: context.t.login.label.back_button,
+ icon: Symbols.arrow_back_rounded,
+ onPressed:
+ context.read().resetServerValidation,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/logs/pages/log.page.dart b/mobile-v2/lib/presentation/modules/logs/pages/log.page.dart
new file mode 100644
index 0000000000..1aaea8a514
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/logs/pages/log.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class LogsPage extends StatelessWidget {
+ const LogsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(body: Center(child: Text("Logs Page")));
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/models/settings_section.model.dart b/mobile-v2/lib/presentation/modules/settings/models/settings_section.model.dart
new file mode 100644
index 0000000000..d2a56d4dd7
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/settings/models/settings_section.model.dart
@@ -0,0 +1,32 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:material_symbols_icons/symbols.dart';
+
+enum SettingSection {
+ general(
+ icon: Symbols.interests_rounded,
+ labelKey: 'settings.sections.general',
+ destination: GeneralSettingsRoute(),
+ ),
+ advance(
+ icon: Symbols.build_rounded,
+ labelKey: 'settings.sections.advance',
+ destination: AdvanceSettingsRoute(),
+ ),
+ about(
+ icon: Symbols.help_rounded,
+ labelKey: 'settings.sections.about',
+ destination: AboutSettingsRoute(),
+ );
+
+ final PageRouteInfo destination;
+ final String labelKey;
+ final IconData icon;
+
+ const SettingSection({
+ required this.labelKey,
+ required this.icon,
+ required this.destination,
+ });
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/pages/about_settings.page.dart b/mobile-v2/lib/presentation/modules/settings/pages/about_settings.page.dart
new file mode 100644
index 0000000000..daa0ec146d
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/settings/pages/about_settings.page.dart
@@ -0,0 +1,16 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart';
+
+@RoutePage()
+class AboutSettingsPage extends StatelessWidget {
+ const AboutSettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ appBar: ImAdaptiveRouteSecondaryAppBar(),
+ body: Center(child: Text('About Settings')),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/pages/advance_settings.page.dart b/mobile-v2/lib/presentation/modules/settings/pages/advance_settings.page.dart
new file mode 100644
index 0000000000..0ae30810d9
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/settings/pages/advance_settings.page.dart
@@ -0,0 +1,16 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart';
+
+@RoutePage()
+class AdvanceSettingsPage extends StatelessWidget {
+ const AdvanceSettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ appBar: ImAdaptiveRouteSecondaryAppBar(),
+ body: Center(child: Text('Advanced Settings')),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/pages/general_settings.page.dart b/mobile-v2/lib/presentation/modules/settings/pages/general_settings.page.dart
new file mode 100644
index 0000000000..deabaead98
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/settings/pages/general_settings.page.dart
@@ -0,0 +1,16 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_secondary_appbar.widget.dart';
+
+@RoutePage()
+class GeneralSettingsPage extends StatelessWidget {
+ const GeneralSettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ appBar: ImAdaptiveRouteSecondaryAppBar(),
+ body: Center(child: Text('General Settings')),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart b/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
index e6cb37ac1c..59a3729c9e 100644
--- a/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
+++ b/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
@@ -1,5 +1,25 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
+import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_primary_appbar.widget.dart';
+import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
+import 'package:immich_mobile/presentation/modules/settings/models/settings_section.model.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
+
+@RoutePage()
+class SettingsWrapperPage extends StatelessWidget {
+ const SettingsWrapperPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ImAdaptiveRouteWrapper(
+ primaryBody: (_) => const SettingsPage(),
+ primaryRoute: SettingsRoute.name,
+ bodyRatio: 0.3,
+ );
+ }
+}
@RoutePage()
class SettingsPage extends StatelessWidget {
@@ -7,6 +27,21 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container();
+ return Scaffold(
+ appBar: const ImAdaptiveRoutePrimaryAppBar(),
+ body: ListView.builder(
+ itemCount: SettingSection.values.length,
+ itemBuilder: (_, index) {
+ final section = SettingSection.values.elementAt(index);
+ return ListTile(
+ title: Text(context.t[section.labelKey]),
+ onTap: () {
+ context.navigateRoot(section.destination);
+ },
+ leading: Icon(section.icon),
+ );
+ },
+ ),
+ );
}
}
diff --git a/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart b/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart
new file mode 100644
index 0000000000..7682f9fa1c
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart
@@ -0,0 +1,71 @@
+import 'package:flutter/material.dart';
+
+@immutable
+abstract class AppColors {
+ const AppColors();
+
+ /// Blue color
+ static const ColorScheme blueLight = ColorScheme(
+ brightness: Brightness.light,
+ primary: Color(0xff1145a4),
+ onPrimary: Color(0xffffffff),
+ primaryContainer: Color(0xffdae2ff),
+ onPrimaryContainer: Color(0xff001848),
+ secondary: Color(0xff4b73d3),
+ onSecondary: Color(0xfffefbff),
+ secondaryContainer: Color(0xffeef0ff),
+ onSecondaryContainer: Color(0xff001848),
+ tertiary: Color(0xff814b81),
+ onTertiary: Color(0xfffffbff),
+ tertiaryContainer: Color(0xffffd6fa),
+ onTertiaryContainer: Color(0xff340439),
+ error: Color(0xffba1a1a),
+ onError: Color(0xfffffbff),
+ errorContainer: Color(0xffffdad6),
+ onErrorContainer: Color(0xff410002),
+ surface: Color(0xfffefbff),
+ onSurface: Color(0xff1a1b21),
+ onSurfaceVariant: Color(0xff444651),
+ surfaceContainerHighest: Color(0xffe0e2ef),
+ outline: Color(0xff747782),
+ outlineVariant: Color(0xffc4c6d3),
+ shadow: Color(0xff000000),
+ scrim: Color(0xff000000),
+ inverseSurface: Color(0xff2f3036),
+ onInverseSurface: Color(0xfff1f0f7),
+ inversePrimary: Color(0xffb2c5ff),
+ surfaceTint: Color(0xff06409f),
+ );
+
+ static const ColorScheme blueDark = ColorScheme(
+ brightness: Brightness.dark,
+ primary: Color(0xffa9c7ff),
+ onPrimary: Color(0xff001b3d),
+ primaryContainer: Color(0xff00468c),
+ onPrimaryContainer: Color(0xffd6e3ff),
+ secondary: Color(0xffd6e3ff),
+ onSecondary: Color(0xff001b3d),
+ secondaryContainer: Color(0xff003063),
+ onSecondaryContainer: Color(0xffd6e3ff),
+ tertiary: Color(0xffeab4f6),
+ onTertiary: Color(0xff310540),
+ tertiaryContainer: Color(0xff61356e),
+ onTertiaryContainer: Color(0xfffad7ff),
+ error: Color(0xffffb4ab),
+ onError: Color(0xff410002),
+ errorContainer: Color(0xff93000a),
+ onErrorContainer: Color(0xffffb4ab),
+ surface: Color(0xff1a1e22),
+ onSurface: Color(0xffe2e2e9),
+ onSurfaceVariant: Color(0xffc2c6d2),
+ surfaceContainerHighest: Color(0xff424852),
+ outline: Color(0xff8c919c),
+ outlineVariant: Color(0xff424751),
+ shadow: Color(0xff000000),
+ scrim: Color(0xff000000),
+ inverseSurface: Color(0xffe1e1e9),
+ onInverseSurface: Color(0xff2e3036),
+ inversePrimary: Color(0xff005db7),
+ surfaceTint: Color(0xffa9c7ff),
+ );
+}
diff --git a/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart b/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart
new file mode 100644
index 0000000000..e674ac04ac
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart
@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_colors.model.dart';
+import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
+
+enum AppTheme {
+ blue(AppColors.blueLight, AppColors.blueDark),
+ // Fallback color for dynamic theme for non-supported platforms
+ dynamic(AppColors.blueLight, AppColors.blueDark);
+
+ final ColorScheme lightSchema;
+ final ColorScheme darkSchema;
+
+ const AppTheme(this.lightSchema, this.darkSchema);
+
+ static ThemeData generateThemeData(ColorScheme color) {
+ return ThemeData(
+ colorScheme: color,
+ primaryColor: color.primary,
+ iconTheme: const IconThemeData(weight: 500, opticalSize: 24),
+ navigationBarTheme: NavigationBarThemeData(
+ backgroundColor: color.surface,
+ indicatorColor: color.primary,
+ iconTheme: WidgetStateProperty.resolveWith(
+ (Set states) {
+ if (states.isSelected) {
+ return IconThemeData(color: color.onPrimary);
+ }
+ return IconThemeData(color: color.onSurface.withAlpha(175));
+ },
+ ),
+ ),
+ navigationRailTheme: NavigationRailThemeData(
+ backgroundColor: color.surface,
+ elevation: 3,
+ indicatorColor: color.primary,
+ selectedIconTheme:
+ IconThemeData(weight: 500, opticalSize: 24, color: color.onPrimary),
+ unselectedIconTheme: IconThemeData(
+ weight: 500,
+ opticalSize: 24,
+ color: color.onSurface.withAlpha(175),
+ ),
+ ),
+ inputDecorationTheme: const InputDecorationTheme(
+ border: OutlineInputBorder(),
+ ),
+ sliderTheme: SliderThemeData(
+ valueIndicatorColor:
+ Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
+ .withAlpha(240),
+ ),
+ snackBarTheme: SnackBarThemeData(
+ elevation: 4,
+ behavior: SnackBarBehavior.floating,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(4.0)),
+ ),
+ insetPadding: const EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 25.0),
+ backgroundColor:
+ Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
+ .withAlpha(240),
+ actionTextColor: color.inversePrimary,
+ contentTextStyle: TextStyle(color: color.onInverseSurface),
+ closeIconColor: color.onInverseSurface,
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/theme/states/app_theme.state.dart b/mobile-v2/lib/presentation/modules/theme/states/app_theme.state.dart
new file mode 100644
index 0000000000..4b38625218
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/theme/states/app_theme.state.dart
@@ -0,0 +1,23 @@
+import 'dart:async';
+
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/domain/models/app_setting.model.dart';
+import 'package:immich_mobile/domain/services/app_setting.service.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
+
+class AppThemeCubit extends Cubit {
+ final AppSettingService _appSettings;
+ StreamSubscription? _appSettingSubscription;
+
+ AppThemeCubit(this._appSettings) : super(AppTheme.blue) {
+ _appSettingSubscription = _appSettings
+ .watchSetting(AppSetting.appTheme)
+ .listen((theme) => emit(theme));
+ }
+
+ @override
+ Future close() {
+ _appSettingSubscription?.cancel();
+ return super.close();
+ }
+}
diff --git a/mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart b/mobile-v2/lib/presentation/modules/theme/widgets/app_theme_builder.widget.dart
similarity index 54%
rename from mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart
rename to mobile-v2/lib/presentation/modules/theme/widgets/app_theme_builder.widget.dart
index 29acaa457b..a06fac41b9 100644
--- a/mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart
+++ b/mobile-v2/lib/presentation/modules/theme/widgets/app_theme_builder.widget.dart
@@ -1,6 +1,6 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
-import 'package:immich_mobile/presentation/theme/utils/colors.dart';
+import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
class AppThemeBuilder extends StatelessWidget {
const AppThemeBuilder({
@@ -14,26 +14,30 @@ class AppThemeBuilder extends StatelessWidget {
/// Builds the child widget of this widget, providing a light and dark [ThemeData] based on the
/// [theme] passed.
- final Widget Function(ThemeData lightTheme, ThemeData darkTheme) builder;
+ final Widget Function(
+ BuildContext context,
+ ThemeData lightTheme,
+ ThemeData darkTheme,
+ ) builder;
@override
Widget build(BuildContext context) {
// Static colors
if (theme != AppTheme.dynamic) {
- final lightTheme = AppColors.getThemeForColorScheme(theme.lightSchema);
- final darkTheme = AppColors.getThemeForColorScheme(theme.darkSchema);
+ final lightTheme = AppTheme.generateThemeData(theme.lightSchema);
+ final darkTheme = AppTheme.generateThemeData(theme.darkSchema);
- return builder(lightTheme, darkTheme);
+ return builder(context, lightTheme, darkTheme);
}
// Dynamic color builder
return DynamicColorBuilder(builder: (lightDynamic, darkDynamic) {
final lightTheme =
- AppColors.getThemeForColorScheme(lightDynamic ?? theme.lightSchema);
+ AppTheme.generateThemeData(lightDynamic ?? theme.lightSchema);
final darkTheme =
- AppColors.getThemeForColorScheme(darkDynamic ?? theme.darkSchema);
+ AppTheme.generateThemeData(darkDynamic ?? theme.darkSchema);
- return builder(lightTheme, darkTheme);
+ return builder(context, lightTheme, darkTheme);
});
}
}
diff --git a/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart
new file mode 100644
index 0000000000..398bb1b12e
--- /dev/null
+++ b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart
@@ -0,0 +1,73 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
+import 'package:immich_mobile/presentation/modules/login/states/login_page.state.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';
+
+@RoutePage()
+class SplashScreenWrapperPage extends AutoRouter implements AutoRouteWrapper {
+ const SplashScreenWrapperPage({super.key});
+
+ @override
+ Widget wrappedRoute(BuildContext context) {
+ return BlocProvider(create: (_) => LoginPageCubit(), child: this);
+ }
+}
+
+@RoutePage()
+class SplashScreenPage extends StatefulWidget {
+ const SplashScreenPage({super.key});
+
+ @override
+ State createState() => _SplashScreenState();
+}
+
+class _SplashScreenState extends State
+ with SingleTickerProviderStateMixin, LogContext {
+ late final AnimationController _animationController;
+
+ @override
+ void initState() {
+ super.initState();
+ _animationController = AnimationController(
+ duration: const Duration(seconds: 30),
+ vsync: this,
+ )..repeat();
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: FutureBuilder(
+ future: di.allReady(),
+ builder: (_, snap) {
+ if (snap.hasData) {
+ context.replaceRoute(const LoginRoute());
+ } else if (snap.hasError) {
+ log.severe(
+ "Error while initializing the app",
+ snap.error,
+ snap.stackTrace,
+ );
+ }
+
+ return Center(
+ child: RotationTransition(
+ turns: _animationController,
+ child: const ImLogo(width: 100),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/router/router.dart b/mobile-v2/lib/presentation/router/router.dart
index f310e823bd..97fd4619f6 100644
--- a/mobile-v2/lib/presentation/router/router.dart
+++ b/mobile-v2/lib/presentation/router/router.dart
@@ -1,25 +1,52 @@
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/presentation/modules/home/pages/home.page.dart';
import 'package:immich_mobile/presentation/modules/library/pages/library.page.dart';
+import 'package:immich_mobile/presentation/modules/login/pages/login.page.dart';
+import 'package:immich_mobile/presentation/modules/logs/pages/log.page.dart';
import 'package:immich_mobile/presentation/modules/search/pages/search.page.dart';
+import 'package:immich_mobile/presentation/modules/settings/pages/about_settings.page.dart';
+import 'package:immich_mobile/presentation/modules/settings/pages/advance_settings.page.dart';
+import 'package:immich_mobile/presentation/modules/settings/pages/general_settings.page.dart';
import 'package:immich_mobile/presentation/modules/settings/pages/settings.page.dart';
import 'package:immich_mobile/presentation/modules/sharing/pages/sharing.page.dart';
+import 'package:immich_mobile/presentation/router/pages/splash_screen.page.dart';
import 'package:immich_mobile/presentation/router/pages/tab_controller.page.dart';
part 'router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
-class AppRouter extends _$AppRouter {
+class AppRouter extends _$AppRouter implements AutoRouteGuard {
AppRouter();
@override
List get routes => [
- AutoRoute(page: TabControllerRoute.page, initial: true, children: [
+ AutoRoute(
+ page: SplashScreenWrapperRoute.page,
+ initial: true,
+ children: [
+ AutoRoute(page: SplashScreenRoute.page, initial: true),
+ AutoRoute(page: LoginRoute.page),
+ ],
+ ),
+ AutoRoute(page: LogsRoute.page),
+ AutoRoute(page: TabControllerRoute.page, children: [
AutoRoute(page: HomeRoute.page),
AutoRoute(page: SearchRoute.page),
AutoRoute(page: SharingRoute.page),
AutoRoute(page: LibraryRoute.page),
]),
- AutoRoute(page: SettingsRoute.page),
+ AutoRoute(page: SettingsWrapperRoute.page, children: [
+ AutoRoute(page: SettingsRoute.page),
+ AutoRoute(page: GeneralSettingsRoute.page),
+ AutoRoute(page: AboutSettingsRoute.page),
+ AutoRoute(page: AdvanceSettingsRoute.page),
+ ]),
];
+
+ // Global guards
+ @override
+ void onNavigation(NavigationResolver resolver, StackRouter router) {
+ // Prevent duplicates
+ resolver.next(resolver.route.name != router.current.name);
+ }
}
diff --git a/mobile-v2/lib/presentation/theme/states/app_theme.state.dart b/mobile-v2/lib/presentation/theme/states/app_theme.state.dart
deleted file mode 100644
index 1cc3c4f990..0000000000
--- a/mobile-v2/lib/presentation/theme/states/app_theme.state.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-import 'package:immich_mobile/domain/models/app_setting.model.dart';
-import 'package:immich_mobile/domain/services/app_setting.service.dart';
-import 'package:immich_mobile/presentation/theme/utils/colors.dart';
-
-class AppThemeState extends ValueNotifier {
- final AppSettingsService _appSettings;
- StreamSubscription? _appSettingSubscription;
-
- AppThemeState({required AppSettingsService appSettings})
- : _appSettings = appSettings,
- super(AppTheme.blue);
-
- void init() {
- _appSettingSubscription =
- _appSettings.watchSetting(AppSettings.appTheme).listen((themeIndex) {
- final theme =
- AppTheme.values.elementAtOrNull(themeIndex) ?? AppTheme.blue;
- value = theme;
- });
- }
-
- @override
- void dispose() {
- _appSettingSubscription?.cancel();
- return super.dispose();
- }
-}
diff --git a/mobile-v2/lib/presentation/theme/utils/colors.dart b/mobile-v2/lib/presentation/theme/utils/colors.dart
deleted file mode 100644
index f365c4fc9c..0000000000
--- a/mobile-v2/lib/presentation/theme/utils/colors.dart
+++ /dev/null
@@ -1,92 +0,0 @@
-import 'package:flutter/material.dart';
-
-enum AppTheme {
- blue(AppColors._blueLight, AppColors._blueDark),
- // Fallback color for dynamic theme for non-supported platforms
- dynamic(AppColors._blueLight, AppColors._blueDark);
-
- final ColorScheme lightSchema;
- final ColorScheme darkSchema;
-
- const AppTheme(this.lightSchema, this.darkSchema);
-}
-
-class AppColors {
- const AppColors();
-
- /// Blue color
- static const ColorScheme _blueLight = ColorScheme(
- brightness: Brightness.light,
- primary: Color(0xff1565c0),
- onPrimary: Color(0xffffffff),
- primaryContainer: Color(0xffd6e3ff),
- onPrimaryContainer: Color(0xff001b3d),
- secondary: Color(0xff3277d2),
- onSecondary: Color(0xfffdfbff),
- secondaryContainer: Color(0xffecf0ff),
- onSecondaryContainer: Color(0xff001b3d),
- tertiary: Color(0xff7b4d88),
- onTertiary: Color(0xfffffbff),
- tertiaryContainer: Color(0xfffad7ff),
- onTertiaryContainer: Color(0xff310540),
- error: Color(0xffba1a1a),
- onError: Color(0xfffffbff),
- errorContainer: Color(0xffffdad6),
- onErrorContainer: Color(0xff410002),
- background: Color(0xfffcfafe),
- onBackground: Color(0xff191c20),
- surface: Color(0xfffdfbff),
- onSurface: Color(0xff191c20),
- surfaceVariant: Color(0xffdfe2ef),
- onSurfaceVariant: Color(0xff424751),
- outline: Color(0xff737782),
- outlineVariant: Color(0xffc2c6d2),
- shadow: Color(0xff000000),
- scrim: Color(0xff000000),
- inverseSurface: Color(0xff2e3036),
- onInverseSurface: Color(0xfff0f0f7),
- inversePrimary: Color(0xffa9c7ff),
- surfaceTint: Color(0xff00468c),
- );
-
- static const ColorScheme _blueDark = ColorScheme(
- brightness: Brightness.dark,
- primary: Color(0xffa9c7ff),
- onPrimary: Color(0xff001b3d),
- primaryContainer: Color(0xff00468c),
- onPrimaryContainer: Color(0xffd6e3ff),
- secondary: Color(0xffd6e3ff),
- onSecondary: Color(0xff001b3d),
- secondaryContainer: Color(0xff003063),
- onSecondaryContainer: Color(0xffd6e3ff),
- tertiary: Color(0xffeab4f6),
- onTertiary: Color(0xff310540),
- tertiaryContainer: Color(0xff61356e),
- onTertiaryContainer: Color(0xfffad7ff),
- error: Color(0xffffb4ab),
- onError: Color(0xff410002),
- errorContainer: Color(0xff93000a),
- onErrorContainer: Color(0xffffb4ab),
- background: Color(0xff1a1d21),
- onBackground: Color(0xffe2e2e9),
- surface: Color(0xff1a1e22),
- onSurface: Color(0xffe2e2e9),
- surfaceVariant: Color(0xff424852),
- onSurfaceVariant: Color(0xffc2c6d2),
- outline: Color(0xff8c919c),
- outlineVariant: Color(0xff424751),
- shadow: Color(0xff000000),
- scrim: Color(0xff000000),
- inverseSurface: Color(0xffe1e1e9),
- onInverseSurface: Color(0xff2e3036),
- inversePrimary: Color(0xff005db7),
- surfaceTint: Color(0xffa9c7ff),
- );
-
- static ThemeData getThemeForColorScheme(ColorScheme color) {
- return ThemeData(
- primaryColor: color.primary,
- iconTheme: const IconThemeData(weight: 400),
- );
- }
-}
diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart
index 532f99f4e8..2b7ffcd1b9 100644
--- a/mobile-v2/lib/service_locator.dart
+++ b/mobile-v2/lib/service_locator.dart
@@ -1,13 +1,19 @@
+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';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/domain/repositories/log.repository.dart';
import 'package:immich_mobile/domain/repositories/store.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/store_manager.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/presentation/theme/states/app_theme.state.dart';
-import 'package:watch_it/watch_it.dart';
+import 'package:openapi/openapi.dart';
+
+final di = GetIt.I;
class ServiceLocator {
const ServiceLocator._internal();
@@ -15,26 +21,50 @@ class ServiceLocator {
static void configureServices() {
// Register DB
di.registerSingleton(DriftDatabaseRepository());
- _registerDomainServices();
- _registerPresentationService();
+ _registerPreValidationServices();
}
- static void _registerDomainServices() {
+ static void _registerPreValidationServices() {
+ // ====== DOMAIN
+
// Init store
di.registerFactory(() => StoreDriftRepository(di()));
- di.registerSingleton(StoreManager(di()));
+ // StoreManager populates its cache with a async gap, manually signalReady once the cache is populated.
+ di.registerSingleton(StoreManager(di()), signalsReady: true);
// Logs
di.registerFactory(() => LogDriftRepository(di()));
// App Settings
- di.registerFactory(() => AppSettingsService(di()));
- }
+ di.registerFactory(() => AppSettingService(di()));
+ // Login Service
+ di.registerFactory(() => const LoginService());
+
+ // ====== PRESENTATION
- static void _registerPresentationService() {
// App router
di.registerSingleton(AppRouter());
// Global states
- di.registerLazySingleton(
- () => AppThemeState(appSettings: di())..init(),
+ di.registerLazySingleton(() => AppThemeCubit(di()));
+ }
+
+ static void registerPostValidationServices(String endpoint) {
+ if (di.isRegistered()) {
+ return;
+ }
+
+ // ====== DOMAIN
+
+ di.registerSingleton(
+ Openapi(
+ basePathOverride: endpoint,
+ interceptors: [BearerAuthInterceptor()],
+ ),
+ );
+ di.registerFactory(() => ServerInfoService(di()));
+
+ // ====== PRESENTATION
+
+ di.registerLazySingleton(
+ () => ServerFeatureConfigCubit(di()),
);
}
}
diff --git a/mobile-v2/lib/utils/constants/globals.dart b/mobile-v2/lib/utils/constants/globals.dart
new file mode 100644
index 0000000000..bedd6cdd49
--- /dev/null
+++ b/mobile-v2/lib/utils/constants/globals.dart
@@ -0,0 +1,4 @@
+import 'package:flutter/material.dart';
+
+/// Global ScaffoldMessengerKey to show snackbars
+final GlobalKey kScafMessengerKey = GlobalKey();
diff --git a/mobile-v2/lib/utils/constants/size_constants.dart b/mobile-v2/lib/utils/constants/size_constants.dart
new file mode 100644
index 0000000000..e4dbe1c86e
--- /dev/null
+++ b/mobile-v2/lib/utils/constants/size_constants.dart
@@ -0,0 +1,11 @@
+import 'package:flutter/material.dart';
+
+@immutable
+class SizeConstants {
+ const SizeConstants._();
+
+ static const s = 8.0;
+ static const m = 16.0;
+ static const l = 32.0;
+ static const xl = 64.0;
+}
diff --git a/mobile-v2/lib/utils/extensions/build_context.extension.dart b/mobile-v2/lib/utils/extensions/build_context.extension.dart
new file mode 100644
index 0000000000..96174caaed
--- /dev/null
+++ b/mobile-v2/lib/utils/extensions/build_context.extension.dart
@@ -0,0 +1,30 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+extension BuildContextHelper on BuildContext {
+ /// Get the current [ThemeData] used
+ ThemeData get theme => Theme.of(this);
+
+ /// Get the default [TextStyle]
+ TextStyle get defaultTextStyle => DefaultTextStyle.of(this).style;
+
+ /// Get the [Size] of [MediaQuery]
+ Size get mediaQuerySize => MediaQuery.sizeOf(this);
+
+ /// Get the [EdgeInsets] of [MediaQuery]
+ EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
+
+ /// True if the current device is a Tablet
+ bool get isTablet => (mediaQuerySize.width >= 600);
+
+ /// True if the current app theme is dark
+ bool get isDarkTheme => theme.brightness == Brightness.dark;
+
+ /// Navigate using the root router
+ // ignore: avoid-dynamic
+ Future navigateRoot(
+ PageRouteInfo route, {
+ OnNavigationFailure? onFailure,
+ }) =>
+ router.root.navigate(route, onFailure: onFailure);
+}
diff --git a/mobile-v2/lib/utils/extensions/material_state.extension.dart b/mobile-v2/lib/utils/extensions/material_state.extension.dart
new file mode 100644
index 0000000000..71fe2fb4e0
--- /dev/null
+++ b/mobile-v2/lib/utils/extensions/material_state.extension.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/material.dart';
+
+extension MaterialStateHelpers on Iterable {
+ bool get isDisabled => contains(WidgetState.disabled);
+ bool get isDragged => contains(WidgetState.dragged);
+ bool get isError => contains(WidgetState.error);
+ bool get isFocused => contains(WidgetState.focused);
+ bool get isHovered => contains(WidgetState.hovered);
+ bool get isPressed => contains(WidgetState.pressed);
+ bool get isScrolledUnder => contains(WidgetState.scrolledUnder);
+ bool get isSelected => contains(WidgetState.selected);
+}
diff --git a/mobile-v2/lib/utils/mixins/log_context.mixin.dart b/mobile-v2/lib/utils/mixins/log_context.mixin.dart
index 54b3eaf9d6..a15ff6633a 100644
--- a/mobile-v2/lib/utils/mixins/log_context.mixin.dart
+++ b/mobile-v2/lib/utils/mixins/log_context.mixin.dart
@@ -2,13 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
mixin LogContext {
- late final String ctx = logContext;
-
- /// Context name of the log message
- /// Override this to provide a custom name
- String get logContext => runtimeType.toString();
-
@protected
@nonVirtual
- Logger get log => Logger.detached(ctx);
+ Logger get log => Logger.detached(runtimeType.toString());
}
diff --git a/mobile-v2/lib/utils/snackbar_manager.dart b/mobile-v2/lib/utils/snackbar_manager.dart
new file mode 100644
index 0000000000..2f0010f8b8
--- /dev/null
+++ b/mobile-v2/lib/utils/snackbar_manager.dart
@@ -0,0 +1,13 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/utils/constants/globals.dart';
+
+class SnackbarManager {
+ const SnackbarManager();
+
+ static ScaffoldMessengerState? get _s => kScafMessengerKey.currentState;
+
+ static void showError(String errorMsg) {
+ _s?.clearSnackBars();
+ _s?.showSnackBar(SnackBar(content: Text(errorMsg)));
+ }
+}
diff --git a/mobile-v2/openapi/.gitignore b/mobile-v2/openapi/.gitignore
deleted file mode 100644
index 4298cdcbd1..0000000000
--- a/mobile-v2/openapi/.gitignore
+++ /dev/null
@@ -1,41 +0,0 @@
-# See https://dart.dev/guides/libraries/private-files
-
-# Files and directories created by pub
-.dart_tool/
-.buildlog
-.packages
-.project
-.pub/
-build/
-**/packages/
-
-# Files created by dart2js
-# (Most Dart developers will use pub build to compile Dart, use/modify these
-# rules if you intend to use dart2js directly
-# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
-# differentiate from explicit Javascript files)
-*.dart.js
-*.part.js
-*.js.deps
-*.js.map
-*.info.json
-
-# Directory created by dartdoc
-doc/api/
-
-# Don't commit pubspec lock file
-# (Library packages only! Remove pattern if developing an application package)
-pubspec.lock
-
-# Don’t commit files and directories created by other development environments.
-# For example, if your development environment creates any of the following files,
-# consider putting them in a global ignore file:
-
-# IntelliJ
-*.iml
-*.ipr
-*.iws
-.idea/
-
-# Mac
-.DS_Store
diff --git a/mobile-v2/openapi/.openapi-generator-ignore b/mobile-v2/openapi/.openapi-generator-ignore
deleted file mode 100644
index 7484ee590a..0000000000
--- a/mobile-v2/openapi/.openapi-generator-ignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# OpenAPI Generator Ignore
-# Generated by openapi-generator https://github.com/openapitools/openapi-generator
-
-# Use this file to prevent files from being overwritten by the generator.
-# The patterns follow closely to .gitignore or .dockerignore.
-
-# As an example, the C# client generator defines ApiClient.cs.
-# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
-#ApiClient.cs
-
-# You can match any string of characters against a directory, file or extension with a single asterisk (*):
-#foo/*/qux
-# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
-
-# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
-#foo/**/qux
-# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
-
-# You can also negate patterns with an exclamation (!).
-# For example, you can ignore all files in a docs folder with the file extension .md:
-#docs/*.md
-# Then explicitly reverse the ignore rule for a single file:
-#!docs/README.md
diff --git a/mobile-v2/openapi/.openapi-generator/FILES b/mobile-v2/openapi/.openapi-generator/FILES
deleted file mode 100644
index 45401dd919..0000000000
--- a/mobile-v2/openapi/.openapi-generator/FILES
+++ /dev/null
@@ -1,221 +0,0 @@
-.gitignore
-.openapi-generator-ignore
-README.md
-analysis_options.yaml
-lib/openapi.dart
-lib/src/api.dart
-lib/src/api/activity_api.dart
-lib/src/api/album_api.dart
-lib/src/api/api_key_api.dart
-lib/src/api/asset_api.dart
-lib/src/api/audit_api.dart
-lib/src/api/authentication_api.dart
-lib/src/api/download_api.dart
-lib/src/api/face_api.dart
-lib/src/api/file_report_api.dart
-lib/src/api/job_api.dart
-lib/src/api/library_api.dart
-lib/src/api/memory_api.dart
-lib/src/api/o_auth_api.dart
-lib/src/api/partner_api.dart
-lib/src/api/person_api.dart
-lib/src/api/search_api.dart
-lib/src/api/server_info_api.dart
-lib/src/api/sessions_api.dart
-lib/src/api/shared_link_api.dart
-lib/src/api/sync_api.dart
-lib/src/api/system_config_api.dart
-lib/src/api/system_metadata_api.dart
-lib/src/api/tag_api.dart
-lib/src/api/timeline_api.dart
-lib/src/api/trash_api.dart
-lib/src/api/user_api.dart
-lib/src/api_util.dart
-lib/src/auth/api_key_auth.dart
-lib/src/auth/auth.dart
-lib/src/auth/basic_auth.dart
-lib/src/auth/bearer_auth.dart
-lib/src/auth/oauth.dart
-lib/src/date_serializer.dart
-lib/src/model/activity_create_dto.dart
-lib/src/model/activity_response_dto.dart
-lib/src/model/activity_statistics_response_dto.dart
-lib/src/model/add_users_dto.dart
-lib/src/model/admin_onboarding_update_dto.dart
-lib/src/model/album_count_response_dto.dart
-lib/src/model/album_response_dto.dart
-lib/src/model/all_job_status_response_dto.dart
-lib/src/model/api_key_create_dto.dart
-lib/src/model/api_key_create_response_dto.dart
-lib/src/model/api_key_response_dto.dart
-lib/src/model/api_key_update_dto.dart
-lib/src/model/asset_bulk_delete_dto.dart
-lib/src/model/asset_bulk_update_dto.dart
-lib/src/model/asset_bulk_upload_check_dto.dart
-lib/src/model/asset_bulk_upload_check_item.dart
-lib/src/model/asset_bulk_upload_check_response_dto.dart
-lib/src/model/asset_bulk_upload_check_result.dart
-lib/src/model/asset_delta_sync_response_dto.dart
-lib/src/model/asset_face_response_dto.dart
-lib/src/model/asset_face_update_dto.dart
-lib/src/model/asset_face_update_item.dart
-lib/src/model/asset_face_without_person_response_dto.dart
-lib/src/model/asset_file_upload_response_dto.dart
-lib/src/model/asset_ids_dto.dart
-lib/src/model/asset_ids_response_dto.dart
-lib/src/model/asset_job_name.dart
-lib/src/model/asset_jobs_dto.dart
-lib/src/model/asset_order.dart
-lib/src/model/asset_response_dto.dart
-lib/src/model/asset_stats_response_dto.dart
-lib/src/model/asset_type_enum.dart
-lib/src/model/audio_codec.dart
-lib/src/model/audit_deletes_response_dto.dart
-lib/src/model/bulk_id_response_dto.dart
-lib/src/model/bulk_ids_dto.dart
-lib/src/model/change_password_dto.dart
-lib/src/model/check_existing_assets_dto.dart
-lib/src/model/check_existing_assets_response_dto.dart
-lib/src/model/clip_config.dart
-lib/src/model/clip_mode.dart
-lib/src/model/colorspace.dart
-lib/src/model/cq_mode.dart
-lib/src/model/create_album_dto.dart
-lib/src/model/create_asset_dto.dart
-lib/src/model/create_library_dto.dart
-lib/src/model/create_profile_image_dto.dart
-lib/src/model/create_profile_image_response_dto.dart
-lib/src/model/create_tag_dto.dart
-lib/src/model/create_user_dto.dart
-lib/src/model/curated_locations_response_dto.dart
-lib/src/model/curated_objects_response_dto.dart
-lib/src/model/date.dart
-lib/src/model/delete_user_dto.dart
-lib/src/model/download_archive_info.dart
-lib/src/model/download_info_dto.dart
-lib/src/model/download_response_dto.dart
-lib/src/model/entity_type.dart
-lib/src/model/exif_response_dto.dart
-lib/src/model/face_dto.dart
-lib/src/model/file_checksum_dto.dart
-lib/src/model/file_checksum_response_dto.dart
-lib/src/model/file_report_dto.dart
-lib/src/model/file_report_fix_dto.dart
-lib/src/model/file_report_item_dto.dart
-lib/src/model/image_format.dart
-lib/src/model/job_command.dart
-lib/src/model/job_command_dto.dart
-lib/src/model/job_counts_dto.dart
-lib/src/model/job_name.dart
-lib/src/model/job_settings_dto.dart
-lib/src/model/job_status_dto.dart
-lib/src/model/library_response_dto.dart
-lib/src/model/library_stats_response_dto.dart
-lib/src/model/library_type.dart
-lib/src/model/log_level.dart
-lib/src/model/login_credential_dto.dart
-lib/src/model/login_response_dto.dart
-lib/src/model/logout_response_dto.dart
-lib/src/model/map_marker_response_dto.dart
-lib/src/model/map_theme.dart
-lib/src/model/memory_create_dto.dart
-lib/src/model/memory_lane_response_dto.dart
-lib/src/model/memory_response_dto.dart
-lib/src/model/memory_type.dart
-lib/src/model/memory_update_dto.dart
-lib/src/model/merge_person_dto.dart
-lib/src/model/metadata_search_dto.dart
-lib/src/model/model_type.dart
-lib/src/model/o_auth_authorize_response_dto.dart
-lib/src/model/o_auth_callback_dto.dart
-lib/src/model/o_auth_config_dto.dart
-lib/src/model/on_this_day_dto.dart
-lib/src/model/partner_response_dto.dart
-lib/src/model/path_entity_type.dart
-lib/src/model/path_type.dart
-lib/src/model/people_response_dto.dart
-lib/src/model/people_update_dto.dart
-lib/src/model/people_update_item.dart
-lib/src/model/person_create_dto.dart
-lib/src/model/person_response_dto.dart
-lib/src/model/person_statistics_response_dto.dart
-lib/src/model/person_update_dto.dart
-lib/src/model/person_with_faces_response_dto.dart
-lib/src/model/places_response_dto.dart
-lib/src/model/queue_status_dto.dart
-lib/src/model/reaction_level.dart
-lib/src/model/reaction_type.dart
-lib/src/model/recognition_config.dart
-lib/src/model/reverse_geocoding_state_response_dto.dart
-lib/src/model/scan_library_dto.dart
-lib/src/model/search_album_response_dto.dart
-lib/src/model/search_asset_response_dto.dart
-lib/src/model/search_explore_item.dart
-lib/src/model/search_explore_response_dto.dart
-lib/src/model/search_facet_count_response_dto.dart
-lib/src/model/search_facet_response_dto.dart
-lib/src/model/search_response_dto.dart
-lib/src/model/search_suggestion_type.dart
-lib/src/model/server_config_dto.dart
-lib/src/model/server_features_dto.dart
-lib/src/model/server_info_response_dto.dart
-lib/src/model/server_media_types_response_dto.dart
-lib/src/model/server_ping_response.dart
-lib/src/model/server_stats_response_dto.dart
-lib/src/model/server_theme_dto.dart
-lib/src/model/server_version_response_dto.dart
-lib/src/model/session_response_dto.dart
-lib/src/model/shared_link_create_dto.dart
-lib/src/model/shared_link_edit_dto.dart
-lib/src/model/shared_link_response_dto.dart
-lib/src/model/shared_link_type.dart
-lib/src/model/sign_up_dto.dart
-lib/src/model/smart_info_response_dto.dart
-lib/src/model/smart_search_dto.dart
-lib/src/model/system_config_dto.dart
-lib/src/model/system_config_f_fmpeg_dto.dart
-lib/src/model/system_config_image_dto.dart
-lib/src/model/system_config_job_dto.dart
-lib/src/model/system_config_library_dto.dart
-lib/src/model/system_config_library_scan_dto.dart
-lib/src/model/system_config_library_watch_dto.dart
-lib/src/model/system_config_logging_dto.dart
-lib/src/model/system_config_machine_learning_dto.dart
-lib/src/model/system_config_map_dto.dart
-lib/src/model/system_config_new_version_check_dto.dart
-lib/src/model/system_config_o_auth_dto.dart
-lib/src/model/system_config_password_login_dto.dart
-lib/src/model/system_config_reverse_geocoding_dto.dart
-lib/src/model/system_config_server_dto.dart
-lib/src/model/system_config_storage_template_dto.dart
-lib/src/model/system_config_template_storage_option_dto.dart
-lib/src/model/system_config_theme_dto.dart
-lib/src/model/system_config_trash_dto.dart
-lib/src/model/system_config_user_dto.dart
-lib/src/model/tag_response_dto.dart
-lib/src/model/tag_type_enum.dart
-lib/src/model/thumbnail_format.dart
-lib/src/model/time_bucket_response_dto.dart
-lib/src/model/time_bucket_size.dart
-lib/src/model/tone_mapping.dart
-lib/src/model/transcode_hw_accel.dart
-lib/src/model/transcode_policy.dart
-lib/src/model/update_album_dto.dart
-lib/src/model/update_asset_dto.dart
-lib/src/model/update_library_dto.dart
-lib/src/model/update_partner_dto.dart
-lib/src/model/update_stack_parent_dto.dart
-lib/src/model/update_tag_dto.dart
-lib/src/model/update_user_dto.dart
-lib/src/model/usage_by_user_dto.dart
-lib/src/model/user_avatar_color.dart
-lib/src/model/user_dto.dart
-lib/src/model/user_response_dto.dart
-lib/src/model/user_status.dart
-lib/src/model/validate_access_token_response_dto.dart
-lib/src/model/validate_library_dto.dart
-lib/src/model/validate_library_import_path_response_dto.dart
-lib/src/model/validate_library_response_dto.dart
-lib/src/model/video_codec.dart
-lib/src/serializers.dart
-pubspec.yaml
diff --git a/mobile-v2/openapi/.openapi-generator/VERSION b/mobile-v2/openapi/.openapi-generator/VERSION
deleted file mode 100644
index 4b49d9bb63..0000000000
--- a/mobile-v2/openapi/.openapi-generator/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-7.2.0
\ No newline at end of file
diff --git a/mobile-v2/openapi/README.md b/mobile-v2/openapi/README.md
deleted file mode 100644
index c62219a1d0..0000000000
--- a/mobile-v2/openapi/README.md
+++ /dev/null
@@ -1,427 +0,0 @@
-# openapi (EXPERIMENTAL)
-Immich API
-
-This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-
-- API version: 1.102.3
-- Build package: org.openapitools.codegen.languages.DartDioClientCodegen
-
-## Requirements
-
-* Dart 2.15.0+ or Flutter 2.8.0+
-* Dio 5.0.0+ (https://pub.dev/packages/dio)
-
-## Installation & Usage
-
-### pub.dev
-To use the package from [pub.dev](https://pub.dev), please include the following in pubspec.yaml
-```yaml
-dependencies:
- openapi: 1.0.0
-```
-
-### Github
-If this Dart package is published to Github, please include the following in pubspec.yaml
-```yaml
-dependencies:
- openapi:
- git:
- url: https://github.com/GIT_USER_ID/GIT_REPO_ID.git
- #ref: main
-```
-
-### Local development
-To use the package from your local drive, please include the following in pubspec.yaml
-```yaml
-dependencies:
- openapi:
- path: /path/to/openapi
-```
-
-## Getting Started
-
-Please follow the [installation procedure](#installation--usage) and then run the following:
-
-```dart
-import 'package:openapi/openapi.dart';
-
-
-final api = Openapi().getAPIKeyApi();
-final APIKeyCreateDto aPIKeyCreateDto = ; // APIKeyCreateDto |
-
-try {
- final response = await api.createApiKey(aPIKeyCreateDto);
- print(response);
-} catch on DioException (e) {
- print("Exception when calling APIKeyApi->createApiKey: $e\n");
-}
-
-```
-
-## Documentation for API Endpoints
-
-All URIs are relative to */api*
-
-Class | Method | HTTP request | Description
------------- | ------------- | ------------- | -------------
-[*APIKeyApi*](doc/APIKeyApi.md) | [**createApiKey**](doc/APIKeyApi.md#createapikey) | **POST** /api-key |
-[*APIKeyApi*](doc/APIKeyApi.md) | [**deleteApiKey**](doc/APIKeyApi.md#deleteapikey) | **DELETE** /api-key/{id} |
-[*APIKeyApi*](doc/APIKeyApi.md) | [**getApiKey**](doc/APIKeyApi.md#getapikey) | **GET** /api-key/{id} |
-[*APIKeyApi*](doc/APIKeyApi.md) | [**getApiKeys**](doc/APIKeyApi.md#getapikeys) | **GET** /api-key |
-[*APIKeyApi*](doc/APIKeyApi.md) | [**updateApiKey**](doc/APIKeyApi.md#updateapikey) | **PUT** /api-key/{id} |
-[*ActivityApi*](doc/ActivityApi.md) | [**createActivity**](doc/ActivityApi.md#createactivity) | **POST** /activity |
-[*ActivityApi*](doc/ActivityApi.md) | [**deleteActivity**](doc/ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
-[*ActivityApi*](doc/ActivityApi.md) | [**getActivities**](doc/ActivityApi.md#getactivities) | **GET** /activity |
-[*ActivityApi*](doc/ActivityApi.md) | [**getActivityStatistics**](doc/ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
-[*AlbumApi*](doc/AlbumApi.md) | [**addAssetsToAlbum**](doc/AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
-[*AlbumApi*](doc/AlbumApi.md) | [**addUsersToAlbum**](doc/AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
-[*AlbumApi*](doc/AlbumApi.md) | [**createAlbum**](doc/AlbumApi.md#createalbum) | **POST** /album |
-[*AlbumApi*](doc/AlbumApi.md) | [**deleteAlbum**](doc/AlbumApi.md#deletealbum) | **DELETE** /album/{id} |
-[*AlbumApi*](doc/AlbumApi.md) | [**getAlbumCount**](doc/AlbumApi.md#getalbumcount) | **GET** /album/count |
-[*AlbumApi*](doc/AlbumApi.md) | [**getAlbumInfo**](doc/AlbumApi.md#getalbuminfo) | **GET** /album/{id} |
-[*AlbumApi*](doc/AlbumApi.md) | [**getAllAlbums**](doc/AlbumApi.md#getallalbums) | **GET** /album |
-[*AlbumApi*](doc/AlbumApi.md) | [**removeAssetFromAlbum**](doc/AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
-[*AlbumApi*](doc/AlbumApi.md) | [**removeUserFromAlbum**](doc/AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
-[*AlbumApi*](doc/AlbumApi.md) | [**updateAlbumInfo**](doc/AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |
-[*AssetApi*](doc/AssetApi.md) | [**checkBulkUpload**](doc/AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check |
-[*AssetApi*](doc/AssetApi.md) | [**checkExistingAssets**](doc/AssetApi.md#checkexistingassets) | **POST** /asset/exist |
-[*AssetApi*](doc/AssetApi.md) | [**deleteAssets**](doc/AssetApi.md#deleteassets) | **DELETE** /asset |
-[*AssetApi*](doc/AssetApi.md) | [**getAllAssets**](doc/AssetApi.md#getallassets) | **GET** /asset |
-[*AssetApi*](doc/AssetApi.md) | [**getAllUserAssetsByDeviceId**](doc/AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} |
-[*AssetApi*](doc/AssetApi.md) | [**getAssetInfo**](doc/AssetApi.md#getassetinfo) | **GET** /asset/{id} |
-[*AssetApi*](doc/AssetApi.md) | [**getAssetSearchTerms**](doc/AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
-[*AssetApi*](doc/AssetApi.md) | [**getAssetStatistics**](doc/AssetApi.md#getassetstatistics) | **GET** /asset/statistics |
-[*AssetApi*](doc/AssetApi.md) | [**getAssetThumbnail**](doc/AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} |
-[*AssetApi*](doc/AssetApi.md) | [**getCuratedLocations**](doc/AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
-[*AssetApi*](doc/AssetApi.md) | [**getCuratedObjects**](doc/AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
-[*AssetApi*](doc/AssetApi.md) | [**getMapMarkers**](doc/AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
-[*AssetApi*](doc/AssetApi.md) | [**getMemoryLane**](doc/AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
-[*AssetApi*](doc/AssetApi.md) | [**getRandom**](doc/AssetApi.md#getrandom) | **GET** /asset/random |
-[*AssetApi*](doc/AssetApi.md) | [**runAssetJobs**](doc/AssetApi.md#runassetjobs) | **POST** /asset/jobs |
-[*AssetApi*](doc/AssetApi.md) | [**searchAssets**](doc/AssetApi.md#searchassets) | **GET** /assets |
-[*AssetApi*](doc/AssetApi.md) | [**serveFile**](doc/AssetApi.md#servefile) | **GET** /asset/file/{id} |
-[*AssetApi*](doc/AssetApi.md) | [**updateAsset**](doc/AssetApi.md#updateasset) | **PUT** /asset/{id} |
-[*AssetApi*](doc/AssetApi.md) | [**updateAssets**](doc/AssetApi.md#updateassets) | **PUT** /asset |
-[*AssetApi*](doc/AssetApi.md) | [**updateStackParent**](doc/AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
-[*AssetApi*](doc/AssetApi.md) | [**uploadFile**](doc/AssetApi.md#uploadfile) | **POST** /asset/upload |
-[*AuditApi*](doc/AuditApi.md) | [**getAuditDeletes**](doc/AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
-[*AuthenticationApi*](doc/AuthenticationApi.md) | [**changePassword**](doc/AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
-[*AuthenticationApi*](doc/AuthenticationApi.md) | [**login**](doc/AuthenticationApi.md#login) | **POST** /auth/login |
-[*AuthenticationApi*](doc/AuthenticationApi.md) | [**logout**](doc/AuthenticationApi.md#logout) | **POST** /auth/logout |
-[*AuthenticationApi*](doc/AuthenticationApi.md) | [**signUpAdmin**](doc/AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
-[*AuthenticationApi*](doc/AuthenticationApi.md) | [**validateAccessToken**](doc/AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
-[*DownloadApi*](doc/DownloadApi.md) | [**downloadArchive**](doc/DownloadApi.md#downloadarchive) | **POST** /download/archive |
-[*DownloadApi*](doc/DownloadApi.md) | [**downloadFile**](doc/DownloadApi.md#downloadfile) | **POST** /download/asset/{id} |
-[*DownloadApi*](doc/DownloadApi.md) | [**getDownloadInfo**](doc/DownloadApi.md#getdownloadinfo) | **POST** /download/info |
-[*FaceApi*](doc/FaceApi.md) | [**getFaces**](doc/FaceApi.md#getfaces) | **GET** /face |
-[*FaceApi*](doc/FaceApi.md) | [**reassignFacesById**](doc/FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
-[*FileReportApi*](doc/FileReportApi.md) | [**fixAuditFiles**](doc/FileReportApi.md#fixauditfiles) | **POST** /report/fix |
-[*FileReportApi*](doc/FileReportApi.md) | [**getAuditFiles**](doc/FileReportApi.md#getauditfiles) | **GET** /report |
-[*FileReportApi*](doc/FileReportApi.md) | [**getFileChecksums**](doc/FileReportApi.md#getfilechecksums) | **POST** /report/checksum |
-[*JobApi*](doc/JobApi.md) | [**getAllJobsStatus**](doc/JobApi.md#getalljobsstatus) | **GET** /jobs |
-[*JobApi*](doc/JobApi.md) | [**sendJobCommand**](doc/JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
-[*LibraryApi*](doc/LibraryApi.md) | [**createLibrary**](doc/LibraryApi.md#createlibrary) | **POST** /library |
-[*LibraryApi*](doc/LibraryApi.md) | [**deleteLibrary**](doc/LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
-[*LibraryApi*](doc/LibraryApi.md) | [**getAllLibraries**](doc/LibraryApi.md#getalllibraries) | **GET** /library |
-[*LibraryApi*](doc/LibraryApi.md) | [**getLibrary**](doc/LibraryApi.md#getlibrary) | **GET** /library/{id} |
-[*LibraryApi*](doc/LibraryApi.md) | [**getLibraryStatistics**](doc/LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
-[*LibraryApi*](doc/LibraryApi.md) | [**removeOfflineFiles**](doc/LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
-[*LibraryApi*](doc/LibraryApi.md) | [**scanLibrary**](doc/LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
-[*LibraryApi*](doc/LibraryApi.md) | [**updateLibrary**](doc/LibraryApi.md#updatelibrary) | **PUT** /library/{id} |
-[*LibraryApi*](doc/LibraryApi.md) | [**validate**](doc/LibraryApi.md#validate) | **POST** /library/{id}/validate |
-[*MemoryApi*](doc/MemoryApi.md) | [**addMemoryAssets**](doc/MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
-[*MemoryApi*](doc/MemoryApi.md) | [**createMemory**](doc/MemoryApi.md#creatememory) | **POST** /memories |
-[*MemoryApi*](doc/MemoryApi.md) | [**deleteMemory**](doc/MemoryApi.md#deletememory) | **DELETE** /memories/{id} |
-[*MemoryApi*](doc/MemoryApi.md) | [**getMemory**](doc/MemoryApi.md#getmemory) | **GET** /memories/{id} |
-[*MemoryApi*](doc/MemoryApi.md) | [**removeMemoryAssets**](doc/MemoryApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
-[*MemoryApi*](doc/MemoryApi.md) | [**searchMemories**](doc/MemoryApi.md#searchmemories) | **GET** /memories |
-[*MemoryApi*](doc/MemoryApi.md) | [**updateMemory**](doc/MemoryApi.md#updatememory) | **PUT** /memories/{id} |
-[*OAuthApi*](doc/OAuthApi.md) | [**finishOAuth**](doc/OAuthApi.md#finishoauth) | **POST** /oauth/callback |
-[*OAuthApi*](doc/OAuthApi.md) | [**linkOAuthAccount**](doc/OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
-[*OAuthApi*](doc/OAuthApi.md) | [**redirectOAuthToMobile**](doc/OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
-[*OAuthApi*](doc/OAuthApi.md) | [**startOAuth**](doc/OAuthApi.md#startoauth) | **POST** /oauth/authorize |
-[*OAuthApi*](doc/OAuthApi.md) | [**unlinkOAuthAccount**](doc/OAuthApi.md#unlinkoauthaccount) | **POST** /oauth/unlink |
-[*PartnerApi*](doc/PartnerApi.md) | [**createPartner**](doc/PartnerApi.md#createpartner) | **POST** /partner/{id} |
-[*PartnerApi*](doc/PartnerApi.md) | [**getPartners**](doc/PartnerApi.md#getpartners) | **GET** /partner |
-[*PartnerApi*](doc/PartnerApi.md) | [**removePartner**](doc/PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
-[*PartnerApi*](doc/PartnerApi.md) | [**updatePartner**](doc/PartnerApi.md#updatepartner) | **PUT** /partner/{id} |
-[*PersonApi*](doc/PersonApi.md) | [**createPerson**](doc/PersonApi.md#createperson) | **POST** /person |
-[*PersonApi*](doc/PersonApi.md) | [**getAllPeople**](doc/PersonApi.md#getallpeople) | **GET** /person |
-[*PersonApi*](doc/PersonApi.md) | [**getPerson**](doc/PersonApi.md#getperson) | **GET** /person/{id} |
-[*PersonApi*](doc/PersonApi.md) | [**getPersonAssets**](doc/PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
-[*PersonApi*](doc/PersonApi.md) | [**getPersonStatistics**](doc/PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
-[*PersonApi*](doc/PersonApi.md) | [**getPersonThumbnail**](doc/PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
-[*PersonApi*](doc/PersonApi.md) | [**mergePerson**](doc/PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
-[*PersonApi*](doc/PersonApi.md) | [**reassignFaces**](doc/PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
-[*PersonApi*](doc/PersonApi.md) | [**updatePeople**](doc/PersonApi.md#updatepeople) | **PUT** /person |
-[*PersonApi*](doc/PersonApi.md) | [**updatePerson**](doc/PersonApi.md#updateperson) | **PUT** /person/{id} |
-[*SearchApi*](doc/SearchApi.md) | [**getAssetsByCity**](doc/SearchApi.md#getassetsbycity) | **GET** /search/cities |
-[*SearchApi*](doc/SearchApi.md) | [**getExploreData**](doc/SearchApi.md#getexploredata) | **GET** /search/explore |
-[*SearchApi*](doc/SearchApi.md) | [**getSearchSuggestions**](doc/SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
-[*SearchApi*](doc/SearchApi.md) | [**search**](doc/SearchApi.md#search) | **GET** /search |
-[*SearchApi*](doc/SearchApi.md) | [**searchMetadata**](doc/SearchApi.md#searchmetadata) | **POST** /search/metadata |
-[*SearchApi*](doc/SearchApi.md) | [**searchPerson**](doc/SearchApi.md#searchperson) | **GET** /search/person |
-[*SearchApi*](doc/SearchApi.md) | [**searchPlaces**](doc/SearchApi.md#searchplaces) | **GET** /search/places |
-[*SearchApi*](doc/SearchApi.md) | [**searchSmart**](doc/SearchApi.md#searchsmart) | **POST** /search/smart |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getServerConfig**](doc/ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getServerFeatures**](doc/ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getServerInfo**](doc/ServerInfoApi.md#getserverinfo) | **GET** /server-info |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getServerStatistics**](doc/ServerInfoApi.md#getserverstatistics) | **GET** /server-info/statistics |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getServerVersion**](doc/ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getSupportedMediaTypes**](doc/ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**getTheme**](doc/ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
-[*ServerInfoApi*](doc/ServerInfoApi.md) | [**pingServer**](doc/ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
-[*SessionsApi*](doc/SessionsApi.md) | [**deleteAllSessions**](doc/SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
-[*SessionsApi*](doc/SessionsApi.md) | [**deleteSession**](doc/SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
-[*SessionsApi*](doc/SessionsApi.md) | [**getSessions**](doc/SessionsApi.md#getsessions) | **GET** /sessions |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**addSharedLinkAssets**](doc/SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**createSharedLink**](doc/SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**getAllSharedLinks**](doc/SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**getMySharedLink**](doc/SharedLinkApi.md#getmysharedlink) | **GET** /shared-link/me |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**getSharedLinkById**](doc/SharedLinkApi.md#getsharedlinkbyid) | **GET** /shared-link/{id} |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**removeSharedLink**](doc/SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**removeSharedLinkAssets**](doc/SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
-[*SharedLinkApi*](doc/SharedLinkApi.md) | [**updateSharedLink**](doc/SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
-[*SyncApi*](doc/SyncApi.md) | [**getAllForUserFullSync**](doc/SyncApi.md#getallforuserfullsync) | **GET** /sync/full-sync |
-[*SyncApi*](doc/SyncApi.md) | [**getDeltaSync**](doc/SyncApi.md#getdeltasync) | **GET** /sync/delta-sync |
-[*SystemConfigApi*](doc/SystemConfigApi.md) | [**getConfig**](doc/SystemConfigApi.md#getconfig) | **GET** /system-config |
-[*SystemConfigApi*](doc/SystemConfigApi.md) | [**getConfigDefaults**](doc/SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
-[*SystemConfigApi*](doc/SystemConfigApi.md) | [**getMapStyle**](doc/SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json |
-[*SystemConfigApi*](doc/SystemConfigApi.md) | [**getStorageTemplateOptions**](doc/SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
-[*SystemConfigApi*](doc/SystemConfigApi.md) | [**updateConfig**](doc/SystemConfigApi.md#updateconfig) | **PUT** /system-config |
-[*SystemMetadataApi*](doc/SystemMetadataApi.md) | [**getAdminOnboarding**](doc/SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
-[*SystemMetadataApi*](doc/SystemMetadataApi.md) | [**getReverseGeocodingState**](doc/SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state |
-[*SystemMetadataApi*](doc/SystemMetadataApi.md) | [**updateAdminOnboarding**](doc/SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding |
-[*TagApi*](doc/TagApi.md) | [**createTag**](doc/TagApi.md#createtag) | **POST** /tag |
-[*TagApi*](doc/TagApi.md) | [**deleteTag**](doc/TagApi.md#deletetag) | **DELETE** /tag/{id} |
-[*TagApi*](doc/TagApi.md) | [**getAllTags**](doc/TagApi.md#getalltags) | **GET** /tag |
-[*TagApi*](doc/TagApi.md) | [**getTagAssets**](doc/TagApi.md#gettagassets) | **GET** /tag/{id}/assets |
-[*TagApi*](doc/TagApi.md) | [**getTagById**](doc/TagApi.md#gettagbyid) | **GET** /tag/{id} |
-[*TagApi*](doc/TagApi.md) | [**tagAssets**](doc/TagApi.md#tagassets) | **PUT** /tag/{id}/assets |
-[*TagApi*](doc/TagApi.md) | [**untagAssets**](doc/TagApi.md#untagassets) | **DELETE** /tag/{id}/assets |
-[*TagApi*](doc/TagApi.md) | [**updateTag**](doc/TagApi.md#updatetag) | **PATCH** /tag/{id} |
-[*TimelineApi*](doc/TimelineApi.md) | [**getTimeBucket**](doc/TimelineApi.md#gettimebucket) | **GET** /timeline/bucket |
-[*TimelineApi*](doc/TimelineApi.md) | [**getTimeBuckets**](doc/TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets |
-[*TrashApi*](doc/TrashApi.md) | [**emptyTrash**](doc/TrashApi.md#emptytrash) | **POST** /trash/empty |
-[*TrashApi*](doc/TrashApi.md) | [**restoreAssets**](doc/TrashApi.md#restoreassets) | **POST** /trash/restore/assets |
-[*TrashApi*](doc/TrashApi.md) | [**restoreTrash**](doc/TrashApi.md#restoretrash) | **POST** /trash/restore |
-[*UserApi*](doc/UserApi.md) | [**createProfileImage**](doc/UserApi.md#createprofileimage) | **POST** /user/profile-image |
-[*UserApi*](doc/UserApi.md) | [**createUser**](doc/UserApi.md#createuser) | **POST** /user |
-[*UserApi*](doc/UserApi.md) | [**deleteProfileImage**](doc/UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image |
-[*UserApi*](doc/UserApi.md) | [**deleteUser**](doc/UserApi.md#deleteuser) | **DELETE** /user/{id} |
-[*UserApi*](doc/UserApi.md) | [**getAllUsers**](doc/UserApi.md#getallusers) | **GET** /user |
-[*UserApi*](doc/UserApi.md) | [**getMyUserInfo**](doc/UserApi.md#getmyuserinfo) | **GET** /user/me |
-[*UserApi*](doc/UserApi.md) | [**getProfileImage**](doc/UserApi.md#getprofileimage) | **GET** /user/profile-image/{id} |
-[*UserApi*](doc/UserApi.md) | [**getUserById**](doc/UserApi.md#getuserbyid) | **GET** /user/info/{id} |
-[*UserApi*](doc/UserApi.md) | [**restoreUser**](doc/UserApi.md#restoreuser) | **POST** /user/{id}/restore |
-[*UserApi*](doc/UserApi.md) | [**updateUser**](doc/UserApi.md#updateuser) | **PUT** /user |
-
-
-## Documentation For Models
-
- - [APIKeyCreateDto](doc/APIKeyCreateDto.md)
- - [APIKeyCreateResponseDto](doc/APIKeyCreateResponseDto.md)
- - [APIKeyResponseDto](doc/APIKeyResponseDto.md)
- - [APIKeyUpdateDto](doc/APIKeyUpdateDto.md)
- - [ActivityCreateDto](doc/ActivityCreateDto.md)
- - [ActivityResponseDto](doc/ActivityResponseDto.md)
- - [ActivityStatisticsResponseDto](doc/ActivityStatisticsResponseDto.md)
- - [AddUsersDto](doc/AddUsersDto.md)
- - [AdminOnboardingUpdateDto](doc/AdminOnboardingUpdateDto.md)
- - [AlbumCountResponseDto](doc/AlbumCountResponseDto.md)
- - [AlbumResponseDto](doc/AlbumResponseDto.md)
- - [AllJobStatusResponseDto](doc/AllJobStatusResponseDto.md)
- - [AssetBulkDeleteDto](doc/AssetBulkDeleteDto.md)
- - [AssetBulkUpdateDto](doc/AssetBulkUpdateDto.md)
- - [AssetBulkUploadCheckDto](doc/AssetBulkUploadCheckDto.md)
- - [AssetBulkUploadCheckItem](doc/AssetBulkUploadCheckItem.md)
- - [AssetBulkUploadCheckResponseDto](doc/AssetBulkUploadCheckResponseDto.md)
- - [AssetBulkUploadCheckResult](doc/AssetBulkUploadCheckResult.md)
- - [AssetDeltaSyncResponseDto](doc/AssetDeltaSyncResponseDto.md)
- - [AssetFaceResponseDto](doc/AssetFaceResponseDto.md)
- - [AssetFaceUpdateDto](doc/AssetFaceUpdateDto.md)
- - [AssetFaceUpdateItem](doc/AssetFaceUpdateItem.md)
- - [AssetFaceWithoutPersonResponseDto](doc/AssetFaceWithoutPersonResponseDto.md)
- - [AssetFileUploadResponseDto](doc/AssetFileUploadResponseDto.md)
- - [AssetIdsDto](doc/AssetIdsDto.md)
- - [AssetIdsResponseDto](doc/AssetIdsResponseDto.md)
- - [AssetJobName](doc/AssetJobName.md)
- - [AssetJobsDto](doc/AssetJobsDto.md)
- - [AssetOrder](doc/AssetOrder.md)
- - [AssetResponseDto](doc/AssetResponseDto.md)
- - [AssetStatsResponseDto](doc/AssetStatsResponseDto.md)
- - [AssetTypeEnum](doc/AssetTypeEnum.md)
- - [AudioCodec](doc/AudioCodec.md)
- - [AuditDeletesResponseDto](doc/AuditDeletesResponseDto.md)
- - [BulkIdResponseDto](doc/BulkIdResponseDto.md)
- - [BulkIdsDto](doc/BulkIdsDto.md)
- - [CLIPConfig](doc/CLIPConfig.md)
- - [CLIPMode](doc/CLIPMode.md)
- - [CQMode](doc/CQMode.md)
- - [ChangePasswordDto](doc/ChangePasswordDto.md)
- - [CheckExistingAssetsDto](doc/CheckExistingAssetsDto.md)
- - [CheckExistingAssetsResponseDto](doc/CheckExistingAssetsResponseDto.md)
- - [Colorspace](doc/Colorspace.md)
- - [CreateAlbumDto](doc/CreateAlbumDto.md)
- - [CreateAssetDto](doc/CreateAssetDto.md)
- - [CreateLibraryDto](doc/CreateLibraryDto.md)
- - [CreateProfileImageDto](doc/CreateProfileImageDto.md)
- - [CreateProfileImageResponseDto](doc/CreateProfileImageResponseDto.md)
- - [CreateTagDto](doc/CreateTagDto.md)
- - [CreateUserDto](doc/CreateUserDto.md)
- - [CuratedLocationsResponseDto](doc/CuratedLocationsResponseDto.md)
- - [CuratedObjectsResponseDto](doc/CuratedObjectsResponseDto.md)
- - [DeleteUserDto](doc/DeleteUserDto.md)
- - [DownloadArchiveInfo](doc/DownloadArchiveInfo.md)
- - [DownloadInfoDto](doc/DownloadInfoDto.md)
- - [DownloadResponseDto](doc/DownloadResponseDto.md)
- - [EntityType](doc/EntityType.md)
- - [ExifResponseDto](doc/ExifResponseDto.md)
- - [FaceDto](doc/FaceDto.md)
- - [FileChecksumDto](doc/FileChecksumDto.md)
- - [FileChecksumResponseDto](doc/FileChecksumResponseDto.md)
- - [FileReportDto](doc/FileReportDto.md)
- - [FileReportFixDto](doc/FileReportFixDto.md)
- - [FileReportItemDto](doc/FileReportItemDto.md)
- - [ImageFormat](doc/ImageFormat.md)
- - [JobCommand](doc/JobCommand.md)
- - [JobCommandDto](doc/JobCommandDto.md)
- - [JobCountsDto](doc/JobCountsDto.md)
- - [JobName](doc/JobName.md)
- - [JobSettingsDto](doc/JobSettingsDto.md)
- - [JobStatusDto](doc/JobStatusDto.md)
- - [LibraryResponseDto](doc/LibraryResponseDto.md)
- - [LibraryStatsResponseDto](doc/LibraryStatsResponseDto.md)
- - [LibraryType](doc/LibraryType.md)
- - [LogLevel](doc/LogLevel.md)
- - [LoginCredentialDto](doc/LoginCredentialDto.md)
- - [LoginResponseDto](doc/LoginResponseDto.md)
- - [LogoutResponseDto](doc/LogoutResponseDto.md)
- - [MapMarkerResponseDto](doc/MapMarkerResponseDto.md)
- - [MapTheme](doc/MapTheme.md)
- - [MemoryCreateDto](doc/MemoryCreateDto.md)
- - [MemoryLaneResponseDto](doc/MemoryLaneResponseDto.md)
- - [MemoryResponseDto](doc/MemoryResponseDto.md)
- - [MemoryType](doc/MemoryType.md)
- - [MemoryUpdateDto](doc/MemoryUpdateDto.md)
- - [MergePersonDto](doc/MergePersonDto.md)
- - [MetadataSearchDto](doc/MetadataSearchDto.md)
- - [ModelType](doc/ModelType.md)
- - [OAuthAuthorizeResponseDto](doc/OAuthAuthorizeResponseDto.md)
- - [OAuthCallbackDto](doc/OAuthCallbackDto.md)
- - [OAuthConfigDto](doc/OAuthConfigDto.md)
- - [OnThisDayDto](doc/OnThisDayDto.md)
- - [PartnerResponseDto](doc/PartnerResponseDto.md)
- - [PathEntityType](doc/PathEntityType.md)
- - [PathType](doc/PathType.md)
- - [PeopleResponseDto](doc/PeopleResponseDto.md)
- - [PeopleUpdateDto](doc/PeopleUpdateDto.md)
- - [PeopleUpdateItem](doc/PeopleUpdateItem.md)
- - [PersonCreateDto](doc/PersonCreateDto.md)
- - [PersonResponseDto](doc/PersonResponseDto.md)
- - [PersonStatisticsResponseDto](doc/PersonStatisticsResponseDto.md)
- - [PersonUpdateDto](doc/PersonUpdateDto.md)
- - [PersonWithFacesResponseDto](doc/PersonWithFacesResponseDto.md)
- - [PlacesResponseDto](doc/PlacesResponseDto.md)
- - [QueueStatusDto](doc/QueueStatusDto.md)
- - [ReactionLevel](doc/ReactionLevel.md)
- - [ReactionType](doc/ReactionType.md)
- - [RecognitionConfig](doc/RecognitionConfig.md)
- - [ReverseGeocodingStateResponseDto](doc/ReverseGeocodingStateResponseDto.md)
- - [ScanLibraryDto](doc/ScanLibraryDto.md)
- - [SearchAlbumResponseDto](doc/SearchAlbumResponseDto.md)
- - [SearchAssetResponseDto](doc/SearchAssetResponseDto.md)
- - [SearchExploreItem](doc/SearchExploreItem.md)
- - [SearchExploreResponseDto](doc/SearchExploreResponseDto.md)
- - [SearchFacetCountResponseDto](doc/SearchFacetCountResponseDto.md)
- - [SearchFacetResponseDto](doc/SearchFacetResponseDto.md)
- - [SearchResponseDto](doc/SearchResponseDto.md)
- - [SearchSuggestionType](doc/SearchSuggestionType.md)
- - [ServerConfigDto](doc/ServerConfigDto.md)
- - [ServerFeaturesDto](doc/ServerFeaturesDto.md)
- - [ServerInfoResponseDto](doc/ServerInfoResponseDto.md)
- - [ServerMediaTypesResponseDto](doc/ServerMediaTypesResponseDto.md)
- - [ServerPingResponse](doc/ServerPingResponse.md)
- - [ServerStatsResponseDto](doc/ServerStatsResponseDto.md)
- - [ServerThemeDto](doc/ServerThemeDto.md)
- - [ServerVersionResponseDto](doc/ServerVersionResponseDto.md)
- - [SessionResponseDto](doc/SessionResponseDto.md)
- - [SharedLinkCreateDto](doc/SharedLinkCreateDto.md)
- - [SharedLinkEditDto](doc/SharedLinkEditDto.md)
- - [SharedLinkResponseDto](doc/SharedLinkResponseDto.md)
- - [SharedLinkType](doc/SharedLinkType.md)
- - [SignUpDto](doc/SignUpDto.md)
- - [SmartInfoResponseDto](doc/SmartInfoResponseDto.md)
- - [SmartSearchDto](doc/SmartSearchDto.md)
- - [SystemConfigDto](doc/SystemConfigDto.md)
- - [SystemConfigFFmpegDto](doc/SystemConfigFFmpegDto.md)
- - [SystemConfigImageDto](doc/SystemConfigImageDto.md)
- - [SystemConfigJobDto](doc/SystemConfigJobDto.md)
- - [SystemConfigLibraryDto](doc/SystemConfigLibraryDto.md)
- - [SystemConfigLibraryScanDto](doc/SystemConfigLibraryScanDto.md)
- - [SystemConfigLibraryWatchDto](doc/SystemConfigLibraryWatchDto.md)
- - [SystemConfigLoggingDto](doc/SystemConfigLoggingDto.md)
- - [SystemConfigMachineLearningDto](doc/SystemConfigMachineLearningDto.md)
- - [SystemConfigMapDto](doc/SystemConfigMapDto.md)
- - [SystemConfigNewVersionCheckDto](doc/SystemConfigNewVersionCheckDto.md)
- - [SystemConfigOAuthDto](doc/SystemConfigOAuthDto.md)
- - [SystemConfigPasswordLoginDto](doc/SystemConfigPasswordLoginDto.md)
- - [SystemConfigReverseGeocodingDto](doc/SystemConfigReverseGeocodingDto.md)
- - [SystemConfigServerDto](doc/SystemConfigServerDto.md)
- - [SystemConfigStorageTemplateDto](doc/SystemConfigStorageTemplateDto.md)
- - [SystemConfigTemplateStorageOptionDto](doc/SystemConfigTemplateStorageOptionDto.md)
- - [SystemConfigThemeDto](doc/SystemConfigThemeDto.md)
- - [SystemConfigTrashDto](doc/SystemConfigTrashDto.md)
- - [SystemConfigUserDto](doc/SystemConfigUserDto.md)
- - [TagResponseDto](doc/TagResponseDto.md)
- - [TagTypeEnum](doc/TagTypeEnum.md)
- - [ThumbnailFormat](doc/ThumbnailFormat.md)
- - [TimeBucketResponseDto](doc/TimeBucketResponseDto.md)
- - [TimeBucketSize](doc/TimeBucketSize.md)
- - [ToneMapping](doc/ToneMapping.md)
- - [TranscodeHWAccel](doc/TranscodeHWAccel.md)
- - [TranscodePolicy](doc/TranscodePolicy.md)
- - [UpdateAlbumDto](doc/UpdateAlbumDto.md)
- - [UpdateAssetDto](doc/UpdateAssetDto.md)
- - [UpdateLibraryDto](doc/UpdateLibraryDto.md)
- - [UpdatePartnerDto](doc/UpdatePartnerDto.md)
- - [UpdateStackParentDto](doc/UpdateStackParentDto.md)
- - [UpdateTagDto](doc/UpdateTagDto.md)
- - [UpdateUserDto](doc/UpdateUserDto.md)
- - [UsageByUserDto](doc/UsageByUserDto.md)
- - [UserAvatarColor](doc/UserAvatarColor.md)
- - [UserDto](doc/UserDto.md)
- - [UserResponseDto](doc/UserResponseDto.md)
- - [UserStatus](doc/UserStatus.md)
- - [ValidateAccessTokenResponseDto](doc/ValidateAccessTokenResponseDto.md)
- - [ValidateLibraryDto](doc/ValidateLibraryDto.md)
- - [ValidateLibraryImportPathResponseDto](doc/ValidateLibraryImportPathResponseDto.md)
- - [ValidateLibraryResponseDto](doc/ValidateLibraryResponseDto.md)
- - [VideoCodec](doc/VideoCodec.md)
-
-
-## Documentation For Authorization
-
-
-Authentication schemes defined for the API:
-### bearer
-
-- **Type**: HTTP Bearer Token authentication (JWT)
-
-### cookie
-
-- **Type**: API key
-- **API key parameter name**: immich_access_token
-- **Location**:
-
-### api_key
-
-- **Type**: API key
-- **API key parameter name**: x-api-key
-- **Location**: HTTP header
-
-
-## Author
-
-
-
diff --git a/mobile-v2/openapi/lib/openapi.dart b/mobile-v2/openapi/lib/openapi.dart
deleted file mode 100644
index bb60b4ce8d..0000000000
--- a/mobile-v2/openapi/lib/openapi.dart
+++ /dev/null
@@ -1,218 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-
-export 'package:openapi/src/api.dart';
-export 'package:openapi/src/auth/api_key_auth.dart';
-export 'package:openapi/src/auth/basic_auth.dart';
-export 'package:openapi/src/auth/oauth.dart';
-export 'package:openapi/src/serializers.dart';
-export 'package:openapi/src/model/date.dart';
-
-export 'package:openapi/src/api/api_key_api.dart';
-export 'package:openapi/src/api/activity_api.dart';
-export 'package:openapi/src/api/album_api.dart';
-export 'package:openapi/src/api/asset_api.dart';
-export 'package:openapi/src/api/audit_api.dart';
-export 'package:openapi/src/api/authentication_api.dart';
-export 'package:openapi/src/api/download_api.dart';
-export 'package:openapi/src/api/face_api.dart';
-export 'package:openapi/src/api/file_report_api.dart';
-export 'package:openapi/src/api/job_api.dart';
-export 'package:openapi/src/api/library_api.dart';
-export 'package:openapi/src/api/memory_api.dart';
-export 'package:openapi/src/api/o_auth_api.dart';
-export 'package:openapi/src/api/partner_api.dart';
-export 'package:openapi/src/api/person_api.dart';
-export 'package:openapi/src/api/search_api.dart';
-export 'package:openapi/src/api/server_info_api.dart';
-export 'package:openapi/src/api/sessions_api.dart';
-export 'package:openapi/src/api/shared_link_api.dart';
-export 'package:openapi/src/api/sync_api.dart';
-export 'package:openapi/src/api/system_config_api.dart';
-export 'package:openapi/src/api/system_metadata_api.dart';
-export 'package:openapi/src/api/tag_api.dart';
-export 'package:openapi/src/api/timeline_api.dart';
-export 'package:openapi/src/api/trash_api.dart';
-export 'package:openapi/src/api/user_api.dart';
-
-export 'package:openapi/src/model/api_key_create_dto.dart';
-export 'package:openapi/src/model/api_key_create_response_dto.dart';
-export 'package:openapi/src/model/api_key_response_dto.dart';
-export 'package:openapi/src/model/api_key_update_dto.dart';
-export 'package:openapi/src/model/activity_create_dto.dart';
-export 'package:openapi/src/model/activity_response_dto.dart';
-export 'package:openapi/src/model/activity_statistics_response_dto.dart';
-export 'package:openapi/src/model/add_users_dto.dart';
-export 'package:openapi/src/model/admin_onboarding_update_dto.dart';
-export 'package:openapi/src/model/album_count_response_dto.dart';
-export 'package:openapi/src/model/album_response_dto.dart';
-export 'package:openapi/src/model/all_job_status_response_dto.dart';
-export 'package:openapi/src/model/asset_bulk_delete_dto.dart';
-export 'package:openapi/src/model/asset_bulk_update_dto.dart';
-export 'package:openapi/src/model/asset_bulk_upload_check_dto.dart';
-export 'package:openapi/src/model/asset_bulk_upload_check_item.dart';
-export 'package:openapi/src/model/asset_bulk_upload_check_response_dto.dart';
-export 'package:openapi/src/model/asset_bulk_upload_check_result.dart';
-export 'package:openapi/src/model/asset_delta_sync_response_dto.dart';
-export 'package:openapi/src/model/asset_face_response_dto.dart';
-export 'package:openapi/src/model/asset_face_update_dto.dart';
-export 'package:openapi/src/model/asset_face_update_item.dart';
-export 'package:openapi/src/model/asset_face_without_person_response_dto.dart';
-export 'package:openapi/src/model/asset_file_upload_response_dto.dart';
-export 'package:openapi/src/model/asset_ids_dto.dart';
-export 'package:openapi/src/model/asset_ids_response_dto.dart';
-export 'package:openapi/src/model/asset_job_name.dart';
-export 'package:openapi/src/model/asset_jobs_dto.dart';
-export 'package:openapi/src/model/asset_order.dart';
-export 'package:openapi/src/model/asset_response_dto.dart';
-export 'package:openapi/src/model/asset_stats_response_dto.dart';
-export 'package:openapi/src/model/asset_type_enum.dart';
-export 'package:openapi/src/model/audio_codec.dart';
-export 'package:openapi/src/model/audit_deletes_response_dto.dart';
-export 'package:openapi/src/model/bulk_id_response_dto.dart';
-export 'package:openapi/src/model/bulk_ids_dto.dart';
-export 'package:openapi/src/model/clip_config.dart';
-export 'package:openapi/src/model/clip_mode.dart';
-export 'package:openapi/src/model/cq_mode.dart';
-export 'package:openapi/src/model/change_password_dto.dart';
-export 'package:openapi/src/model/check_existing_assets_dto.dart';
-export 'package:openapi/src/model/check_existing_assets_response_dto.dart';
-export 'package:openapi/src/model/colorspace.dart';
-export 'package:openapi/src/model/create_album_dto.dart';
-export 'package:openapi/src/model/create_asset_dto.dart';
-export 'package:openapi/src/model/create_library_dto.dart';
-export 'package:openapi/src/model/create_profile_image_dto.dart';
-export 'package:openapi/src/model/create_profile_image_response_dto.dart';
-export 'package:openapi/src/model/create_tag_dto.dart';
-export 'package:openapi/src/model/create_user_dto.dart';
-export 'package:openapi/src/model/curated_locations_response_dto.dart';
-export 'package:openapi/src/model/curated_objects_response_dto.dart';
-export 'package:openapi/src/model/delete_user_dto.dart';
-export 'package:openapi/src/model/download_archive_info.dart';
-export 'package:openapi/src/model/download_info_dto.dart';
-export 'package:openapi/src/model/download_response_dto.dart';
-export 'package:openapi/src/model/entity_type.dart';
-export 'package:openapi/src/model/exif_response_dto.dart';
-export 'package:openapi/src/model/face_dto.dart';
-export 'package:openapi/src/model/file_checksum_dto.dart';
-export 'package:openapi/src/model/file_checksum_response_dto.dart';
-export 'package:openapi/src/model/file_report_dto.dart';
-export 'package:openapi/src/model/file_report_fix_dto.dart';
-export 'package:openapi/src/model/file_report_item_dto.dart';
-export 'package:openapi/src/model/image_format.dart';
-export 'package:openapi/src/model/job_command.dart';
-export 'package:openapi/src/model/job_command_dto.dart';
-export 'package:openapi/src/model/job_counts_dto.dart';
-export 'package:openapi/src/model/job_name.dart';
-export 'package:openapi/src/model/job_settings_dto.dart';
-export 'package:openapi/src/model/job_status_dto.dart';
-export 'package:openapi/src/model/library_response_dto.dart';
-export 'package:openapi/src/model/library_stats_response_dto.dart';
-export 'package:openapi/src/model/library_type.dart';
-export 'package:openapi/src/model/log_level.dart';
-export 'package:openapi/src/model/login_credential_dto.dart';
-export 'package:openapi/src/model/login_response_dto.dart';
-export 'package:openapi/src/model/logout_response_dto.dart';
-export 'package:openapi/src/model/map_marker_response_dto.dart';
-export 'package:openapi/src/model/map_theme.dart';
-export 'package:openapi/src/model/memory_create_dto.dart';
-export 'package:openapi/src/model/memory_lane_response_dto.dart';
-export 'package:openapi/src/model/memory_response_dto.dart';
-export 'package:openapi/src/model/memory_type.dart';
-export 'package:openapi/src/model/memory_update_dto.dart';
-export 'package:openapi/src/model/merge_person_dto.dart';
-export 'package:openapi/src/model/metadata_search_dto.dart';
-export 'package:openapi/src/model/model_type.dart';
-export 'package:openapi/src/model/o_auth_authorize_response_dto.dart';
-export 'package:openapi/src/model/o_auth_callback_dto.dart';
-export 'package:openapi/src/model/o_auth_config_dto.dart';
-export 'package:openapi/src/model/on_this_day_dto.dart';
-export 'package:openapi/src/model/partner_response_dto.dart';
-export 'package:openapi/src/model/path_entity_type.dart';
-export 'package:openapi/src/model/path_type.dart';
-export 'package:openapi/src/model/people_response_dto.dart';
-export 'package:openapi/src/model/people_update_dto.dart';
-export 'package:openapi/src/model/people_update_item.dart';
-export 'package:openapi/src/model/person_create_dto.dart';
-export 'package:openapi/src/model/person_response_dto.dart';
-export 'package:openapi/src/model/person_statistics_response_dto.dart';
-export 'package:openapi/src/model/person_update_dto.dart';
-export 'package:openapi/src/model/person_with_faces_response_dto.dart';
-export 'package:openapi/src/model/places_response_dto.dart';
-export 'package:openapi/src/model/queue_status_dto.dart';
-export 'package:openapi/src/model/reaction_level.dart';
-export 'package:openapi/src/model/reaction_type.dart';
-export 'package:openapi/src/model/recognition_config.dart';
-export 'package:openapi/src/model/reverse_geocoding_state_response_dto.dart';
-export 'package:openapi/src/model/scan_library_dto.dart';
-export 'package:openapi/src/model/search_album_response_dto.dart';
-export 'package:openapi/src/model/search_asset_response_dto.dart';
-export 'package:openapi/src/model/search_explore_item.dart';
-export 'package:openapi/src/model/search_explore_response_dto.dart';
-export 'package:openapi/src/model/search_facet_count_response_dto.dart';
-export 'package:openapi/src/model/search_facet_response_dto.dart';
-export 'package:openapi/src/model/search_response_dto.dart';
-export 'package:openapi/src/model/search_suggestion_type.dart';
-export 'package:openapi/src/model/server_config_dto.dart';
-export 'package:openapi/src/model/server_features_dto.dart';
-export 'package:openapi/src/model/server_info_response_dto.dart';
-export 'package:openapi/src/model/server_media_types_response_dto.dart';
-export 'package:openapi/src/model/server_ping_response.dart';
-export 'package:openapi/src/model/server_stats_response_dto.dart';
-export 'package:openapi/src/model/server_theme_dto.dart';
-export 'package:openapi/src/model/server_version_response_dto.dart';
-export 'package:openapi/src/model/session_response_dto.dart';
-export 'package:openapi/src/model/shared_link_create_dto.dart';
-export 'package:openapi/src/model/shared_link_edit_dto.dart';
-export 'package:openapi/src/model/shared_link_response_dto.dart';
-export 'package:openapi/src/model/shared_link_type.dart';
-export 'package:openapi/src/model/sign_up_dto.dart';
-export 'package:openapi/src/model/smart_info_response_dto.dart';
-export 'package:openapi/src/model/smart_search_dto.dart';
-export 'package:openapi/src/model/system_config_dto.dart';
-export 'package:openapi/src/model/system_config_f_fmpeg_dto.dart';
-export 'package:openapi/src/model/system_config_image_dto.dart';
-export 'package:openapi/src/model/system_config_job_dto.dart';
-export 'package:openapi/src/model/system_config_library_dto.dart';
-export 'package:openapi/src/model/system_config_library_scan_dto.dart';
-export 'package:openapi/src/model/system_config_library_watch_dto.dart';
-export 'package:openapi/src/model/system_config_logging_dto.dart';
-export 'package:openapi/src/model/system_config_machine_learning_dto.dart';
-export 'package:openapi/src/model/system_config_map_dto.dart';
-export 'package:openapi/src/model/system_config_new_version_check_dto.dart';
-export 'package:openapi/src/model/system_config_o_auth_dto.dart';
-export 'package:openapi/src/model/system_config_password_login_dto.dart';
-export 'package:openapi/src/model/system_config_reverse_geocoding_dto.dart';
-export 'package:openapi/src/model/system_config_server_dto.dart';
-export 'package:openapi/src/model/system_config_storage_template_dto.dart';
-export 'package:openapi/src/model/system_config_template_storage_option_dto.dart';
-export 'package:openapi/src/model/system_config_theme_dto.dart';
-export 'package:openapi/src/model/system_config_trash_dto.dart';
-export 'package:openapi/src/model/system_config_user_dto.dart';
-export 'package:openapi/src/model/tag_response_dto.dart';
-export 'package:openapi/src/model/tag_type_enum.dart';
-export 'package:openapi/src/model/thumbnail_format.dart';
-export 'package:openapi/src/model/time_bucket_response_dto.dart';
-export 'package:openapi/src/model/time_bucket_size.dart';
-export 'package:openapi/src/model/tone_mapping.dart';
-export 'package:openapi/src/model/transcode_hw_accel.dart';
-export 'package:openapi/src/model/transcode_policy.dart';
-export 'package:openapi/src/model/update_album_dto.dart';
-export 'package:openapi/src/model/update_asset_dto.dart';
-export 'package:openapi/src/model/update_library_dto.dart';
-export 'package:openapi/src/model/update_partner_dto.dart';
-export 'package:openapi/src/model/update_stack_parent_dto.dart';
-export 'package:openapi/src/model/update_tag_dto.dart';
-export 'package:openapi/src/model/update_user_dto.dart';
-export 'package:openapi/src/model/usage_by_user_dto.dart';
-export 'package:openapi/src/model/user_avatar_color.dart';
-export 'package:openapi/src/model/user_dto.dart';
-export 'package:openapi/src/model/user_response_dto.dart';
-export 'package:openapi/src/model/user_status.dart';
-export 'package:openapi/src/model/validate_access_token_response_dto.dart';
-export 'package:openapi/src/model/validate_library_dto.dart';
-export 'package:openapi/src/model/validate_library_import_path_response_dto.dart';
-export 'package:openapi/src/model/validate_library_response_dto.dart';
-export 'package:openapi/src/model/video_codec.dart';
-export 'package:openapi/src/auth/bearer_auth.dart';
diff --git a/mobile-v2/openapi/lib/src/api.dart b/mobile-v2/openapi/lib/src/api.dart
deleted file mode 100644
index 796fbe2a42..0000000000
--- a/mobile-v2/openapi/lib/src/api.dart
+++ /dev/null
@@ -1,248 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-
-import 'package:dio/dio.dart';
-import 'package:built_value/serializer.dart';
-import 'package:openapi/src/serializers.dart';
-import 'package:openapi/src/auth/api_key_auth.dart';
-import 'package:openapi/src/auth/basic_auth.dart';
-import 'package:openapi/src/auth/bearer_auth.dart';
-import 'package:openapi/src/auth/oauth.dart';
-import 'package:openapi/src/api/api_key_api.dart';
-import 'package:openapi/src/api/activity_api.dart';
-import 'package:openapi/src/api/album_api.dart';
-import 'package:openapi/src/api/asset_api.dart';
-import 'package:openapi/src/api/audit_api.dart';
-import 'package:openapi/src/api/authentication_api.dart';
-import 'package:openapi/src/api/download_api.dart';
-import 'package:openapi/src/api/face_api.dart';
-import 'package:openapi/src/api/file_report_api.dart';
-import 'package:openapi/src/api/job_api.dart';
-import 'package:openapi/src/api/library_api.dart';
-import 'package:openapi/src/api/memory_api.dart';
-import 'package:openapi/src/api/o_auth_api.dart';
-import 'package:openapi/src/api/partner_api.dart';
-import 'package:openapi/src/api/person_api.dart';
-import 'package:openapi/src/api/search_api.dart';
-import 'package:openapi/src/api/server_info_api.dart';
-import 'package:openapi/src/api/sessions_api.dart';
-import 'package:openapi/src/api/shared_link_api.dart';
-import 'package:openapi/src/api/sync_api.dart';
-import 'package:openapi/src/api/system_config_api.dart';
-import 'package:openapi/src/api/system_metadata_api.dart';
-import 'package:openapi/src/api/tag_api.dart';
-import 'package:openapi/src/api/timeline_api.dart';
-import 'package:openapi/src/api/trash_api.dart';
-import 'package:openapi/src/api/user_api.dart';
-
-class Openapi {
- static const String basePath = r'/api';
-
- final Dio dio;
- final Serializers serializers;
-
- Openapi({
- Dio? dio,
- Serializers? serializers,
- String? basePathOverride,
- List? interceptors,
- }) : this.serializers = serializers ?? standardSerializers,
- this.dio = dio ??
- Dio(BaseOptions(
- baseUrl: basePathOverride ?? basePath,
- connectTimeout: const Duration(milliseconds: 5000),
- receiveTimeout: const Duration(milliseconds: 3000),
- )) {
- if (interceptors == null) {
- this.dio.interceptors.addAll([
- OAuthInterceptor(),
- BasicAuthInterceptor(),
- BearerAuthInterceptor(),
- ApiKeyAuthInterceptor(),
- ]);
- } else {
- this.dio.interceptors.addAll(interceptors);
- }
- }
-
- void setOAuthToken(String name, String token) {
- if (this.dio.interceptors.any((i) => i is OAuthInterceptor)) {
- (this.dio.interceptors.firstWhere((i) => i is OAuthInterceptor) as OAuthInterceptor).tokens[name] = token;
- }
- }
-
- void setBearerAuth(String name, String token) {
- if (this.dio.interceptors.any((i) => i is BearerAuthInterceptor)) {
- (this.dio.interceptors.firstWhere((i) => i is BearerAuthInterceptor) as BearerAuthInterceptor).tokens[name] = token;
- }
- }
-
- void setBasicAuth(String name, String username, String password) {
- if (this.dio.interceptors.any((i) => i is BasicAuthInterceptor)) {
- (this.dio.interceptors.firstWhere((i) => i is BasicAuthInterceptor) as BasicAuthInterceptor).authInfo[name] = BasicAuthInfo(username, password);
- }
- }
-
- void setApiKey(String name, String apiKey) {
- if (this.dio.interceptors.any((i) => i is ApiKeyAuthInterceptor)) {
- (this.dio.interceptors.firstWhere((element) => element is ApiKeyAuthInterceptor) as ApiKeyAuthInterceptor).apiKeys[name] = apiKey;
- }
- }
-
- /// Get APIKeyApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- APIKeyApi getAPIKeyApi() {
- return APIKeyApi(dio, serializers);
- }
-
- /// Get ActivityApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- ActivityApi getActivityApi() {
- return ActivityApi(dio, serializers);
- }
-
- /// Get AlbumApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- AlbumApi getAlbumApi() {
- return AlbumApi(dio, serializers);
- }
-
- /// Get AssetApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- AssetApi getAssetApi() {
- return AssetApi(dio, serializers);
- }
-
- /// Get AuditApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- AuditApi getAuditApi() {
- return AuditApi(dio, serializers);
- }
-
- /// Get AuthenticationApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- AuthenticationApi getAuthenticationApi() {
- return AuthenticationApi(dio, serializers);
- }
-
- /// Get DownloadApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- DownloadApi getDownloadApi() {
- return DownloadApi(dio, serializers);
- }
-
- /// Get FaceApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- FaceApi getFaceApi() {
- return FaceApi(dio, serializers);
- }
-
- /// Get FileReportApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- FileReportApi getFileReportApi() {
- return FileReportApi(dio, serializers);
- }
-
- /// Get JobApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- JobApi getJobApi() {
- return JobApi(dio, serializers);
- }
-
- /// Get LibraryApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- LibraryApi getLibraryApi() {
- return LibraryApi(dio, serializers);
- }
-
- /// Get MemoryApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- MemoryApi getMemoryApi() {
- return MemoryApi(dio, serializers);
- }
-
- /// Get OAuthApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- OAuthApi getOAuthApi() {
- return OAuthApi(dio, serializers);
- }
-
- /// Get PartnerApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- PartnerApi getPartnerApi() {
- return PartnerApi(dio, serializers);
- }
-
- /// Get PersonApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- PersonApi getPersonApi() {
- return PersonApi(dio, serializers);
- }
-
- /// Get SearchApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SearchApi getSearchApi() {
- return SearchApi(dio, serializers);
- }
-
- /// Get ServerInfoApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- ServerInfoApi getServerInfoApi() {
- return ServerInfoApi(dio, serializers);
- }
-
- /// Get SessionsApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SessionsApi getSessionsApi() {
- return SessionsApi(dio, serializers);
- }
-
- /// Get SharedLinkApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SharedLinkApi getSharedLinkApi() {
- return SharedLinkApi(dio, serializers);
- }
-
- /// Get SyncApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SyncApi getSyncApi() {
- return SyncApi(dio, serializers);
- }
-
- /// Get SystemConfigApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SystemConfigApi getSystemConfigApi() {
- return SystemConfigApi(dio, serializers);
- }
-
- /// Get SystemMetadataApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- SystemMetadataApi getSystemMetadataApi() {
- return SystemMetadataApi(dio, serializers);
- }
-
- /// Get TagApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- TagApi getTagApi() {
- return TagApi(dio, serializers);
- }
-
- /// Get TimelineApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- TimelineApi getTimelineApi() {
- return TimelineApi(dio, serializers);
- }
-
- /// Get TrashApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- TrashApi getTrashApi() {
- return TrashApi(dio, serializers);
- }
-
- /// Get UserApi instance, base route and serializer can be overridden by a given but be careful,
- /// by doing that all interceptors will not be executed
- UserApi getUserApi() {
- return UserApi(dio, serializers);
- }
-}
diff --git a/mobile-v2/openapi/lib/src/api/activity_api.dart b/mobile-v2/openapi/lib/src/api/activity_api.dart
deleted file mode 100644
index 488af14d66..0000000000
--- a/mobile-v2/openapi/lib/src/api/activity_api.dart
+++ /dev/null
@@ -1,407 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-
-import 'dart:async';
-
-import 'package:built_value/serializer.dart';
-import 'package:dio/dio.dart';
-
-import 'package:built_collection/built_collection.dart';
-import 'package:openapi/src/api_util.dart';
-import 'package:openapi/src/model/activity_create_dto.dart';
-import 'package:openapi/src/model/activity_response_dto.dart';
-import 'package:openapi/src/model/activity_statistics_response_dto.dart';
-import 'package:openapi/src/model/reaction_level.dart';
-import 'package:openapi/src/model/reaction_type.dart';
-
-class ActivityApi {
-
- final Dio _dio;
-
- final Serializers _serializers;
-
- const ActivityApi(this._dio, this._serializers);
-
- /// createActivity
- ///
- ///
- /// Parameters:
- /// * [activityCreateDto]
- /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation
- /// * [headers] - Can be used to add additional headers to the request
- /// * [extras] - Can be used to add flags to the request
- /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response
- /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress
- /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress
- ///
- /// Returns a [Future] containing a [Response] with a [ActivityResponseDto] as data
- /// Throws [DioException] if API call or serialization fails
- Future> createActivity({
- required ActivityCreateDto activityCreateDto,
- CancelToken? cancelToken,
- Map? headers,
- Map? extra,
- ValidateStatus? validateStatus,
- ProgressCallback? onSendProgress,
- ProgressCallback? onReceiveProgress,
- }) async {
- final _path = r'/activity';
- final _options = Options(
- method: r'POST',
- headers: {
- ...?headers,
- },
- extra: {
- 'secure':