🚀
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user