add proper logging
This commit is contained in:
@@ -4,7 +4,7 @@ class LocalAlbum extends Table {
|
||||
const LocalAlbum();
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get localId => text()();
|
||||
TextColumn get localId => text().unique()();
|
||||
TextColumn get name => text()();
|
||||
DateTimeColumn get modifiedTime =>
|
||||
dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@@ -5,9 +5,9 @@ class LocalAsset extends Table {
|
||||
const LocalAsset();
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get localId => text()();
|
||||
TextColumn get localId => text().unique()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get checksum => text()();
|
||||
TextColumn get checksum => text().unique()();
|
||||
IntColumn get height => integer()();
|
||||
IntColumn get width => integer()();
|
||||
IntColumn get type => intEnum<AssetType>()();
|
||||
|
||||
@@ -6,7 +6,10 @@ class Store extends Table {
|
||||
@override
|
||||
String get tableName => 'store';
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get id => integer()();
|
||||
IntColumn get intValue => integer().nullable()();
|
||||
TextColumn get stringValue => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
class User extends Table {
|
||||
const User();
|
||||
|
||||
TextColumn get id => text()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get email => text()();
|
||||
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
|
||||
// Quota
|
||||
IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))();
|
||||
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
|
||||
// Sharing
|
||||
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();
|
||||
// User prefs
|
||||
TextColumn get profileImagePath => text()();
|
||||
BoolColumn get memoryEnabled => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get avatarColor => intEnum<UserAvatarColor>()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -6,6 +6,15 @@ abstract class ILogRepository {
|
||||
/// Fetches all logs
|
||||
FutureOr<List<LogMessage>> fetchLogs();
|
||||
|
||||
/// Inserts a new log into the DB
|
||||
FutureOr<bool> add(LogMessage log);
|
||||
|
||||
/// Bulk insert logs into DB
|
||||
FutureOr<bool> addAll(List<LogMessage> log);
|
||||
|
||||
/// Clears all logs
|
||||
FutureOr<bool> clear();
|
||||
|
||||
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
|
||||
FutureOr<void> truncateLogs({int limit = 250});
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ abstract class IStoreConverter<T, U> {
|
||||
U toPrimitive(T value);
|
||||
|
||||
/// Converts the value back to T? from the primitive type U from the Store
|
||||
T? fromPrimitive(U value);
|
||||
FutureOr<T?> fromPrimitive(U value);
|
||||
}
|
||||
|
||||
abstract class IStoreRepository {
|
||||
FutureOr<T?> getValue<T, U>(StoreKey<T, U> key);
|
||||
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key);
|
||||
|
||||
FutureOr<bool> setValue<T, U>(StoreKey<T, U> key, T value);
|
||||
FutureOr<T> get<T, U>(StoreKey<T, U> key);
|
||||
|
||||
FutureOr<void> deleteValue(StoreKey key);
|
||||
FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value);
|
||||
|
||||
Stream<T?> watchValue<T, U>(StoreKey<T, U> key);
|
||||
FutureOr<void> delete(StoreKey key);
|
||||
|
||||
Stream<List<StoreValue>> watchStore();
|
||||
Stream<T?> watch<T, U>(StoreKey<T, U> key);
|
||||
|
||||
FutureOr<void> clearStore();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
abstract class IUserRepository {
|
||||
/// Fetches user
|
||||
FutureOr<User?> getUser(String userId);
|
||||
|
||||
/// Insert user
|
||||
FutureOr<bool> insertUser(User user);
|
||||
}
|
||||
@@ -24,7 +24,6 @@ extension LevelExtension on Level {
|
||||
|
||||
@immutable
|
||||
class LogMessage {
|
||||
final int id;
|
||||
final String content;
|
||||
final LogLevel level;
|
||||
final DateTime createdAt;
|
||||
@@ -33,7 +32,6 @@ class LogMessage {
|
||||
final String? stack;
|
||||
|
||||
const LogMessage({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.level,
|
||||
required this.createdAt,
|
||||
@@ -51,8 +49,7 @@ class LogMessage {
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
content.hashCode ^
|
||||
return content.hashCode ^
|
||||
level.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
logger.hashCode ^
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/store_converters.dart';
|
||||
import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
|
||||
|
||||
@@ -21,14 +22,33 @@ class StoreValue<T> {
|
||||
int get hashCode => id.hashCode ^ value.hashCode;
|
||||
}
|
||||
|
||||
class StoreKeyNotFoundException implements Exception {
|
||||
final StoreKey key;
|
||||
const StoreKeyNotFoundException(this.key);
|
||||
|
||||
@override
|
||||
String toString() => "Key '${key.name}' not found in Store";
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
converter: StoreStringConverter(),
|
||||
type: String,
|
||||
),
|
||||
accessToken<String, String>(
|
||||
1,
|
||||
converter: StoreStringConverter(),
|
||||
type: String,
|
||||
),
|
||||
currentUser<User, String>(
|
||||
2,
|
||||
converter: StoreUserConverter(),
|
||||
type: String,
|
||||
),
|
||||
// App settings
|
||||
appTheme<AppTheme, int>(
|
||||
1000,
|
||||
converter: StoreEnumConverter(AppTheme.values),
|
||||
@@ -44,7 +64,7 @@ enum StoreKey<T, U> {
|
||||
const StoreKey(this.id, {required this.converter, required this.type});
|
||||
final int id;
|
||||
|
||||
/// Type is also stored here easily fetch it during runtime
|
||||
/// Primitive Type is also stored here to easily fetch it during runtime
|
||||
final Type type;
|
||||
final IStoreConverter<T, U> converter;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:openapi/openapi.dart' as api;
|
||||
|
||||
class User {
|
||||
const User({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.isAdmin,
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
required this.inTimeline,
|
||||
required this.profileImagePath,
|
||||
required this.memoryEnabled,
|
||||
required this.avatarColor,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final DateTime updatedAt;
|
||||
final String name;
|
||||
final String email;
|
||||
final bool isAdmin;
|
||||
// Quota
|
||||
final int quotaSizeInBytes;
|
||||
final int quotaUsageInBytes;
|
||||
// Sharing
|
||||
final bool inTimeline;
|
||||
// User prefs
|
||||
final String profileImagePath;
|
||||
final bool memoryEnabled;
|
||||
final UserAvatarColor avatarColor;
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
DateTime? updatedAt,
|
||||
String? name,
|
||||
String? email,
|
||||
bool? isAdmin,
|
||||
int? quotaSizeInBytes,
|
||||
int? quotaUsageInBytes,
|
||||
bool? inTimeline,
|
||||
String? profileImagePath,
|
||||
bool? memoryEnabled,
|
||||
UserAvatarColor? avatarColor,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
|
||||
inTimeline: inTimeline ?? this.inTimeline,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
memoryEnabled: memoryEnabled ?? this.memoryEnabled,
|
||||
avatarColor: avatarColor ?? this.avatarColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'User(id: $id, updatedAt: $updatedAt, name: $name, email: $email, isAdmin: $isAdmin, quotaSizeInBytes: $quotaSizeInBytes, quotaUsageInBytes: $quotaUsageInBytes, inTimeline: $inTimeline, profileImagePath: $profileImagePath, memoryEnabled: $memoryEnabled, avatarColor: $avatarColor)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant User other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.name == name &&
|
||||
other.email == email &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||
other.quotaUsageInBytes == quotaUsageInBytes &&
|
||||
other.inTimeline == inTimeline &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
other.memoryEnabled == memoryEnabled &&
|
||||
other.avatarColor == avatarColor;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
name.hashCode ^
|
||||
email.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
quotaSizeInBytes.hashCode ^
|
||||
quotaUsageInBytes.hashCode ^
|
||||
inTimeline.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
memoryEnabled.hashCode ^
|
||||
avatarColor.hashCode;
|
||||
}
|
||||
|
||||
factory User.fromAdminDto(
|
||||
api.UserAdminResponseDto userDto, [
|
||||
api.UserPreferencesResponseDto? userPreferences,
|
||||
]) {
|
||||
return User(
|
||||
id: userDto.id,
|
||||
updatedAt: DateTime.now(),
|
||||
name: userDto.name,
|
||||
email: userDto.email,
|
||||
isAdmin: userDto.isAdmin,
|
||||
quotaSizeInBytes: userDto.quotaSizeInBytes ?? 0,
|
||||
quotaUsageInBytes: userDto.quotaUsageInBytes ?? 0,
|
||||
inTimeline: true,
|
||||
profileImagePath: userDto.profileImagePath,
|
||||
memoryEnabled: userPreferences?.memories.enabled ?? true,
|
||||
avatarColor: userDto.avatarColor.toEnum(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum UserAvatarColor {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber,
|
||||
}
|
||||
|
||||
extension AvatarColorEnumHelper on api.UserAvatarColor {
|
||||
UserAvatarColor toEnum() {
|
||||
switch (this) {
|
||||
case api.UserAvatarColor.primary:
|
||||
return UserAvatarColor.primary;
|
||||
case api.UserAvatarColor.pink:
|
||||
return UserAvatarColor.pink;
|
||||
case api.UserAvatarColor.red:
|
||||
return UserAvatarColor.red;
|
||||
case api.UserAvatarColor.yellow:
|
||||
return UserAvatarColor.yellow;
|
||||
case api.UserAvatarColor.blue:
|
||||
return UserAvatarColor.blue;
|
||||
case api.UserAvatarColor.green:
|
||||
return UserAvatarColor.green;
|
||||
case api.UserAvatarColor.purple:
|
||||
return UserAvatarColor.purple;
|
||||
case api.UserAvatarColor.orange:
|
||||
return UserAvatarColor.orange;
|
||||
case api.UserAvatarColor.gray:
|
||||
return UserAvatarColor.gray;
|
||||
case api.UserAvatarColor.amber:
|
||||
return UserAvatarColor.amber;
|
||||
}
|
||||
return UserAvatarColor.primary;
|
||||
}
|
||||
}
|
||||
|
||||
extension AvatarColorToColorHelper on UserAvatarColor {
|
||||
Color toColor([bool isDarkTheme = false]) {
|
||||
switch (this) {
|
||||
case UserAvatarColor.primary:
|
||||
return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF);
|
||||
case UserAvatarColor.pink:
|
||||
return const Color.fromARGB(255, 244, 114, 182);
|
||||
case UserAvatarColor.red:
|
||||
return const Color.fromARGB(255, 239, 68, 68);
|
||||
case UserAvatarColor.yellow:
|
||||
return const Color.fromARGB(255, 234, 179, 8);
|
||||
case UserAvatarColor.blue:
|
||||
return const Color.fromARGB(255, 59, 130, 246);
|
||||
case UserAvatarColor.green:
|
||||
return const Color.fromARGB(255, 22, 163, 74);
|
||||
case UserAvatarColor.purple:
|
||||
return const Color.fromARGB(255, 147, 51, 234);
|
||||
case UserAvatarColor.orange:
|
||||
return const Color.fromARGB(255, 234, 88, 12);
|
||||
case UserAvatarColor.gray:
|
||||
return const Color.fromARGB(255, 75, 85, 99);
|
||||
case UserAvatarColor.amber:
|
||||
return const Color.fromARGB(255, 217, 119, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@ import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:drift_dev/api/migrations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/domain/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/domain/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/domain/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/domain/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/database.interface.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -14,7 +18,7 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
import 'database.repository.drift.dart';
|
||||
|
||||
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset])
|
||||
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User])
|
||||
class DriftDatabaseRepository extends $DriftDatabaseRepository
|
||||
implements IDatabaseRepository<GeneratedDatabase> {
|
||||
DriftDatabaseRepository() : super(_openConnection());
|
||||
@@ -51,6 +55,18 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository
|
||||
@override
|
||||
// ignore: no-empty-block
|
||||
void migrateDB() {
|
||||
// No migrations yet
|
||||
// Migrations are handled automatically using the migrator field
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) => m.createAll(),
|
||||
beforeOpen: (details) async {
|
||||
if (kDebugMode) {
|
||||
await validateDatabaseSchema();
|
||||
}
|
||||
},
|
||||
// ignore: no-empty-block
|
||||
onUpgrade: (m, from, to) async {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/entities/log.entity.drift.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
@@ -10,7 +14,7 @@ class LogDriftRepository implements ILogRepository {
|
||||
|
||||
@override
|
||||
Future<List<LogMessage>> fetchLogs() async {
|
||||
return await db.select(db.logs).map((l) => l.toModel()).get();
|
||||
return await db.managers.logs.map((l) => l.toModel()).get();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -26,12 +30,65 @@ class LogDriftRepository implements ILogRepository {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> add(LogMessage log) async {
|
||||
try {
|
||||
await db.transaction(() async {
|
||||
await db.into(db.logs).insert(LogsCompanion.insert(
|
||||
content: log.content,
|
||||
level: log.level,
|
||||
createdAt: Value(log.createdAt),
|
||||
error: Value(log.error),
|
||||
logger: Value(log.logger),
|
||||
stack: Value(log.stack),
|
||||
));
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error while adding a log to the DB - $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> addAll(List<LogMessage> logs) async {
|
||||
try {
|
||||
await db.batch((b) {
|
||||
b.insertAll(
|
||||
db.logs,
|
||||
logs.map((log) => LogsCompanion.insert(
|
||||
content: log.content,
|
||||
level: log.level,
|
||||
createdAt: Value(log.createdAt),
|
||||
error: Value(log.error),
|
||||
logger: Value(log.logger),
|
||||
stack: Value(log.stack),
|
||||
)),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error while adding a log to the DB - $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> clear() async {
|
||||
try {
|
||||
await db.managers.logs.delete();
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error while clearning the logs in DB - $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _LogToLogMessage on Log {
|
||||
LogMessage toModel() {
|
||||
return LogMessage(
|
||||
id: id,
|
||||
content: content,
|
||||
createdAt: createdAt,
|
||||
level: level,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -14,7 +13,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
||||
const StoreDriftRepository(this.db);
|
||||
|
||||
@override
|
||||
FutureOr<T?> getValue<T, U>(StoreKey<T, U> key) async {
|
||||
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key) async {
|
||||
final storeData = await db.managers.store
|
||||
.filter((s) => s.id.equals(key.id))
|
||||
.getSingleOrNull();
|
||||
@@ -22,7 +21,16 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> setValue<T, U>(StoreKey<T, U> key, T value) async {
|
||||
FutureOr<T> get<T, U>(StoreKey<T, U> key) async {
|
||||
final value = await tryGet(key);
|
||||
if (value == null) {
|
||||
throw StoreKeyNotFoundException(key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value) async {
|
||||
try {
|
||||
await db.transaction(() async {
|
||||
final storeValue = key.converter.toPrimitive(value);
|
||||
@@ -42,30 +50,18 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> deleteValue(StoreKey key) async {
|
||||
FutureOr<void> delete(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) {
|
||||
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, U>(StoreKey<T, U> key) {
|
||||
Stream<T?> watch<T, U>(StoreKey<T, U> key) {
|
||||
return db.managers.store
|
||||
.filter((s) => s.id.equals(key.id))
|
||||
.watchSingleOrNull()
|
||||
.map((e) => _getValueFromStoreData(key, e));
|
||||
.asyncMap((e) async => await _getValueFromStoreData(key, e));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -75,7 +71,10 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
|
||||
});
|
||||
}
|
||||
|
||||
T? _getValueFromStoreData<T, U>(StoreKey<T, U> key, StoreData? data) {
|
||||
FutureOr<T?> _getValueFromStoreData<T, U>(
|
||||
StoreKey<T, U> key,
|
||||
StoreData? data,
|
||||
) async {
|
||||
final primitive = switch (key.type) {
|
||||
const (int) => data?.intValue,
|
||||
const (String) => data?.stringValue,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||
|
||||
class UserDriftRepository with LogContext implements IUserRepository {
|
||||
final DriftDatabaseRepository db;
|
||||
|
||||
const UserDriftRepository(this.db);
|
||||
|
||||
@override
|
||||
FutureOr<User?> getUser(String userId) async {
|
||||
return await db.managers.user
|
||||
.filter((f) => f.id.equals(userId))
|
||||
.map((u) => u.toModel())
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<bool> insertUser(User user) async {
|
||||
try {
|
||||
return await db.transaction(() async {
|
||||
await db.into(db.user).insertOnConflictUpdate(UserCompanion.insert(
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
profileImagePath: user.profileImagePath,
|
||||
avatarColor: user.avatarColor,
|
||||
inTimeline: Value(user.inTimeline),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
memoryEnabled: Value(user.memoryEnabled),
|
||||
quotaSizeInBytes: Value(user.quotaSizeInBytes),
|
||||
quotaUsageInBytes: Value(user.quotaSizeInBytes),
|
||||
updatedAt: Value(user.updatedAt),
|
||||
));
|
||||
return true;
|
||||
});
|
||||
} catch (e, s) {
|
||||
log.severe("Cannot insert User into table - $user", e, s);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _UserDataToUser on UserData {
|
||||
User toModel() {
|
||||
return User(
|
||||
id: id,
|
||||
email: email,
|
||||
avatarColor: avatarColor,
|
||||
inTimeline: inTimeline,
|
||||
isAdmin: isAdmin,
|
||||
memoryEnabled: memoryEnabled,
|
||||
name: name,
|
||||
profileImagePath: profileImagePath,
|
||||
quotaSizeInBytes: quotaSizeInBytes,
|
||||
quotaUsageInBytes: quotaUsageInBytes,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/app_setting.model.dart';
|
||||
import 'package:immich_mobile/domain/store_manager.dart';
|
||||
|
||||
class AppSettingService {
|
||||
final StoreManager store;
|
||||
final IStoreRepository store;
|
||||
|
||||
const AppSettingService(this.store);
|
||||
|
||||
T getSetting<T>(AppSetting<T> setting) {
|
||||
return store.get(setting.storeKey, setting.defaultValue);
|
||||
Future<T> getSetting<T>(AppSetting<T> setting) async {
|
||||
final value = await store.tryGet(setting.storeKey);
|
||||
return value ?? setting.defaultValue;
|
||||
}
|
||||
|
||||
Future<bool> setSetting<T>(AppSetting<T> setting, T value) async {
|
||||
return await store.put(setting.storeKey, value);
|
||||
return await store.set(setting.storeKey, value);
|
||||
}
|
||||
|
||||
Stream<T> watchSetting<T>(AppSetting<T> setting) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||
import 'package:openapi/openapi.dart';
|
||||
|
||||
@@ -52,4 +54,58 @@ class LoginService with LogContext {
|
||||
// No well-known, return the baseUrl
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
Future<String?> passwordLogin(String email, String password) async {
|
||||
try {
|
||||
final loginResponse = await di<Openapi>().getAuthenticationApi().login(
|
||||
loginCredentialDto: LoginCredentialDto((builder) {
|
||||
builder.email = email;
|
||||
builder.password = password;
|
||||
}),
|
||||
);
|
||||
|
||||
return loginResponse.data?.accessToken;
|
||||
} catch (e, s) {
|
||||
log.severe("Exception occured while performing password login", e, s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> oAuthLogin() async {
|
||||
const String oAuthCallbackSchema = 'app.immich';
|
||||
|
||||
final oAuthApi = di<Openapi>().getOAuthApi();
|
||||
|
||||
try {
|
||||
final oAuthUrl = await oAuthApi.startOAuth(
|
||||
oAuthConfigDto: OAuthConfigDto((builder) {
|
||||
builder.redirectUri = "$oAuthCallbackSchema:/";
|
||||
}),
|
||||
);
|
||||
|
||||
final oAuthUrlRes = oAuthUrl.data?.url;
|
||||
if (oAuthUrlRes == null) {
|
||||
log.severe(
|
||||
"oAuth Server URL not available. Kindly ensure oAuth login is enabled in the server",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final oAuthCallbackUrl = await FlutterWebAuth2.authenticate(
|
||||
url: oAuthUrlRes,
|
||||
callbackUrlScheme: oAuthCallbackSchema,
|
||||
);
|
||||
|
||||
final loginResponse = await oAuthApi.finishOAuth(
|
||||
oAuthCallbackDto: OAuthCallbackDto((builder) {
|
||||
builder.url = oAuthCallbackUrl;
|
||||
}),
|
||||
);
|
||||
|
||||
return loginResponse.data?.accessToken;
|
||||
} catch (e) {
|
||||
log.severe("Exception occured while performing oauth login", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||
import 'package:openapi/openapi.dart';
|
||||
|
||||
class UserService with LogContext {
|
||||
final Openapi _api;
|
||||
|
||||
UsersApi get _userApi => _api.getUsersApi();
|
||||
|
||||
UserService(this._api);
|
||||
|
||||
Future<User?> getMyUser() async {
|
||||
try {
|
||||
final response = await _userApi.getMyUser();
|
||||
final dto = response.data;
|
||||
if (dto == null) {
|
||||
log.severe("Cannot fetch my user.");
|
||||
return null;
|
||||
}
|
||||
|
||||
final preferences = await _userApi.getMyPreferences();
|
||||
return User.fromAdminDto(dto, preferences.data);
|
||||
} catch (e, s) {
|
||||
log.severe("Error while fetching server features", e, s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
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 {
|
||||
final StoreKey key;
|
||||
const StoreKeyNotFoundException(this.key);
|
||||
|
||||
@override
|
||||
String toString() => "Key '${key.name}' not found in Store";
|
||||
}
|
||||
|
||||
/// Key-value cache for individual items enumerated in StoreKey.
|
||||
class StoreManager with LogContext {
|
||||
late final IStoreRepository _db;
|
||||
late final StreamSubscription _subscription;
|
||||
final Map<int, dynamic> _cache = {};
|
||||
|
||||
StoreManager(IStoreRepository db) {
|
||||
_db = db;
|
||||
_subscription = _db.watchStore().listen(_onChangeListener);
|
||||
_populateCache();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
}
|
||||
|
||||
FutureOr<void> _populateCache() async {
|
||||
for (StoreKey key in StoreKey.values) {
|
||||
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!
|
||||
Future<void> clear() async {
|
||||
_cache.clear();
|
||||
return await _db.clearStore();
|
||||
}
|
||||
|
||||
/// Returns the stored value for the given key (possibly null)
|
||||
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, U>(StoreKey<T, U> key, [T? defaultValue]) {
|
||||
final value = _cache[key.id] ?? defaultValue;
|
||||
if (value == null) {
|
||||
throw StoreKeyNotFoundException(key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Watches a specific key for changes
|
||||
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<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<T, U>(StoreKey<T, U> key) async {
|
||||
if (_cache[key.id] == null) return Future.value();
|
||||
_cache.remove(key.id);
|
||||
return await _db.deleteValue(key);
|
||||
}
|
||||
|
||||
/// Updates the state in cache if a value is updated in any isolate
|
||||
void _onChangeListener(List<StoreValue>? data) {
|
||||
if (data != null) {
|
||||
for (StoreValue storeValue in data) {
|
||||
if (storeValue.value != null) {
|
||||
_cache[storeValue.id] = storeValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
|
||||
class StoreEnumConverter<T extends Enum> extends IStoreConverter<T, int> {
|
||||
const StoreEnumConverter(this.values);
|
||||
@@ -22,8 +27,8 @@ class StoreBooleanConverter extends IStoreConverter<bool, int> {
|
||||
int toPrimitive(bool value) => value ? 1 : 0;
|
||||
}
|
||||
|
||||
class StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
|
||||
const StorePrimitiveConverter();
|
||||
class _StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
|
||||
const _StorePrimitiveConverter();
|
||||
|
||||
@override
|
||||
T fromPrimitive(T value) => value;
|
||||
@@ -31,3 +36,23 @@ class StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
|
||||
@override
|
||||
T toPrimitive(T value) => value;
|
||||
}
|
||||
|
||||
class StoreStringConverter extends _StorePrimitiveConverter<String> {
|
||||
const StoreStringConverter();
|
||||
}
|
||||
|
||||
class StoreIntConverter extends _StorePrimitiveConverter<int> {
|
||||
const StoreIntConverter();
|
||||
}
|
||||
|
||||
class StoreUserConverter extends IStoreConverter<User, String> {
|
||||
const StoreUserConverter();
|
||||
|
||||
@override
|
||||
Future<User?> fromPrimitive(String value) async {
|
||||
return await di<IUserRepository>().getUser(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toPrimitive(User value) => value.id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user