add proper logging

This commit is contained in:
shenlong-tanwen 2024-08-21 23:43:48 +05:30
parent 1631df70e9
commit 75448ce56b
37 changed files with 923 additions and 224 deletions

View File

@ -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:

View File

@ -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",

View File

@ -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)();

View File

@ -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>()();

View File

@ -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};
}

View 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};
}

View File

@ -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});
}

View File

@ -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();
}

View 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);
}

View File

@ -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 ^

View File

@ -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;
}

View 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);
}
}
}

View File

@ -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 {},
);
}

View File

@ -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,

View File

@ -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,

View 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,
);
}
}

View File

@ -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) {

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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),
),
);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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,
),
),
),
);

View File

@ -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() {

View File

@ -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(),

View File

@ -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));
}
}

View File

@ -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,

View File

@ -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();

View 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);
}
}

View 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();
}
}

View File

@ -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());
}

View File

@ -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:

View File

@ -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