add adaptive_scaffold
This commit is contained in:
@@ -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)();
|
||||
}
|
||||
@@ -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<AssetType>()();
|
||||
DateTimeColumn get createdTime => dateTime()();
|
||||
DateTimeColumn get modifiedTime =>
|
||||
dateTime().withDefault(currentDateAndTime)();
|
||||
IntColumn get duration => integer().withDefault(const Constant(0))();
|
||||
BoolColumn get isLivePhoto => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
@@ -2,14 +2,24 @@ import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
|
||||
abstract class IStoreRepository {
|
||||
FutureOr<T?> getValue<T>(StoreKey key);
|
||||
abstract class IStoreConverter<T, U> {
|
||||
const IStoreConverter();
|
||||
|
||||
FutureOr<void> setValue<T>(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<T?> getValue<T, U>(StoreKey<T, U> key);
|
||||
|
||||
FutureOr<bool> setValue<T, U>(StoreKey<T, U> key, T value);
|
||||
|
||||
FutureOr<void> deleteValue(StoreKey key);
|
||||
|
||||
Stream<T?> watchValue<T>(StoreKey key);
|
||||
Stream<T?> watchValue<T, U>(StoreKey<T, U> key);
|
||||
|
||||
Stream<List<StoreValue>> watchStore();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
appTheme<int>(StoreKey.appTheme, 10);
|
||||
enum AppSetting<T> {
|
||||
appTheme<AppTheme>(StoreKey.appTheme, AppTheme.blue),
|
||||
themeMode<ThemeMode>(StoreKey.themeMode, ThemeMode.system),
|
||||
darkMode<bool>(StoreKey.darkMode, false);
|
||||
|
||||
const AppSettings(this.storeKey, this.defaultValue);
|
||||
const AppSetting(this.storeKey, this.defaultValue);
|
||||
|
||||
final StoreKey storeKey;
|
||||
// ignore: avoid-dynamic
|
||||
final StoreKey<T, dynamic> storeKey;
|
||||
final T defaultValue;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ?? '<NULL>'})';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant ServerConfig other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.oauthButtonText == oauthButtonText;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => oauthButtonText.hashCode;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<T> {
|
||||
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<T>(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<T>(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<T, U> {
|
||||
serverEndpoint<String, String>(
|
||||
0,
|
||||
converter: StorePrimitiveConverter(),
|
||||
type: String,
|
||||
),
|
||||
appTheme<AppTheme, int>(
|
||||
1000,
|
||||
converter: StoreEnumConverter(AppTheme.values),
|
||||
type: int,
|
||||
),
|
||||
themeMode<ThemeMode, int>(
|
||||
1001,
|
||||
converter: StoreEnumConverter(ThemeMode.values),
|
||||
type: int,
|
||||
),
|
||||
darkMode<bool, int>(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<T, U> converter;
|
||||
}
|
||||
|
||||
@@ -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<GeneratedDatabase> {
|
||||
DriftDatabaseRepository() : super(_openConnection());
|
||||
|
||||
@@ -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<T?> getValue<T>(StoreKey key) async {
|
||||
final value = await db.managers.store
|
||||
FutureOr<T?> getValue<T, U>(StoreKey<T, U> 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<void> setValue<T>(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<bool> setValue<T, U>(StoreKey<T, U> 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<void> deleteValue(StoreKey key) {
|
||||
return db.transaction(() async {
|
||||
FutureOr<void> deleteValue(StoreKey key) async {
|
||||
return await db.transaction(() async {
|
||||
await db.managers.store.filter((s) => s.id.equals(key.id)).delete();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<StoreValue>> 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<T?> watchValue<T>(StoreKey key) {
|
||||
Stream<T?> watchValue<T, U>(StoreKey<T, U> 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<void> clearStore() {
|
||||
return db.transaction(() async {
|
||||
FutureOr<void> 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<T, U>(StoreKey<T, U> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>(AppSettings<T> setting) {
|
||||
T getSetting<T>(AppSetting<T> setting) {
|
||||
return store.get(setting.storeKey, setting.defaultValue);
|
||||
}
|
||||
|
||||
void setSetting<T>(AppSettings<T> setting, T value) {
|
||||
store.put(setting.storeKey, value);
|
||||
Future<bool> setSetting<T>(AppSetting<T> setting, T value) async {
|
||||
return await store.put(setting.storeKey, value);
|
||||
}
|
||||
|
||||
Stream<T> watchSetting<T>(AppSettings<T> setting) {
|
||||
Stream<T> watchSetting<T>(AppSetting<T> setting) {
|
||||
return store
|
||||
.watch<T>(setting.storeKey)
|
||||
.watch(setting.storeKey)
|
||||
.map((value) => value ?? setting.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ServerFeatures?> 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<ServerConfig?> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<int, dynamic> _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<void> _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<T>(StoreKey key) => _cache[key.id] as T?;
|
||||
T? tryGet<T, U>(StoreKey<T, U> 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<T>(StoreKey key, [T? defaultValue]) {
|
||||
T get<T, U>(StoreKey<T, U> 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<T?> watch<T>(StoreKey key) => _db.watchValue(key);
|
||||
Stream<T?> watch<T, U>(StoreKey<T, U> key) => _db.watchValue(key);
|
||||
|
||||
/// Stores the value synchronously in the cache and asynchronously in the DB
|
||||
FutureOr<void> put<T>(StoreKey key, T value) async {
|
||||
if (_cache[key.id] == value) return Future.value();
|
||||
FutureOr<bool> put<T, U>(StoreKey<T, U> 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<void> delete(StoreKey key) async {
|
||||
Future<void> delete<T, U>(StoreKey<T, U> 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<StoreValue>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
|
||||
class StoreEnumConverter<T extends Enum> extends IStoreConverter<T, int> {
|
||||
const StoreEnumConverter(this.values);
|
||||
|
||||
final List<T> values;
|
||||
|
||||
@override
|
||||
T? fromPrimitive(int value) => values.elementAtOrNull(value);
|
||||
|
||||
@override
|
||||
int toPrimitive(T value) => value.index;
|
||||
}
|
||||
|
||||
class StoreBooleanConverter extends IStoreConverter<bool, int> {
|
||||
const StoreBooleanConverter();
|
||||
|
||||
@override
|
||||
bool fromPrimitive(int value) => value != 0;
|
||||
|
||||
@override
|
||||
int toPrimitive(bool value) => value ? 1 : 0;
|
||||
}
|
||||
|
||||
class StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
|
||||
const StorePrimitiveConverter();
|
||||
|
||||
@override
|
||||
T fromPrimitive(T value) => value;
|
||||
|
||||
@override
|
||||
T toPrimitive(T value) => value;
|
||||
}
|
||||
Reference in New Issue
Block a user