This commit is contained in:
shenlong-tanwen
2024-04-25 00:53:18 +05:30
parent 4ef7eb56a3
commit 11cef4ec9a
305 changed files with 47185 additions and 4 deletions
@@ -0,0 +1,14 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
class Logs extends Table {
const Logs();
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text()();
IntColumn get level => intEnum<LogLevel>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get logger => text().nullable()();
TextColumn get error => text().nullable()();
TextColumn get stack => text().nullable()();
}
@@ -0,0 +1,12 @@
import 'package:drift/drift.dart';
class Store extends Table {
const Store();
@override
String get tableName => 'store';
IntColumn get id => integer().autoIncrement()();
IntColumn get intValue => integer().nullable()();
TextColumn get stringValue => text().nullable()();
}
@@ -0,0 +1,10 @@
abstract class IDatabaseRepository<T> {
/// Current version of the DB to aid with migration
int get schemaVersion;
/// Initializes the DB and returns the corresponding object
T init();
/// Check and migrate the DB to the latest schema
void migrateDB();
}
@@ -0,0 +1,11 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/log.model.dart';
abstract class ILogRepository {
/// Fetches all logs
FutureOr<List<LogMessage>> fetchLogs();
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
FutureOr<void> truncateLogs({int limit = 250});
}
@@ -0,0 +1,17 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/store.model.dart';
abstract class IStoreRepository {
FutureOr<T?> getValue<T>(StoreKey key);
FutureOr<void> setValue<T>(StoreKey<T> key, T value);
FutureOr<void> deleteValue(StoreKey key);
Stream<T?> watchValue<T>(StoreKey key);
Stream<List<StoreValue>> watchStore();
FutureOr<void> clearStore();
}
@@ -0,0 +1,59 @@
import 'package:logging/logging.dart';
/// Log levels according to dart logging [Level]
enum LogLevel {
all,
finest,
finer,
fine,
config,
info,
warning,
severe,
shout,
off,
}
extension LevelExtension on Level {
LogLevel toLogLevel() =>
LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ??
LogLevel.info;
}
class LogMessage {
final int id;
final String content;
final LogLevel level;
final DateTime createdAt;
final String? logger;
final String? error;
final String? stack;
const LogMessage({
required this.id,
required this.content,
required this.level,
required this.createdAt,
this.logger,
this.error,
this.stack,
});
@override
bool operator ==(covariant LogMessage other) {
if (identical(this, other)) return true;
return other.hashCode == hashCode;
}
@override
int get hashCode {
return id.hashCode ^
content.hashCode ^
level.hashCode ^
createdAt.hashCode ^
logger.hashCode ^
error.hashCode ^
stack.hashCode;
}
}
@@ -0,0 +1,70 @@
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
enum StoreKey<T> {
// Server endpoint related stores
accessToken<String>(0, type: String),
serverEndpoint<String>(1, type: String),
;
const StoreKey(this.id, {required this.type});
final int id;
final Type type;
}
class StoreValue {
final int id;
final int? intValue;
final String? stringValue;
const StoreValue({required this.id, this.intValue, this.stringValue});
@override
bool operator ==(covariant StoreValue other) {
if (identical(this, other)) return true;
return other.hashCode == hashCode;
}
@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<T> 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);
}
}
@@ -0,0 +1,54 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.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';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'database.repository.drift.dart';
@DriftDatabase(tables: [Logs, Store])
class DriftDatabaseRepository extends $DriftDatabaseRepository
implements IDatabaseRepository<GeneratedDatabase> {
DriftDatabaseRepository() : super(_openConnection());
static LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
// Work around limitations on old Android versions
// https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#problems-on-android-6
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
// Make sqlite3 pick a more suitable location for temporary files - the
// one from the system may be inaccessible due to sandboxing.
// https://github.com/simolus3/moor/issues/876#issuecomment-710013503
final cachebase = (await getTemporaryDirectory()).path;
// We can't access /tmp on Android, which sqlite3 would try by default.
// Explicitly tell it about the correct temporary directory.
sqlite3.tempDirectory = cachebase;
return NativeDatabase.createInBackground(file);
});
}
@override
GeneratedDatabase init() => this;
@override
int get schemaVersion => 1;
@override
// ignore: no-empty-block
void migrateDB() {
// No migrations yet
}
}
@@ -0,0 +1,43 @@
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';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
class LogDriftRepository implements ILogRepository {
final DriftDatabaseRepository db;
const LogDriftRepository(this.db);
@override
Future<List<LogMessage>> fetchLogs() async {
return await db.select(db.logs).map((l) => l.toModel()).get();
}
@override
Future<void> truncateLogs({int limit = 250}) {
return db.transaction(() async {
final totalCount = await db.managers.logs.count();
if (totalCount > limit) {
final rowsToDelete = totalCount - limit;
await db.managers.logs
.orderBy((o) => o.createdAt.desc())
.limit(rowsToDelete)
.delete();
}
});
}
}
extension _LogToLogMessage on Log {
LogMessage toModel() {
return LogMessage(
id: id,
content: content,
createdAt: createdAt,
level: level,
error: error,
logger: logger,
stack: stack,
);
}
}
@@ -0,0 +1,66 @@
import 'dart:async';
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';
class StoreDriftRepository implements IStoreRepository {
final DriftDatabaseRepository db;
const StoreDriftRepository(this.db);
@override
FutureOr<T?> getValue<T>(StoreKey key) async {
final value = await db.managers.store
.filter((s) => s.id.equals(key.id))
.getSingleOrNull();
return value?.toModel().extract(key.type);
}
@override
FutureOr<void> setValue<T>(StoreKey<T> 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),
));
});
}
@override
FutureOr<void> deleteValue(StoreKey key) {
return 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();
}
@override
Stream<T?> watchValue<T>(StoreKey key) {
return db.managers.store
.filter((s) => s.id.equals(key.id))
.watchSingleOrNull()
.map((e) => e?.toModel().extract(key.type));
}
@override
FutureOr<void> clearStore() {
return db.transaction(() async {
await db.managers.store.delete();
});
}
}
extension _StoreDataToStoreValue on StoreData {
StoreValue toModel() {
return StoreValue(id: id, intValue: intValue, stringValue: stringValue);
}
}
+29
View File
@@ -0,0 +1,29 @@
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/store_manager.dart';
/// Ambient instance
final getIt = GetIt.instance;
class ServiceLocator {
const ServiceLocator._internal();
static void configureServices() {
// Register DB
getIt.registerSingleton<DriftDatabaseRepository>(DriftDatabaseRepository());
_registerCoreServices();
}
static void _registerCoreServices() {
// Init store
getIt
.registerFactory<IStoreRepository>(() => StoreDriftRepository(getIt()));
getIt.registerSingleton<StoreManager>(StoreManager(getIt()));
// Logs
getIt.registerFactory<ILogRepository>(() => LogDriftRepository(getIt()));
}
}
+99
View File
@@ -0,0 +1,99 @@
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/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 store for individual items enumerated in StoreKey.
/// Supports String, int and JSON-serializable Objects
/// Can be used concurrently from multiple isolates
class StoreManager with LogContext {
late final IStoreRepository _db;
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;
}
void dispose() {
_subscription?.cancel();
}
FutureOr<void> _populateCache() async {
for (StoreKey key in StoreKey.values) {
final StoreValue? value = await _db.getValue(key);
if (value != null) {
_cache[key.id] = value;
}
}
}
/// 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>(StoreKey<T> 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<T> 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>(StoreKey<T> key) => _db.watchValue(key);
/// Stores the value synchronously in the cache and asynchronously in the DB
FutureOr<void> put<T>(StoreKey<T> key, T value) async {
if (_cache[key.id] == value) return Future.value();
_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>(StoreKey<T> 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 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}");
}
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/service_locator.dart';
void main() {
// Ensure the bindings are initialized
WidgetsFlutterBinding.ensureInitialized();
// DI Injection
ServiceLocator.configureServices();
runApp(const MainWidget());
}
class MainWidget extends StatelessWidget {
const MainWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Text('Flutter Demo Home Page'),
);
}
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:auto_route/auto_route.dart';
part 'router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends _$AppRouter {
@override
List<AutoRoute> get routes => [];
}
@@ -0,0 +1,112 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/immich-logo.png
AssetGenImage get immichLogo =>
const AssetGenImage('assets/images/immich-logo.png');
/// File path: assets/images/immich-text-dark.png
AssetGenImage get immichTextDark =>
const AssetGenImage('assets/images/immich-text-dark.png');
/// File path: assets/images/immich-text-light.png
AssetGenImage get immichTextLight =>
const AssetGenImage('assets/images/immich-text-light.png');
/// List of all assets
List<AssetGenImage> get values =>
[immichLogo, immichTextDark, immichTextLight];
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage {
const AssetGenImage(this._assetName, {this.size = null});
final String _assetName;
final Size? size;
Image image({
Key? key,
AssetBundle? bundle,
ImageFrameBuilder? frameBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? scale,
double? width,
double? height,
Color? color,
Animation<double>? opacity,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
String? package,
FilterQuality filterQuality = FilterQuality.low,
int? cacheWidth,
int? cacheHeight,
}) {
return Image.asset(
_assetName,
key: key,
bundle: bundle,
frameBuilder: frameBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
scale: scale,
width: width,
height: height,
color: color,
opacity: opacity,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
package: package,
filterQuality: filterQuality,
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
);
}
ImageProvider provider({
AssetBundle? bundle,
String? package,
}) {
return AssetImage(
_assetName,
bundle: bundle,
package: package,
);
}
String get path => _assetName;
String get keyName => _assetName;
}
@@ -0,0 +1,14 @@
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);
}