mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
add proper logging
This commit is contained in:
parent
1631df70e9
commit
75448ce56b
@ -4,7 +4,8 @@ include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_single_cascade_in_expression_statements: false
|
||||
- avoid_single_cascade_in_expression_statements: false
|
||||
- unawaited_futures
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
@ -16,7 +16,9 @@
|
||||
"error": {
|
||||
"empty_server_url": "Kindly provide a server URL",
|
||||
"invalid_server_url": "Invalid URL",
|
||||
"server_not_reachable": "Server is not reachable"
|
||||
"server_not_reachable": "Server is not reachable",
|
||||
"error_login": "Error logging in",
|
||||
"error_login_oauth": "Error logging using OAuth, check server URL"
|
||||
},
|
||||
"label": {
|
||||
"email": "Email",
|
||||
|
@ -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};
|
||||
}
|
||||
|
24
mobile-v2/lib/domain/entities/user.entity.dart
Normal file
24
mobile-v2/lib/domain/entities/user.entity.dart
Normal file
@ -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();
|
||||
}
|
||||
|
11
mobile-v2/lib/domain/interfaces/user.interface.dart
Normal file
11
mobile-v2/lib/domain/interfaces/user.interface.dart
Normal file
@ -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;
|
||||
}
|
||||
|
187
mobile-v2/lib/domain/models/user.model.dart
Normal file
187
mobile-v2/lib/domain/models/user.model.dart
Normal file
@ -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,
|
||||
|
65
mobile-v2/lib/domain/repositories/user.repository.dart
Normal file
65
mobile-v2/lib/domain/repositories/user.repository.dart
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
28
mobile-v2/lib/domain/services/user.service.dart
Normal file
28
mobile-v2/lib/domain/services/user.service.dart
Normal file
@ -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;
|
||||
}
|
||||
|
@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||
import 'package:immich_mobile/immich_app.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/log_manager.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// DI Injection
|
||||
ServiceLocator.configureServices();
|
||||
// Init logging
|
||||
LogManager.I.init();
|
||||
// Init localization
|
||||
LocaleSettings.useDeviceLocale();
|
||||
|
||||
|
@ -15,7 +15,7 @@ class ImLoadingIndicator extends StatelessWidget {
|
||||
width: dimension ?? 24,
|
||||
height: dimension ?? 24,
|
||||
child: FittedBox(
|
||||
child: CircularProgressIndicator(strokeWidth: strokeWidth ?? 2),
|
||||
child: CircularProgressIndicator(strokeWidth: strokeWidth ?? 4),
|
||||
),
|
||||
);
|
||||
}
|
@ -48,8 +48,13 @@ class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final value = _appSettingService.getSetting(widget.setting);
|
||||
isEnabled = T != bool ? widget.fromAppSetting!(value) : value as bool;
|
||||
_appSettingService.getSetting(widget.setting).then((value) {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
isEnabled = T != bool ? widget.fromAppSetting!(value) : value as bool;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -0,0 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
|
||||
class CurrentUserCubit extends Cubit<User> {
|
||||
CurrentUserCubit(super.initialState);
|
||||
}
|
@ -4,43 +4,51 @@ import 'package:flutter/material.dart';
|
||||
class LoginPageState {
|
||||
final bool isServerValidated;
|
||||
final bool isValidationInProgress;
|
||||
final bool isLoginSuccessful;
|
||||
|
||||
const LoginPageState({
|
||||
required this.isServerValidated,
|
||||
required this.isValidationInProgress,
|
||||
required this.isLoginSuccessful,
|
||||
});
|
||||
|
||||
factory LoginPageState.reset() {
|
||||
return const LoginPageState(
|
||||
isServerValidated: false,
|
||||
isValidationInProgress: false,
|
||||
isLoginSuccessful: false,
|
||||
);
|
||||
}
|
||||
|
||||
LoginPageState copyWith({
|
||||
bool? isServerValidated,
|
||||
bool? isValidationInProgress,
|
||||
bool? isLoginSuccessful,
|
||||
}) {
|
||||
return LoginPageState(
|
||||
isServerValidated: isServerValidated ?? this.isServerValidated,
|
||||
isValidationInProgress:
|
||||
isValidationInProgress ?? this.isValidationInProgress,
|
||||
isLoginSuccessful: isLoginSuccessful ?? this.isLoginSuccessful,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'LoginPageState(isServerValidated: $isServerValidated, isValidationInProgress: $isValidationInProgress)';
|
||||
'LoginPageState(isServerValidated: $isServerValidated, isValidationInProgress: $isValidationInProgress, isLoginSuccessful: $isLoginSuccessful)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant LoginPageState other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.isServerValidated == isServerValidated &&
|
||||
other.isValidationInProgress == isValidationInProgress;
|
||||
other.isValidationInProgress == isValidationInProgress &&
|
||||
other.isLoginSuccessful == isLoginSuccessful;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
isServerValidated.hashCode ^ isValidationInProgress.hashCode;
|
||||
isServerValidated.hashCode ^
|
||||
isValidationInProgress.hashCode ^
|
||||
isLoginSuccessful.hashCode;
|
||||
}
|
||||
|
@ -152,13 +152,20 @@ class _LoginPageState extends State<LoginPage>
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: appBar,
|
||||
body: SafeArea(
|
||||
child: ImAdaptiveScaffoldBody(
|
||||
primaryBody: (_) => primaryBody,
|
||||
secondaryBody: (_) => secondaryBody,
|
||||
return BlocListener<LoginPageCubit, LoginPageState>(
|
||||
listener: (_, loginState) {
|
||||
if (loginState.isLoginSuccessful) {
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: appBar,
|
||||
body: SafeArea(
|
||||
child: ImAdaptiveScaffoldBody(
|
||||
primaryBody: (_) => primaryBody,
|
||||
secondaryBody: (_) => secondaryBody,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,15 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||
import 'package:immich_mobile/domain/store_manager.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
|
||||
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/immich_auth_interceptor.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||
import 'package:immich_mobile/utils/snackbar_manager.dart';
|
||||
import 'package:openapi/openapi.dart';
|
||||
|
||||
class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
|
||||
LoginPageCubit() : super(LoginPageState.reset());
|
||||
@ -60,8 +65,8 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
|
||||
// Check for /.well-known/immich
|
||||
url = await loginService.resolveEndpoint(uri);
|
||||
|
||||
di<StoreManager>().put(StoreKey.serverEndpoint, url);
|
||||
ServiceLocator.registerPostValidationServices(url);
|
||||
di<IStoreRepository>().set(StoreKey.serverEndpoint, url);
|
||||
await ServiceLocator.registerPostValidationServices(url);
|
||||
|
||||
// Fetch server features
|
||||
await di<ServerFeatureConfigCubit>().getFeatures();
|
||||
@ -76,15 +81,64 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
emit(state.copyWith(isValidationInProgress: true));
|
||||
try {
|
||||
emit(state.copyWith(isValidationInProgress: true));
|
||||
final accessToken =
|
||||
await di<LoginService>().passwordLogin(email, password);
|
||||
|
||||
final url = di<StoreManager>().get(StoreKey.serverEndpoint);
|
||||
if (accessToken == null) {
|
||||
SnackbarManager.showError(t.login.error.error_login);
|
||||
return;
|
||||
}
|
||||
|
||||
await _postLogin(accessToken);
|
||||
} finally {
|
||||
emit(state.copyWith(isValidationInProgress: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> oAuthLogin() async {
|
||||
emit(state.copyWith(isValidationInProgress: true));
|
||||
try {
|
||||
emit(state.copyWith(isValidationInProgress: true));
|
||||
|
||||
final url = di<StoreManager>().get(StoreKey.serverEndpoint);
|
||||
final accessToken = await di<LoginService>().oAuthLogin();
|
||||
|
||||
if (accessToken == null) {
|
||||
SnackbarManager.showError(t.login.error.error_login_oauth);
|
||||
return;
|
||||
}
|
||||
|
||||
await _postLogin(accessToken);
|
||||
} finally {
|
||||
emit(state.copyWith(isValidationInProgress: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _postLogin(String accessToken) async {
|
||||
await di<IStoreRepository>().set(StoreKey.accessToken, accessToken);
|
||||
|
||||
/// Set token to interceptor
|
||||
final interceptor = di<Openapi>()
|
||||
.dio
|
||||
.interceptors
|
||||
.firstWhereOrNull((i) => i is ImmichAuthInterceptor)
|
||||
as ImmichAuthInterceptor?;
|
||||
interceptor?.setAccessToken(accessToken);
|
||||
|
||||
final user = await di<UserService>().getMyUser();
|
||||
if (user == null) {
|
||||
SnackbarManager.showError(t.login.error.error_login);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register user
|
||||
ServiceLocator.registerCurrentUser(user);
|
||||
await di<IUserRepository>().insertUser(user);
|
||||
|
||||
emit(state.copyWith(
|
||||
isValidationInProgress: false,
|
||||
isServerValidated: true,
|
||||
));
|
||||
}
|
||||
|
||||
void resetServerValidation() {
|
||||
|
@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:immich_mobile/domain/models/server-info/server_feature_config.model.dart';
|
||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||
import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/common/loading_indaticator.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/common/loading_indicator.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/input/filled_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/input/password_form_field.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/input/text_button.widget.dart';
|
||||
@ -132,6 +132,7 @@ class _CredentialsPageState extends State<_CredentialsPage> {
|
||||
children: [
|
||||
if (state.features.hasPasswordLogin) ...[
|
||||
ImTextFormField(
|
||||
controller: widget.emailController,
|
||||
label: context.t.login.label.email,
|
||||
isDisabled: isValidationInProgress,
|
||||
textInputAction: TextInputAction.next,
|
||||
@ -139,6 +140,7 @@ class _CredentialsPageState extends State<_CredentialsPage> {
|
||||
),
|
||||
const SizedGap.mh(),
|
||||
ImPasswordFormField(
|
||||
controller: widget.passwordController,
|
||||
label: context.t.login.label.password,
|
||||
focusNode: passwordFocusNode,
|
||||
isDisabled: isValidationInProgress,
|
||||
@ -148,11 +150,12 @@ class _CredentialsPageState extends State<_CredentialsPage> {
|
||||
ImFilledButton(
|
||||
label: context.t.login.label.login_button,
|
||||
icon: Symbols.login_rounded,
|
||||
onPressed: () =>
|
||||
context.read<LoginPageCubit>().passwordLogin(
|
||||
email: widget.emailController.text,
|
||||
password: widget.passwordController.text,
|
||||
),
|
||||
onPressed: () => unawaited(
|
||||
context.read<LoginPageCubit>().passwordLogin(
|
||||
email: widget.emailController.text,
|
||||
password: widget.passwordController.text,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Divider when both password and oAuth login is enabled
|
||||
if (state.features.hasOAuthLogin) const Divider(),
|
||||
|
@ -1,16 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
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/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/domain/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/domain/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/domain/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/domain/services/app_setting.service.dart';
|
||||
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||
import 'package:immich_mobile/domain/services/server_info.service.dart';
|
||||
import 'package:immich_mobile/domain/store_manager.dart';
|
||||
import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart';
|
||||
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
|
||||
import 'package:immich_mobile/presentation/modules/theme/states/app_theme.state.dart';
|
||||
import 'package:immich_mobile/presentation/router/router.dart';
|
||||
import 'package:immich_mobile/utils/immich_auth_interceptor.dart';
|
||||
import 'package:openapi/openapi.dart';
|
||||
|
||||
final di = GetIt.I;
|
||||
@ -29,12 +38,12 @@ class ServiceLocator {
|
||||
|
||||
// Init store
|
||||
di.registerFactory<IStoreRepository>(() => StoreDriftRepository(di()));
|
||||
// StoreManager populates its cache with a async gap, manually signalReady once the cache is populated.
|
||||
di.registerSingleton<StoreManager>(StoreManager(di()), signalsReady: true);
|
||||
// Logs
|
||||
di.registerFactory<ILogRepository>(() => LogDriftRepository(di()));
|
||||
// App Settings
|
||||
di.registerFactory<AppSettingService>(() => AppSettingService(di()));
|
||||
// User Repo
|
||||
di.registerFactory<IUserRepository>(() => UserDriftRepository(di()));
|
||||
// Login Service
|
||||
di.registerFactory<LoginService>(() => const LoginService());
|
||||
|
||||
@ -46,17 +55,35 @@ class ServiceLocator {
|
||||
di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di()));
|
||||
}
|
||||
|
||||
static void registerPostValidationServices(String endpoint) {
|
||||
static FutureOr<void> registerPostValidationServices(String endpoint) async {
|
||||
if (di.isRegistered<Openapi>()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final String deviceModel;
|
||||
if (Platform.isIOS) {
|
||||
deviceModel = (await deviceInfo.iosInfo).utsname.machine;
|
||||
} else {
|
||||
deviceModel = (await deviceInfo.androidInfo).model;
|
||||
}
|
||||
|
||||
// ====== DOMAIN
|
||||
|
||||
di.registerSingleton<Openapi>(
|
||||
Openapi(
|
||||
basePathOverride: endpoint,
|
||||
interceptors: [BearerAuthInterceptor()],
|
||||
dio: Dio(
|
||||
BaseOptions(
|
||||
baseUrl: endpoint,
|
||||
connectTimeout: const Duration(milliseconds: 5000),
|
||||
receiveTimeout: const Duration(milliseconds: 3000),
|
||||
headers: {
|
||||
'deviceModel': deviceModel,
|
||||
'deviceType': Platform.operatingSystem,
|
||||
},
|
||||
),
|
||||
),
|
||||
interceptors: [ImmichAuthInterceptor()],
|
||||
),
|
||||
);
|
||||
di.registerFactory<ServerInfoService>(() => ServerInfoService(di()));
|
||||
@ -67,4 +94,8 @@ class ServiceLocator {
|
||||
() => ServerFeatureConfigCubit(di()),
|
||||
);
|
||||
}
|
||||
|
||||
static void registerCurrentUser(User user) {
|
||||
di.registerSingleton<CurrentUserCubit>(CurrentUserCubit(user));
|
||||
}
|
||||
}
|
||||
|
@ -36,11 +36,16 @@ class Assets {
|
||||
}
|
||||
|
||||
class AssetGenImage {
|
||||
const AssetGenImage(this._assetName, {this.size = null});
|
||||
const AssetGenImage(
|
||||
this._assetName, {
|
||||
this.size,
|
||||
this.flavors = const {},
|
||||
});
|
||||
|
||||
final String _assetName;
|
||||
|
||||
final Size? size;
|
||||
final Set<String> flavors;
|
||||
|
||||
Image image({
|
||||
Key? key,
|
||||
|
@ -1,4 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Log messages stored in the DB
|
||||
const int kLogMessageLimit = 500;
|
||||
|
||||
/// Global ScaffoldMessengerKey to show snackbars
|
||||
final GlobalKey<ScaffoldMessengerState> kScafMessengerKey = GlobalKey();
|
||||
|
29
mobile-v2/lib/utils/immich_auth_interceptor.dart
Normal file
29
mobile-v2/lib/utils/immich_auth_interceptor.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:immich_mobile/presentation/router/router.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
|
||||
|
||||
class ImmichAuthInterceptor extends Interceptor with LogContext {
|
||||
String? _accessToken;
|
||||
|
||||
void setAccessToken(String token) => _accessToken = token;
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (_accessToken != null) {
|
||||
options.headers["x-immich-user-token"] = _accessToken;
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (response.statusCode == 401) {
|
||||
log.severe("Token expired. Logging user out");
|
||||
di<AppRouter>().replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
}
|
75
mobile-v2/lib/utils/log_manager.dart
Normal file
75
mobile-v2/lib/utils/log_manager.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// [LogManager] is a custom logger that is built on top of the [logging] package.
|
||||
/// The logs are written to the database and onto console, using `debugPrint` method.
|
||||
///
|
||||
/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property
|
||||
/// in the class.
|
||||
class LogManager {
|
||||
LogManager._();
|
||||
static final LogManager _instance = LogManager._();
|
||||
|
||||
// ignore: match-getter-setter-field-names
|
||||
static LogManager get I => _instance;
|
||||
|
||||
List<LogMessage> _msgBuffer = [];
|
||||
Timer? _timer;
|
||||
late StreamSubscription<LogRecord> _subscription;
|
||||
|
||||
void _onLogRecord(LogRecord record) {
|
||||
// Only print in development
|
||||
assert(() {
|
||||
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
||||
if (record.error != null && record.stackTrace != null) {
|
||||
debugPrint('${record.error}');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
final lm = LogMessage(
|
||||
logger: record.loggerName,
|
||||
content: record.message,
|
||||
level: record.level.toLogLevel(),
|
||||
createdAt: record.time,
|
||||
error: record.error?.toString(),
|
||||
stack: record.stackTrace?.toString(),
|
||||
);
|
||||
_msgBuffer.add(lm);
|
||||
|
||||
// delayed batch writing to database: increases performance when logging
|
||||
// messages in quick succession and reduces NAND wear
|
||||
_timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase);
|
||||
}
|
||||
|
||||
void _flushBufferToDatabase() {
|
||||
_timer = null;
|
||||
final buffer = _msgBuffer;
|
||||
_msgBuffer = [];
|
||||
di<ILogRepository>().addAll(buffer);
|
||||
}
|
||||
|
||||
void init() {
|
||||
_subscription = Logger.root.onRecord.listen(_onLogRecord);
|
||||
}
|
||||
|
||||
void updateLevel(LogLevel level) {
|
||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
}
|
||||
|
||||
void clearLogs() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_msgBuffer.clear();
|
||||
di<ILogRepository>().clear();
|
||||
}
|
||||
}
|
@ -4,5 +4,5 @@ import 'package:logging/logging.dart';
|
||||
mixin LogContext {
|
||||
@protected
|
||||
@nonVirtual
|
||||
Logger get log => Logger.detached(runtimeType.toString());
|
||||
Logger get log => Logger(runtimeType.toString());
|
||||
}
|
||||
|
@ -45,18 +45,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_route
|
||||
sha256: "878186aae276296bf1cfc0a02cd2788cfb473eb622e0f5e4293f40ecdf86d80d"
|
||||
sha256: a9001a90539ca3effc168f7e1029a5885c7326b9032c09ac895e303c1d137704
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
version: "8.3.0"
|
||||
auto_route_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: ba28133d3a3bf0a66772bcc98dade5843753cd9f1a8fb4802b842895515b67d3
|
||||
sha256: a21d7a936c917488653c972f62d884d8adcf8c5d37acc7cd24da33cf784546c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "8.1.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -241,30 +241,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
|
||||
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.3+1"
|
||||
version: "5.5.0+1"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd"
|
||||
sha256: "4e0ffee40d23f0b809e6cff1ad202886f51d629649073ed42d9cd1d194ea943e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.18.0"
|
||||
version: "2.19.1+1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde
|
||||
sha256: ac7647c6cedca99724ca300cff9181f6dd799428f8ed71f94159ed0528eaec26
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.18.0"
|
||||
version: "2.19.1"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -314,10 +338,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_adaptive_scaffold
|
||||
sha256: "794791e6fc0cc23e375d07ea987e76cf7f0eb7cd753532e45dc35b7e90575e2d"
|
||||
sha256: "56d4d81fe88ecffe8ae96b8d89a1ae793c0a85035bb9b74ff28f20eea0cdbdc2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.11"
|
||||
version: "0.1.11+1"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -330,26 +354,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_gen_core
|
||||
sha256: b9894396b2a790cc2d6eb3ed86e5e113aaed993765b21d4b981c9da4476e0f52
|
||||
sha256: d8e828ad015a8511624491b78ad8e3f86edb7993528b1613aefbb4ad95947795
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0+1"
|
||||
version: "5.6.0"
|
||||
flutter_gen_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_gen_runner
|
||||
sha256: b4c4c54e4dd89022f5e405fe96f16781be2dfbeabe8a70ccdf73b7af1302c655
|
||||
sha256: "931b03f77c164df0a4815aac0efc619a6ac8ec4cada55025119fca4894dada90"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0+1"
|
||||
version: "5.6.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "4.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -360,6 +384,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_web_auth_2
|
||||
sha256: "4d3d2fd3d26bf1a26b3beafd4b4b899c0ffe10dc99af25abc58ffe24e991133c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_web_auth_2_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_web_auth_2_platform_interface
|
||||
sha256: e8669e262005a8354389ba2971f0fc1c36188481234ff50d013aaf993f30f739
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -393,10 +433,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
hashcodes:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -409,10 +449,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -505,10 +545,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "4.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -537,10 +577,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: a2c78726048c755f0f90fd2b7c8799cd94338e2e9b7ab6498ae56503262c14bc
|
||||
sha256: "37f88057af06224cd99242bd9b5ceda8c1ebddfff67bd5e8432521910a3d4598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2762.0"
|
||||
version: "4.2771.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -640,10 +680,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
|
||||
sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
version: "2.2.7"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -672,10 +712,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -688,10 +728,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_manager
|
||||
sha256: "68d6099d07ce5033170f8368af8128a4555cf1d590a97242f83669552de989b1"
|
||||
sha256: "2f98fed8fede27eaf55021a1ce382609a715b52096a94a315f99ae33b6d2eaab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.2"
|
||||
photo_manager_image_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -789,10 +829,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: slang
|
||||
sha256: "0d8a8cbfd7858ed2bd9164a79bfb664ea83f1e124740b28acd0618757fc87ecc"
|
||||
sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.31.0"
|
||||
version: "3.31.1"
|
||||
slang_build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -829,26 +869,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
|
||||
sha256: "6d17989c0b06a5870b2190d391925186f944cb943e5262d0d3f778fcfca3bc6e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e"
|
||||
sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.23"
|
||||
version: "0.5.24"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f
|
||||
sha256: "3be52b4968fc2f098ba735863404756d2fe3ea0729cf006a5b5612618f74ca04"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.36.0"
|
||||
version: "0.37.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -933,18 +973,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "6.3.5"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -981,10 +1021,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1037,18 +1077,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1057,6 +1097,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
window_to_front:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: window_to_front
|
||||
sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -48,6 +48,9 @@ dependencies:
|
||||
url_launcher: ^6.3.0
|
||||
# plus_extensions
|
||||
package_info_plus: ^8.0.0
|
||||
device_info_plus: ^10.1.0
|
||||
# oauth login
|
||||
flutter_web_auth_2: ^3.1.2
|
||||
|
||||
openapi:
|
||||
path: openapi
|
||||
@ -57,7 +60,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
# Recommended lints
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_lints: ^4.0.0
|
||||
# Code generator
|
||||
build_runner: ^2.4.9
|
||||
# Database helper
|
||||
@ -67,7 +70,7 @@ dev_dependencies:
|
||||
# Localization generator
|
||||
slang_build_runner: ^3.30.0
|
||||
# Assets constant generator
|
||||
flutter_gen_runner: 5.5.0+1
|
||||
flutter_gen_runner: ^5.6.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
Loading…
x
Reference in New Issue
Block a user