mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
more refactors and logs page handling
This commit is contained in:
parent
8f47645cdb
commit
a0afea04d8
@ -18,51 +18,68 @@ dart_code_metrics:
|
|||||||
- recommended
|
- recommended
|
||||||
rules:
|
rules:
|
||||||
# Common
|
# Common
|
||||||
- avoid-accessing-collections-by-constant-index
|
- arguments-ordering:
|
||||||
|
last:
|
||||||
|
- child
|
||||||
- avoid-accessing-other-classes-private-members
|
- avoid-accessing-other-classes-private-members
|
||||||
- avoid-cascade-after-if-null
|
- avoid-assigning-to-static-field
|
||||||
|
- avoid-assignments-as-conditions
|
||||||
|
- avoid-async-call-in-sync-function
|
||||||
- avoid-collapsible-if
|
- avoid-collapsible-if
|
||||||
- avoid-collection-methods-with-unrelated-types
|
- avoid-collection-equality-checks
|
||||||
- avoid-double-slash-imports
|
- avoid-collection-mutating-methods
|
||||||
- avoid-duplicate-cascades
|
- avoid-complex-loop-conditions
|
||||||
- avoid-duplicate-patterns
|
- avoid-declaring-call-method
|
||||||
- avoid-generics-shadowing
|
- avoid-extensions-on-records
|
||||||
|
- avoid-function-type-in-records
|
||||||
- avoid-global-state
|
- avoid-global-state
|
||||||
|
- avoid-inverted-boolean-checks
|
||||||
|
- avoid-late-final-reassignment
|
||||||
|
- avoid-local-functions
|
||||||
|
- avoid-negated-conditions
|
||||||
|
- avoid-nested-streams-and-futures
|
||||||
|
- avoid-referencing-subclasses
|
||||||
|
- binary-expression-operand-order
|
||||||
|
- move-variable-outside-iteration
|
||||||
|
- prefer-abstract-final-static-class
|
||||||
|
- prefer-early-return
|
||||||
|
- prefer-first
|
||||||
|
- prefer-immediate-return
|
||||||
|
- prefer-last
|
||||||
|
- prefer-simpler-boolean-expressions
|
||||||
|
- prefer-type-over-var
|
||||||
|
- use-existing-variable
|
||||||
# Flutter
|
# Flutter
|
||||||
- always-remove-listener
|
|
||||||
- avoid-border-all
|
- avoid-border-all
|
||||||
- avoid-empty-setstate
|
|
||||||
- avoid-expanded-as-spacer
|
- avoid-expanded-as-spacer
|
||||||
- avoid-incomplete-copy-with
|
|
||||||
- avoid-inherited-widget-in-initstate
|
- avoid-inherited-widget-in-initstate
|
||||||
- avoid-late-context
|
- avoid-late-context
|
||||||
- avoid-recursive-widget-calls
|
|
||||||
- avoid-returning-widgets
|
- avoid-returning-widgets
|
||||||
- avoid-shrink-wrap-in-lists
|
- avoid-shrink-wrap-in-lists
|
||||||
- avoid-single-child-column-or-row
|
- avoid-single-child-column-or-row
|
||||||
- avoid-state-constructors
|
|
||||||
- avoid-stateless-widget-initialized-fields
|
- avoid-stateless-widget-initialized-fields
|
||||||
- avoid-unnecessary-overrides-in-state
|
|
||||||
- avoid-unnecessary-stateful-widgets
|
|
||||||
- avoid-wrapping-in-padding
|
- avoid-wrapping-in-padding
|
||||||
- dispose-fields
|
|
||||||
- prefer-const-border-radius
|
- prefer-const-border-radius
|
||||||
|
- prefer-correct-callback-field-name: false
|
||||||
- prefer-correct-edge-insets-constructor
|
- prefer-correct-edge-insets-constructor
|
||||||
- prefer-dedicated-media-query-methods
|
|
||||||
- prefer-define-hero-tag
|
- prefer-define-hero-tag
|
||||||
- prefer-extracting-callbacks
|
- prefer-extracting-callbacks
|
||||||
|
- prefer-for-loop-in-children
|
||||||
|
- prefer-match-file-name: false
|
||||||
|
- prefer-single-widget-per-file:
|
||||||
|
ignore-private-widgets: true
|
||||||
|
exclude:
|
||||||
|
- lib/presentation/**/*.page.dart
|
||||||
- prefer-sliver-prefix
|
- prefer-sliver-prefix
|
||||||
- prefer-text-rich
|
- prefer-text-rich
|
||||||
- prefer-using-list-view
|
- prefer-using-list-view
|
||||||
- proper-super-calls
|
- prefer-widget-private-members:
|
||||||
- use-setstate-synchronously
|
ignore-static: true
|
||||||
- prefer-match-file-name: false
|
# get-it
|
||||||
- avoid-passing-self-as-argument:
|
- avoid-functions-in-register-singleton
|
||||||
exclude:
|
# bloc
|
||||||
- lib/domain/repositories/**
|
- avoid-empty-build-when
|
||||||
- prefer-single-widget-per-file:
|
- avoid-passing-bloc-to-bloc
|
||||||
ignore-private-widgets: true
|
|
||||||
- prefer-correct-callback-field-name: false
|
|
||||||
|
|
||||||
custom_lint:
|
custom_lint:
|
||||||
rules:
|
rules:
|
||||||
|
@ -37,6 +37,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Logs"
|
"title": "Logs",
|
||||||
|
"no_logs": "No logs available"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Loading"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,8 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- permission_handler_apple (9.3.0):
|
||||||
|
- Flutter
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@ -42,6 +44,7 @@ DEPENDENCIES:
|
|||||||
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||||
@ -62,6 +65,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
permission_handler_apple:
|
||||||
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
@ -72,11 +77,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_web_auth_2: 051cf9f5dc366f31b5dcc4e2952c2b954767be8a
|
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||||
|
@ -193,6 +193,7 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
25B7EF9822CD7FF54B86E39D /* [CP] Embed Pods Frameworks */,
|
25B7EF9822CD7FF54B86E39D /* [CP] Embed Pods Frameworks */,
|
||||||
|
4D9CA4C7FEB0AD3C30F4C600 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -297,6 +298,23 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
|
4D9CA4C7FEB0AD3C30F4C600 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
|
@ -4,14 +4,14 @@ import 'package:immich_mobile/domain/models/album.model.dart';
|
|||||||
|
|
||||||
abstract interface class IAlbumRepository {
|
abstract interface class IAlbumRepository {
|
||||||
/// Inserts a new album into the DB or updates if existing and returns the updated data
|
/// Inserts a new album into the DB or updates if existing and returns the updated data
|
||||||
FutureOr<Album?> upsert(Album album);
|
Future<Album?> upsert(Album album);
|
||||||
|
|
||||||
/// Fetch all albums
|
/// Fetch all albums
|
||||||
FutureOr<List<Album>> getAll({bool localOnly, bool remoteOnly});
|
Future<List<Album>> getAll({bool localOnly, bool remoteOnly});
|
||||||
|
|
||||||
/// Removes album with the given [id]
|
/// Removes album with the given [id]
|
||||||
FutureOr<void> deleteId(int id);
|
Future<void> deleteId(int id);
|
||||||
|
|
||||||
/// Removes all albums
|
/// Removes all albums
|
||||||
FutureOr<void> deleteAll();
|
Future<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,17 @@ import 'package:immich_mobile/domain/models/asset.model.dart';
|
|||||||
|
|
||||||
abstract interface class IAlbumToAssetRepository {
|
abstract interface class IAlbumToAssetRepository {
|
||||||
/// Link a list of assetIds to the given albumId
|
/// Link a list of assetIds to the given albumId
|
||||||
FutureOr<bool> addAssetIds(int albumId, Iterable<int> assetIds);
|
Future<bool> addAssetIds(int albumId, Iterable<int> assetIds);
|
||||||
|
|
||||||
/// Returns assets that are only part of the given album and nothing else
|
/// Returns assets that are only part of the given album and nothing else
|
||||||
FutureOr<List<int>> getAssetIdsOnlyInAlbum(int albumId);
|
Future<List<int>> getAssetIdsOnlyInAlbum(int albumId);
|
||||||
|
|
||||||
/// Returns the assets for the given [albumId]
|
/// Returns the assets for the given [albumId]
|
||||||
FutureOr<List<Asset>> getAssetsForAlbum(int albumId);
|
Future<List<Asset>> getAssetsForAlbum(int albumId);
|
||||||
|
|
||||||
/// Removes album with the given [albumId]
|
/// Removes album with the given [albumId]
|
||||||
FutureOr<void> deleteAlbumId(int albumId);
|
Future<void> deleteAlbumId(int albumId);
|
||||||
|
|
||||||
/// Removes all album to asset mappings
|
/// Removes all album to asset mappings
|
||||||
FutureOr<void> deleteAll();
|
Future<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import 'package:immich_mobile/domain/models/album_etag.model.dart';
|
|||||||
|
|
||||||
abstract interface class IAlbumETagRepository {
|
abstract interface class IAlbumETagRepository {
|
||||||
/// Inserts or updates the album etag for the given [albumId]
|
/// Inserts or updates the album etag for the given [albumId]
|
||||||
FutureOr<bool> upsert(AlbumETag albumETag);
|
Future<bool> upsert(AlbumETag albumETag);
|
||||||
|
|
||||||
/// Fetches the album etag for the given [albumId]
|
/// Fetches the album etag for the given [albumId]
|
||||||
FutureOr<AlbumETag?> get(int albumId);
|
Future<AlbumETag?> get(int albumId);
|
||||||
|
|
||||||
/// Removes all album eTags
|
/// Removes all album eTags
|
||||||
FutureOr<void> deleteAll();
|
Future<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,23 @@ import 'package:immich_mobile/domain/models/asset.model.dart';
|
|||||||
|
|
||||||
abstract interface class IAssetRepository {
|
abstract interface class IAssetRepository {
|
||||||
/// Batch upsert asset
|
/// Batch upsert asset
|
||||||
FutureOr<bool> upsertAll(Iterable<Asset> assets);
|
Future<bool> upsertAll(Iterable<Asset> assets);
|
||||||
|
|
||||||
/// Removes assets with the [localIds]
|
/// Removes assets with the [localIds]
|
||||||
FutureOr<List<Asset>> getForLocalIds(Iterable<String> localIds);
|
Future<List<Asset>> getForLocalIds(Iterable<String> localIds);
|
||||||
|
|
||||||
/// Removes assets with the [remoteIds]
|
/// Removes assets with the [remoteIds]
|
||||||
FutureOr<List<Asset>> getForRemoteIds(Iterable<String> remoteIds);
|
Future<List<Asset>> getForRemoteIds(Iterable<String> remoteIds);
|
||||||
|
|
||||||
/// Get assets with the [hashes]
|
/// Get assets with the [hashes]
|
||||||
FutureOr<List<Asset>> getForHashes(Iterable<String> hashes);
|
Future<List<Asset>> getForHashes(Iterable<String> hashes);
|
||||||
|
|
||||||
/// Fetch assets from the [offset] with the [limit]
|
/// Fetch assets from the [offset] with the [limit]
|
||||||
FutureOr<List<Asset>> getAll({int? offset, int? limit});
|
Future<List<Asset>> getAll({int? offset, int? limit});
|
||||||
|
|
||||||
/// Removes assets with the given [ids]
|
/// Removes assets with the given [ids]
|
||||||
FutureOr<void> deleteIds(Iterable<int> ids);
|
Future<void> deleteIds(Iterable<int> ids);
|
||||||
|
|
||||||
/// Removes all assets
|
/// Removes all assets
|
||||||
FutureOr<bool> deleteAll();
|
Future<bool> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,13 @@ import 'package:immich_mobile/domain/models/asset.model.dart';
|
|||||||
|
|
||||||
abstract interface class IDeviceAlbumRepository {
|
abstract interface class IDeviceAlbumRepository {
|
||||||
/// Fetches all [Album] from device
|
/// Fetches all [Album] from device
|
||||||
FutureOr<List<Album>> getAll();
|
Future<List<Album>> getAll();
|
||||||
|
|
||||||
/// Returns the number of asset in the album
|
/// Returns the number of asset in the album
|
||||||
FutureOr<int> getAssetCount(String albumId);
|
Future<int> getAssetCount(String albumId);
|
||||||
|
|
||||||
/// Fetches assets belong to the albumId
|
/// Fetches assets belong to the albumId
|
||||||
FutureOr<List<Asset>> getAssetsForAlbum(
|
Future<List<Asset>> getAssetsForAlbum(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
int start = 0,
|
int start = 0,
|
||||||
int end = 0x7fffffffffffffff,
|
int end = 0x7fffffffffffffff,
|
||||||
|
@ -8,10 +8,10 @@ import 'package:immich_mobile/utils/constants/globals.dart';
|
|||||||
|
|
||||||
abstract interface class IDeviceAssetRepository<T> {
|
abstract interface class IDeviceAssetRepository<T> {
|
||||||
/// Fetches the [File] for the given [assetId]
|
/// Fetches the [File] for the given [assetId]
|
||||||
FutureOr<File?> getOriginalFile(String assetId);
|
Future<File?> getOriginalFile(String assetId);
|
||||||
|
|
||||||
/// Fetches the thumbnail for the given [assetId]
|
/// Fetches the thumbnail for the given [assetId]
|
||||||
FutureOr<Uint8List?> getThumbnail(
|
Future<Uint8List?> getThumbnail(
|
||||||
String assetId, {
|
String assetId, {
|
||||||
int width = kGridThumbnailSize,
|
int width = kGridThumbnailSize,
|
||||||
int height = kGridThumbnailSize,
|
int height = kGridThumbnailSize,
|
||||||
|
@ -4,11 +4,11 @@ import 'package:immich_mobile/domain/models/device_asset_hash.model.dart';
|
|||||||
|
|
||||||
abstract interface class IDeviceAssetToHashRepository {
|
abstract interface class IDeviceAssetToHashRepository {
|
||||||
/// Add a new device asset to hash entry
|
/// Add a new device asset to hash entry
|
||||||
FutureOr<bool> upsertAll(Iterable<DeviceAssetToHash> assetHash);
|
Future<bool> upsertAll(Iterable<DeviceAssetToHash> assetHash);
|
||||||
|
|
||||||
// Gets the asset with the local ID from the device
|
// Gets the asset with the local ID from the device
|
||||||
FutureOr<List<DeviceAssetToHash>> getForIds(Iterable<String> localIds);
|
Future<List<DeviceAssetToHash>> getForIds(Iterable<String> localIds);
|
||||||
|
|
||||||
/// Removes assets with the given [ids]
|
/// Removes assets with the given [ids]
|
||||||
FutureOr<void> deleteIds(Iterable<int> ids);
|
Future<void> deleteIds(Iterable<int> ids);
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,17 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
|||||||
|
|
||||||
abstract interface class ILogRepository {
|
abstract interface class ILogRepository {
|
||||||
/// Inserts a new log into the DB
|
/// Inserts a new log into the DB
|
||||||
FutureOr<bool> create(LogMessage log);
|
Future<bool> create(LogMessage log);
|
||||||
|
|
||||||
/// Bulk insert logs into DB
|
/// Bulk insert logs into DB
|
||||||
FutureOr<bool> createAll(Iterable<LogMessage> log);
|
Future<bool> createAll(Iterable<LogMessage> log);
|
||||||
|
|
||||||
/// Fetches all logs
|
/// Fetches all logs
|
||||||
FutureOr<List<LogMessage>> getAll();
|
Future<List<LogMessage>> getAll();
|
||||||
|
|
||||||
/// Clears all logs
|
/// Clears all logs
|
||||||
FutureOr<bool> deleteAll();
|
Future<bool> deleteAll();
|
||||||
|
|
||||||
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
|
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
|
||||||
FutureOr<void> truncate({int limit = 250});
|
Future<void> truncate({int limit = 250});
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,15 @@ abstract class IStoreConverter<T, U> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract interface class IStoreRepository {
|
abstract interface class IStoreRepository {
|
||||||
FutureOr<bool> upsert<T, U>(StoreKey<T, U> key, T value);
|
Future<bool> upsert<T, U>(StoreKey<T, U> key, T value);
|
||||||
|
|
||||||
FutureOr<T> get<T, U>(StoreKey<T, U> key);
|
Future<T> get<T, U>(StoreKey<T, U> key);
|
||||||
|
|
||||||
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key);
|
Future<T?> tryGet<T, U>(StoreKey<T, U> key);
|
||||||
|
|
||||||
Stream<T?> watch<T, U>(StoreKey<T, U> key);
|
Stream<T?> watch<T, U>(StoreKey<T, U> key);
|
||||||
|
|
||||||
FutureOr<void> delete(StoreKey key);
|
Future<void> delete(StoreKey key);
|
||||||
|
|
||||||
FutureOr<void> deleteAll();
|
Future<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
|
|
||||||
abstract interface class IUserRepository {
|
abstract interface class IUserRepository {
|
||||||
/// Insert user
|
/// Insert user
|
||||||
FutureOr<bool> upsert(User user);
|
Future<bool> upsert(User user);
|
||||||
|
|
||||||
/// Fetches user
|
/// Fetches user
|
||||||
FutureOr<User?> getForId(String userId);
|
Future<User?> getForId(String userId);
|
||||||
|
|
||||||
/// Removes all users
|
/// Removes all users
|
||||||
FutureOr<void> deleteAll();
|
Future<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -71,8 +71,8 @@ class Asset {
|
|||||||
createdTime: createdTime ?? this.createdTime,
|
createdTime: createdTime ?? this.createdTime,
|
||||||
modifiedTime: modifiedTime ?? this.modifiedTime,
|
modifiedTime: modifiedTime ?? this.modifiedTime,
|
||||||
duration: duration ?? this.duration,
|
duration: duration ?? this.duration,
|
||||||
localId: localId != null ? localId() : this.localId,
|
localId: localId == null ? this.localId : localId(),
|
||||||
remoteId: remoteId != null ? remoteId() : this.remoteId,
|
remoteId: remoteId == null ? this.remoteId : remoteId(),
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -89,20 +89,20 @@ class Asset {
|
|||||||
if (newAsset.modifiedTime.isAfter(existingAsset.modifiedTime)) {
|
if (newAsset.modifiedTime.isAfter(existingAsset.modifiedTime)) {
|
||||||
return newAsset.copyWith(
|
return newAsset.copyWith(
|
||||||
id: newAsset.id ?? existingAsset.id,
|
id: newAsset.id ?? existingAsset.id,
|
||||||
|
height: newAsset.height ?? existingAsset.height,
|
||||||
|
width: newAsset.width ?? existingAsset.width,
|
||||||
|
createdTime: oldestCreationTime,
|
||||||
localId: () => existingAsset.localId ?? newAsset.localId,
|
localId: () => existingAsset.localId ?? newAsset.localId,
|
||||||
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
|
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
|
||||||
width: newAsset.width ?? existingAsset.width,
|
|
||||||
height: newAsset.height ?? existingAsset.height,
|
|
||||||
createdTime: oldestCreationTime,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingAsset.copyWith(
|
return existingAsset.copyWith(
|
||||||
|
height: existingAsset.height ?? newAsset.height,
|
||||||
|
width: existingAsset.width ?? newAsset.width,
|
||||||
|
createdTime: oldestCreationTime,
|
||||||
localId: () => existingAsset.localId ?? newAsset.localId,
|
localId: () => existingAsset.localId ?? newAsset.localId,
|
||||||
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
|
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
|
||||||
width: existingAsset.width ?? newAsset.width,
|
|
||||||
height: existingAsset.height ?? newAsset.height,
|
|
||||||
createdTime: oldestCreationTime,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ import 'package:immich_mobile/domain/models/render_list_element.model.dart';
|
|||||||
class RenderList {
|
class RenderList {
|
||||||
final List<RenderListElement> elements;
|
final List<RenderListElement> elements;
|
||||||
late final int totalCount;
|
late final int totalCount;
|
||||||
|
final DateTime modifiedTime;
|
||||||
|
|
||||||
RenderList({required this.elements}) {
|
RenderList({required this.elements, required this.modifiedTime}) {
|
||||||
final lastAssetElement =
|
final lastAssetElement =
|
||||||
elements.whereType<RenderListAssetElement>().lastOrNull;
|
elements.whereType<RenderListAssetElement>().lastOrNull;
|
||||||
if (lastAssetElement == null) {
|
if (lastAssetElement == null) {
|
||||||
@ -16,6 +17,21 @@ class RenderList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory RenderList.empty() {
|
factory RenderList.empty() {
|
||||||
return RenderList(elements: []);
|
return RenderList(elements: [], modifiedTime: DateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'RenderList(totalCount: $totalCount, modifiedTime: $modifiedTime)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant RenderList other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.totalCount == totalCount &&
|
||||||
|
other.modifiedTime.isAtSameMomentAs(modifiedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => totalCount.hashCode ^ modifiedTime.hashCode;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class AlbumRepository with LogMixin implements IAlbumRepository {
|
|||||||
const AlbumRepository({required DriftDatabaseRepository db}) : _db = db;
|
const AlbumRepository({required DriftDatabaseRepository db}) : _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Album?> upsert(Album album) async {
|
Future<Album?> upsert(Album album) async {
|
||||||
try {
|
try {
|
||||||
final albumData = _toEntity(album);
|
final albumData = _toEntity(album);
|
||||||
final data = await _db.album.insertReturningOrNull(
|
final data = await _db.album.insertReturningOrNull(
|
||||||
@ -30,7 +30,7 @@ class AlbumRepository with LogMixin implements IAlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Album>> getAll({
|
Future<List<Album>> getAll({
|
||||||
bool localOnly = false,
|
bool localOnly = false,
|
||||||
bool remoteOnly = false,
|
bool remoteOnly = false,
|
||||||
}) async {
|
}) async {
|
||||||
@ -49,12 +49,12 @@ class AlbumRepository with LogMixin implements IAlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteId(int id) async {
|
Future<void> deleteId(int id) async {
|
||||||
await _db.album.deleteWhere((row) => row.id.equals(id));
|
await _db.album.deleteWhere((row) => row.id.equals(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAll() async {
|
Future<void> deleteAll() async {
|
||||||
await _db.album.deleteAll();
|
await _db.album.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,11 +62,11 @@ class AlbumRepository with LogMixin implements IAlbumRepository {
|
|||||||
AlbumCompanion _toEntity(Album album) {
|
AlbumCompanion _toEntity(Album album) {
|
||||||
return AlbumCompanion.insert(
|
return AlbumCompanion.insert(
|
||||||
id: Value.absentIfNull(album.id),
|
id: Value.absentIfNull(album.id),
|
||||||
localId: Value(album.localId),
|
|
||||||
remoteId: Value(album.remoteId),
|
|
||||||
name: album.name,
|
name: album.name,
|
||||||
modifiedTime: Value(album.modifiedTime),
|
modifiedTime: Value(album.modifiedTime),
|
||||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||||
|
localId: Value(album.localId),
|
||||||
|
remoteId: Value(album.remoteId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class AlbumToAssetRepository with LogMixin implements IAlbumToAssetRepository {
|
|||||||
: _db = db;
|
: _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> addAssetIds(int albumId, Iterable<int> assetIds) async {
|
Future<bool> addAssetIds(int albumId, Iterable<int> assetIds) async {
|
||||||
try {
|
try {
|
||||||
await _db.albumToAsset.insertAll(
|
await _db.albumToAsset.insertAll(
|
||||||
assetIds.map(
|
assetIds.map(
|
||||||
@ -33,14 +33,14 @@ class AlbumToAssetRepository with LogMixin implements IAlbumToAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<int>> getAssetIdsOnlyInAlbum(int albumId) async {
|
Future<List<int>> getAssetIdsOnlyInAlbum(int albumId) async {
|
||||||
final assetId = _db.asset.id;
|
final assetId = _db.asset.id;
|
||||||
final query = _db.asset.selectOnly()
|
final query = _db.asset.selectOnly()
|
||||||
..addColumns([assetId])
|
..addColumns([assetId])
|
||||||
..join([
|
..join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.albumToAsset,
|
_db.albumToAsset,
|
||||||
_db.albumToAsset.assetId.equalsExp(_db.asset.id) &
|
_db.albumToAsset.assetId.equalsExp(assetId) &
|
||||||
_db.asset.remoteId.isNull(),
|
_db.asset.remoteId.isNull(),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
@ -55,7 +55,7 @@ class AlbumToAssetRepository with LogMixin implements IAlbumToAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Asset>> getAssetsForAlbum(int albumId) async {
|
Future<List<Asset>> getAssetsForAlbum(int albumId) async {
|
||||||
final query = _db.asset.select().join([
|
final query = _db.asset.select().join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.albumToAsset,
|
_db.albumToAsset,
|
||||||
@ -72,12 +72,12 @@ class AlbumToAssetRepository with LogMixin implements IAlbumToAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAlbumId(int albumId) async {
|
Future<void> deleteAlbumId(int albumId) async {
|
||||||
await _db.albumToAsset.deleteWhere((row) => row.albumId.equals(albumId));
|
await _db.albumToAsset.deleteWhere((row) => row.albumId.equals(albumId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAll() async {
|
Future<void> deleteAll() async {
|
||||||
await _db.albumToAsset.deleteAll();
|
await _db.albumToAsset.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class AlbumETagRepository with LogMixin implements IAlbumETagRepository {
|
|||||||
const AlbumETagRepository({required DriftDatabaseRepository db}) : _db = db;
|
const AlbumETagRepository({required DriftDatabaseRepository db}) : _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> upsert(AlbumETag albumETag) async {
|
Future<bool> upsert(AlbumETag albumETag) async {
|
||||||
try {
|
try {
|
||||||
final entity = _toEntity(albumETag);
|
final entity = _toEntity(albumETag);
|
||||||
await _db.albumETag.insertOne(
|
await _db.albumETag.insertOne(
|
||||||
@ -28,14 +28,14 @@ class AlbumETagRepository with LogMixin implements IAlbumETagRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<AlbumETag?> get(int albumId) async {
|
Future<AlbumETag?> get(int albumId) async {
|
||||||
final query = _db.albumETag.select()
|
final query = _db.albumETag.select()
|
||||||
..where((r) => r.albumId.equals(albumId));
|
..where((r) => r.albumId.equals(albumId));
|
||||||
return await query.map(_toModel).getSingleOrNull();
|
return await query.map(_toModel).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAll() async {
|
Future<void> deleteAll() async {
|
||||||
await _db.albumETag.deleteAll();
|
await _db.albumETag.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,17 +43,17 @@ class AlbumETagRepository with LogMixin implements IAlbumETagRepository {
|
|||||||
AlbumETagCompanion _toEntity(AlbumETag albumETag) {
|
AlbumETagCompanion _toEntity(AlbumETag albumETag) {
|
||||||
return AlbumETagCompanion.insert(
|
return AlbumETagCompanion.insert(
|
||||||
id: Value.absentIfNull(albumETag.id),
|
id: Value.absentIfNull(albumETag.id),
|
||||||
modifiedTime: Value(albumETag.modifiedTime),
|
|
||||||
albumId: albumETag.albumId,
|
albumId: albumETag.albumId,
|
||||||
|
modifiedTime: Value(albumETag.modifiedTime),
|
||||||
assetCount: Value(albumETag.assetCount),
|
assetCount: Value(albumETag.assetCount),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumETag _toModel(AlbumETagData albumETag) {
|
AlbumETag _toModel(AlbumETagData albumETag) {
|
||||||
return AlbumETag(
|
return AlbumETag(
|
||||||
|
id: albumETag.id,
|
||||||
albumId: albumETag.albumId,
|
albumId: albumETag.albumId,
|
||||||
assetCount: albumETag.assetCount,
|
assetCount: albumETag.assetCount,
|
||||||
modifiedTime: albumETag.modifiedTime,
|
modifiedTime: albumETag.modifiedTime,
|
||||||
id: albumETag.id,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,16 +32,16 @@ class SyncApiRepository with LogMixin implements ISyncApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Asset _fromAssetResponseDto(AssetResponseDto dto) => Asset(
|
Asset _fromAssetResponseDto(AssetResponseDto dto) => Asset(
|
||||||
remoteId: dto.id,
|
name: dto.originalFileName,
|
||||||
createdTime: dto.fileCreatedAt,
|
hash: dto.checksum,
|
||||||
duration: dto.duration.tryParseInt() ?? 0,
|
|
||||||
height: dto.exifInfo?.exifImageHeight?.toInt(),
|
height: dto.exifInfo?.exifImageHeight?.toInt(),
|
||||||
width: dto.exifInfo?.exifImageWidth?.toInt(),
|
width: dto.exifInfo?.exifImageWidth?.toInt(),
|
||||||
hash: dto.checksum,
|
|
||||||
name: dto.originalFileName,
|
|
||||||
livePhotoVideoId: dto.livePhotoVideoId,
|
|
||||||
modifiedTime: dto.fileModifiedAt,
|
|
||||||
type: _toAssetType(dto.type),
|
type: _toAssetType(dto.type),
|
||||||
|
createdTime: dto.fileCreatedAt,
|
||||||
|
modifiedTime: dto.fileModifiedAt,
|
||||||
|
duration: dto.duration.tryParseInt() ?? 0,
|
||||||
|
remoteId: dto.id,
|
||||||
|
livePhotoVideoId: dto.livePhotoVideoId,
|
||||||
);
|
);
|
||||||
|
|
||||||
AssetType _toAssetType(AssetTypeEnum type) => switch (type) {
|
AssetType _toAssetType(AssetTypeEnum type) => switch (type) {
|
||||||
|
@ -16,16 +16,16 @@ class AssetRepository with LogMixin implements IAssetRepository {
|
|||||||
@override
|
@override
|
||||||
Future<bool> upsertAll(Iterable<Asset> assets) async {
|
Future<bool> upsertAll(Iterable<Asset> assets) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) {
|
await _db.txn(() async => await _db.batch((batch) {
|
||||||
final rows = assets.map(_toEntity);
|
final rows = assets.map(_toEntity);
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
batch.insert(
|
batch.insert(
|
||||||
_db.asset,
|
_db.asset,
|
||||||
row,
|
row,
|
||||||
onConflict: DoUpdate((_) => row, target: [_db.asset.hash]),
|
onConflict: DoUpdate((_) => row, target: [_db.asset.hash]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@ -85,7 +85,7 @@ class AssetRepository with LogMixin implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteIds(Iterable<int> ids) async {
|
Future<void> deleteIds(Iterable<int> ids) async {
|
||||||
await _db.asset.deleteWhere((row) => row.id.isIn(ids));
|
await _db.asset.deleteWhere((row) => row.id.isIn(ids));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,16 +93,16 @@ class AssetRepository with LogMixin implements IAssetRepository {
|
|||||||
AssetCompanion _toEntity(Asset asset) {
|
AssetCompanion _toEntity(Asset asset) {
|
||||||
return AssetCompanion.insert(
|
return AssetCompanion.insert(
|
||||||
id: Value.absentIfNull(asset.id),
|
id: Value.absentIfNull(asset.id),
|
||||||
localId: Value(asset.localId),
|
|
||||||
remoteId: Value(asset.remoteId),
|
|
||||||
name: asset.name,
|
name: asset.name,
|
||||||
hash: asset.hash,
|
hash: asset.hash,
|
||||||
height: Value(asset.height),
|
height: Value(asset.height),
|
||||||
width: Value(asset.width),
|
width: Value(asset.width),
|
||||||
type: asset.type,
|
type: asset.type,
|
||||||
createdTime: asset.createdTime,
|
createdTime: asset.createdTime,
|
||||||
duration: Value(asset.duration),
|
|
||||||
modifiedTime: Value(asset.modifiedTime),
|
modifiedTime: Value(asset.modifiedTime),
|
||||||
|
duration: Value(asset.duration),
|
||||||
|
localId: Value(asset.localId),
|
||||||
|
remoteId: Value(asset.remoteId),
|
||||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository
|
|||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
onCreate: (m) => m.createAll(),
|
onCreate: (m) => m.createAll(),
|
||||||
|
// ignore: no-empty-block
|
||||||
|
onUpgrade: (m, from, to) async {},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
await validateDatabaseSchema();
|
await validateDatabaseSchema();
|
||||||
@ -52,8 +54,6 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository
|
|||||||
await customStatement('PRAGMA journal_mode = WAL');
|
await customStatement('PRAGMA journal_mode = WAL');
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
},
|
},
|
||||||
// ignore: no-empty-block
|
|
||||||
onUpgrade: (m, from, to) async {},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -52,17 +52,17 @@ class DeviceAlbumRepository with LogMixin implements IDeviceAlbumRepository {
|
|||||||
return await AssetPathEntity.fromId(
|
return await AssetPathEntity.fromId(
|
||||||
albumId,
|
albumId,
|
||||||
filterOption: FilterOptionGroup(
|
filterOption: FilterOptionGroup(
|
||||||
containsPathModified: true,
|
|
||||||
orders: orderByModificationDate
|
|
||||||
? [const OrderOption(type: OrderOptionType.updateDate)]
|
|
||||||
: [],
|
|
||||||
imageOption: const FilterOption(needTitle: true),
|
imageOption: const FilterOption(needTitle: true),
|
||||||
videoOption: const FilterOption(needTitle: true),
|
videoOption: const FilterOption(needTitle: true),
|
||||||
|
containsPathModified: true,
|
||||||
updateTimeCond: DateTimeCond(
|
updateTimeCond: DateTimeCond(
|
||||||
min: modifiedFrom ?? DateTime.utc(-271820),
|
min: modifiedFrom ?? DateTime.utc(-271820),
|
||||||
max: modifiedUntil ?? DateTime.utc(275760),
|
max: modifiedUntil ?? DateTime.utc(275760),
|
||||||
ignore: modifiedFrom != null || modifiedUntil != null,
|
ignore: modifiedFrom != null || modifiedUntil != null,
|
||||||
),
|
),
|
||||||
|
orders: orderByModificationDate
|
||||||
|
? [const OrderOption(type: OrderOptionType.updateDate)]
|
||||||
|
: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,25 +16,23 @@ class DeviceAssetRepository
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Asset> toAsset(ph.AssetEntity entity) async {
|
Future<Asset> toAsset(ph.AssetEntity entity) async {
|
||||||
var asset = Asset(
|
return Asset(
|
||||||
hash: '',
|
|
||||||
name: await entity.titleAsync,
|
name: await entity.titleAsync,
|
||||||
type: _toAssetType(entity.type),
|
hash: '',
|
||||||
createdTime: entity.createDateTime,
|
|
||||||
modifiedTime: entity.modifiedDateTime,
|
|
||||||
duration: entity.duration,
|
|
||||||
height: entity.height,
|
height: entity.height,
|
||||||
width: entity.width,
|
width: entity.width,
|
||||||
|
type: _toAssetType(entity.type),
|
||||||
|
createdTime: entity.createDateTime.year == 1970
|
||||||
|
? entity.modifiedDateTime
|
||||||
|
: entity.createDateTime,
|
||||||
|
modifiedTime: entity.modifiedDateTime,
|
||||||
|
duration: entity.duration,
|
||||||
localId: entity.id,
|
localId: entity.id,
|
||||||
);
|
);
|
||||||
if (asset.createdTime.year == 1970) {
|
|
||||||
asset = asset.copyWith(createdTime: asset.modifiedTime);
|
|
||||||
}
|
|
||||||
return asset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<File?> getOriginalFile(String localId) async {
|
Future<File?> getOriginalFile(String localId) async {
|
||||||
try {
|
try {
|
||||||
final entity = await ph.AssetEntity.fromId(localId);
|
final entity = await ph.AssetEntity.fromId(localId);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
@ -49,7 +47,7 @@ class DeviceAssetRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Uint8List?> getThumbnail(
|
Future<Uint8List?> getThumbnail(
|
||||||
String assetId, {
|
String assetId, {
|
||||||
int width = kGridThumbnailSize,
|
int width = kGridThumbnailSize,
|
||||||
int height = kGridThumbnailSize,
|
int height = kGridThumbnailSize,
|
||||||
|
@ -16,12 +16,13 @@ class DeviceAssetToHashRepository
|
|||||||
: _db = db;
|
: _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> upsertAll(Iterable<DeviceAssetToHash> assetHash) async {
|
Future<bool> upsertAll(Iterable<DeviceAssetToHash> assetHash) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) => batch.insertAllOnConflictUpdate(
|
await _db.txn(() async =>
|
||||||
_db.deviceAssetToHash,
|
await _db.batch((batch) => batch.insertAllOnConflictUpdate(
|
||||||
assetHash.map(_toEntity),
|
_db.deviceAssetToHash,
|
||||||
));
|
assetHash.map(_toEntity),
|
||||||
|
)));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@ -38,7 +39,7 @@ class DeviceAssetToHashRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteIds(Iterable<int> ids) async {
|
Future<void> deleteIds(Iterable<int> ids) async {
|
||||||
await _db.deviceAssetToHash.deleteWhere((row) => row.id.isIn(ids));
|
await _db.deviceAssetToHash.deleteWhere((row) => row.id.isIn(ids));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ class LogRepository implements ILogRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<LogMessage>> getAll() async {
|
Future<List<LogMessage>> getAll() async {
|
||||||
return await _db.managers.logs.map(_toModel).get();
|
return await _db.managers.logs
|
||||||
|
.orderBy((o) => o.createdAt.desc())
|
||||||
|
.map(_toModel)
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,14 +26,14 @@ class LogRepository implements ILogRepository {
|
|||||||
if (totalCount > limit) {
|
if (totalCount > limit) {
|
||||||
final rowsToDelete = totalCount - limit;
|
final rowsToDelete = totalCount - limit;
|
||||||
await _db.managers.logs
|
await _db.managers.logs
|
||||||
.orderBy((o) => o.createdAt.desc())
|
.orderBy((o) => o.createdAt.asc())
|
||||||
.limit(rowsToDelete)
|
.limit(rowsToDelete)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> create(LogMessage log) async {
|
Future<bool> create(LogMessage log) async {
|
||||||
try {
|
try {
|
||||||
await _db.logs.insertOne(_toEntity(log));
|
await _db.logs.insertOne(_toEntity(log));
|
||||||
return true;
|
return true;
|
||||||
@ -41,11 +44,11 @@ class LogRepository implements ILogRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> createAll(Iterable<LogMessage> logs) async {
|
Future<bool> createAll(Iterable<LogMessage> logs) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((b) {
|
await _db.txn(() async => await _db.batch((b) {
|
||||||
b.insertAll(_db.logs, logs.map(_toEntity));
|
b.insertAll(_db.logs, logs.map(_toEntity));
|
||||||
});
|
}));
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error while adding a log to the DB - $e");
|
debugPrint("Error while adding a log to the DB - $e");
|
||||||
@ -54,7 +57,7 @@ class LogRepository implements ILogRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> deleteAll() async {
|
Future<bool> deleteAll() async {
|
||||||
try {
|
try {
|
||||||
await _db.logs.deleteAll();
|
await _db.logs.deleteAll();
|
||||||
return true;
|
return true;
|
||||||
@ -70,8 +73,8 @@ LogsCompanion _toEntity(LogMessage log) {
|
|||||||
content: log.content,
|
content: log.content,
|
||||||
level: log.level,
|
level: log.level,
|
||||||
createdAt: Value(log.createdAt),
|
createdAt: Value(log.createdAt),
|
||||||
error: Value(log.error),
|
|
||||||
logger: Value(log.logger),
|
logger: Value(log.logger),
|
||||||
|
error: Value(log.error),
|
||||||
stack: Value(log.stack),
|
stack: Value(log.stack),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,10 +82,10 @@ LogsCompanion _toEntity(LogMessage log) {
|
|||||||
LogMessage _toModel(Log log) {
|
LogMessage _toModel(Log log) {
|
||||||
return LogMessage(
|
return LogMessage(
|
||||||
content: log.content,
|
content: log.content,
|
||||||
createdAt: log.createdAt,
|
|
||||||
level: log.level,
|
level: log.level,
|
||||||
error: log.error,
|
createdAt: log.createdAt,
|
||||||
logger: log.logger,
|
logger: log.logger,
|
||||||
|
error: log.error,
|
||||||
stack: log.stack,
|
stack: log.stack,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,22 +15,30 @@ class RenderListRepository with LogMixin implements IRenderListRepository {
|
|||||||
Stream<RenderList> watchAll() {
|
Stream<RenderList> watchAll() {
|
||||||
final assetCountExp = _db.asset.id.count();
|
final assetCountExp = _db.asset.id.count();
|
||||||
final createdTimeExp = _db.asset.createdTime;
|
final createdTimeExp = _db.asset.createdTime;
|
||||||
final monthYearExp = _db.asset.createdTime.strftime('%m-%Y');
|
final modifiedTimeExp = _db.asset.modifiedTime.max();
|
||||||
|
final monthYearExp = createdTimeExp.strftime('%m-%Y');
|
||||||
|
|
||||||
final query = _db.asset.selectOnly()
|
final query = _db.asset.selectOnly()
|
||||||
..addColumns([assetCountExp, createdTimeExp])
|
..addColumns([assetCountExp, createdTimeExp, modifiedTimeExp])
|
||||||
..groupBy([monthYearExp])
|
..groupBy([monthYearExp])
|
||||||
..orderBy([OrderingTerm.desc(createdTimeExp)]);
|
..orderBy([OrderingTerm.desc(createdTimeExp)]);
|
||||||
|
|
||||||
int lastAssetOffset = 0;
|
int lastAssetOffset = 0;
|
||||||
|
DateTime recentModifiedTime = DateTime(1);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
.expand((row) {
|
.expand((row) {
|
||||||
final createdTime = row.read<DateTime>(createdTimeExp)!;
|
final createdTime = row.read<DateTime>(createdTimeExp)!;
|
||||||
final assetCount = row.read(assetCountExp)!;
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
final modifiedTime = row.read(modifiedTimeExp)!;
|
||||||
final assetOffset = lastAssetOffset;
|
final assetOffset = lastAssetOffset;
|
||||||
lastAssetOffset += assetCount;
|
lastAssetOffset += assetCount;
|
||||||
|
|
||||||
|
// Get the recent modifed time. This is used to prevent unnecessary grid updates
|
||||||
|
if (modifiedTime.isAfter(recentModifiedTime)) {
|
||||||
|
recentModifiedTime = modifiedTime;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
RenderListMonthHeaderElement(date: createdTime),
|
RenderListMonthHeaderElement(date: createdTime),
|
||||||
RenderListAssetElement(
|
RenderListAssetElement(
|
||||||
@ -44,7 +52,9 @@ class RenderListRepository with LogMixin implements IRenderListRepository {
|
|||||||
.map((elements) {
|
.map((elements) {
|
||||||
// Resets the value in closure so the watch refresh will work properly
|
// Resets the value in closure so the watch refresh will work properly
|
||||||
lastAssetOffset = 0;
|
lastAssetOffset = 0;
|
||||||
return RenderList(elements: elements);
|
final modified = recentModifiedTime;
|
||||||
|
recentModifiedTime = DateTime(1);
|
||||||
|
return RenderList(elements: elements, modifiedTime: modified);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class StoreRepository with LogMixin implements IStoreRepository {
|
|||||||
const StoreRepository({required DriftDatabaseRepository db}) : _db = db;
|
const StoreRepository({required DriftDatabaseRepository db}) : _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key) async {
|
Future<T?> tryGet<T, U>(StoreKey<T, U> key) async {
|
||||||
final storeData = await _db.managers.store
|
final storeData = await _db.managers.store
|
||||||
.filter((s) => s.id.equals(key.id))
|
.filter((s) => s.id.equals(key.id))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
@ -21,7 +21,7 @@ class StoreRepository with LogMixin implements IStoreRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<T> get<T, U>(StoreKey<T, U> key) async {
|
Future<T> get<T, U>(StoreKey<T, U> key) async {
|
||||||
final value = await tryGet(key);
|
final value = await tryGet(key);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw StoreKeyNotFoundException(key);
|
throw StoreKeyNotFoundException(key);
|
||||||
@ -30,16 +30,16 @@ class StoreRepository with LogMixin implements IStoreRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> upsert<T, U>(StoreKey<T, U> key, T value) async {
|
Future<bool> upsert<T, U>(StoreKey<T, U> key, T value) async {
|
||||||
try {
|
try {
|
||||||
final storeValue = key.converter.toPrimitive(value);
|
final storeValue = key.converter.toPrimitive(value);
|
||||||
final intValue = (key.type == int) ? storeValue as int : null;
|
final intValue = (key.type == int) ? storeValue as int : null;
|
||||||
final stringValue = (key.type == String) ? storeValue as String : null;
|
final stringValue = (key.type == String) ? storeValue as String : null;
|
||||||
await _db.into(_db.store).insertOnConflictUpdate(StoreCompanion.insert(
|
await _db.store.insertOnConflictUpdate(StoreCompanion.insert(
|
||||||
id: Value(key.id),
|
id: Value(key.id),
|
||||||
intValue: Value(intValue),
|
intValue: Value(intValue),
|
||||||
stringValue: Value(stringValue),
|
stringValue: Value(stringValue),
|
||||||
));
|
));
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log.e("Cannot set store value - ${key.name}; id - ${key.id}", e, s);
|
log.e("Cannot set store value - ${key.name}; id - ${key.id}", e, s);
|
||||||
@ -48,7 +48,7 @@ class StoreRepository with LogMixin implements IStoreRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> delete(StoreKey key) async {
|
Future<void> delete(StoreKey key) async {
|
||||||
await _db.managers.store.filter((s) => s.id.equals(key.id)).delete();
|
await _db.managers.store.filter((s) => s.id.equals(key.id)).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,11 +61,11 @@ class StoreRepository with LogMixin implements IStoreRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAll() async {
|
Future<void> deleteAll() async {
|
||||||
await _db.managers.store.delete();
|
await _db.managers.store.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<T?> _getValueFromStoreData<T, U>(
|
Future<T?> _getValueFromStoreData<T, U>(
|
||||||
StoreKey<T, U> key,
|
StoreKey<T, U> key,
|
||||||
StoreData? data,
|
StoreData? data,
|
||||||
) async {
|
) async {
|
||||||
|
@ -13,7 +13,7 @@ class UserRepository with LogMixin implements IUserRepository {
|
|||||||
const UserRepository({required DriftDatabaseRepository db}) : _db = db;
|
const UserRepository({required DriftDatabaseRepository db}) : _db = db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<User?> getForId(String userId) async {
|
Future<User?> getForId(String userId) async {
|
||||||
return await _db.managers.user
|
return await _db.managers.user
|
||||||
.filter((f) => f.id.equals(userId))
|
.filter((f) => f.id.equals(userId))
|
||||||
.map(_toModel)
|
.map(_toModel)
|
||||||
@ -21,23 +21,23 @@ class UserRepository with LogMixin implements IUserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<bool> upsert(User user) async {
|
Future<bool> upsert(User user) async {
|
||||||
try {
|
try {
|
||||||
await _db.into(_db.user).insertOnConflictUpdate(
|
await _db.user.insertOnConflictUpdate(
|
||||||
UserCompanion.insert(
|
UserCompanion.insert(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
updatedAt: Value(user.updatedAt),
|
||||||
email: user.email,
|
name: user.name,
|
||||||
profileImagePath: user.profileImagePath,
|
email: user.email,
|
||||||
avatarColor: user.avatarColor,
|
isAdmin: Value(user.isAdmin),
|
||||||
inTimeline: Value(user.inTimeline),
|
quotaSizeInBytes: Value(user.quotaSizeInBytes),
|
||||||
isAdmin: Value(user.isAdmin),
|
quotaUsageInBytes: Value(user.quotaSizeInBytes),
|
||||||
memoryEnabled: Value(user.memoryEnabled),
|
inTimeline: Value(user.inTimeline),
|
||||||
quotaSizeInBytes: Value(user.quotaSizeInBytes),
|
profileImagePath: user.profileImagePath,
|
||||||
quotaUsageInBytes: Value(user.quotaSizeInBytes),
|
memoryEnabled: Value(user.memoryEnabled),
|
||||||
updatedAt: Value(user.updatedAt),
|
avatarColor: user.avatarColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log.e("Cannot insert User into table - $user", e, s);
|
log.e("Cannot insert User into table - $user", e, s);
|
||||||
@ -46,7 +46,7 @@ class UserRepository with LogMixin implements IUserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteAll() async {
|
Future<void> deleteAll() async {
|
||||||
await _db.user.deleteAll();
|
await _db.user.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,15 +54,15 @@ class UserRepository with LogMixin implements IUserRepository {
|
|||||||
User _toModel(UserData user) {
|
User _toModel(UserData user) {
|
||||||
return User(
|
return User(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
updatedAt: user.updatedAt,
|
||||||
avatarColor: user.avatarColor,
|
|
||||||
inTimeline: user.inTimeline,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
memoryEnabled: user.memoryEnabled,
|
|
||||||
name: user.name,
|
name: user.name,
|
||||||
profileImagePath: user.profileImagePath,
|
email: user.email,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
quotaSizeInBytes: user.quotaSizeInBytes,
|
quotaSizeInBytes: user.quotaSizeInBytes,
|
||||||
quotaUsageInBytes: user.quotaUsageInBytes,
|
quotaUsageInBytes: user.quotaUsageInBytes,
|
||||||
updatedAt: user.updatedAt,
|
inTimeline: user.inTimeline,
|
||||||
|
profileImagePath: user.profileImagePath,
|
||||||
|
memoryEnabled: user.memoryEnabled,
|
||||||
|
avatarColor: user.avatarColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,9 +51,9 @@ class AssetSyncService with LogMixin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final assetsFromServer = await syncApiRepo.getFullSyncForUser(
|
final assetsFromServer = await syncApiRepo.getFullSyncForUser(
|
||||||
|
lastId: lastAssetId,
|
||||||
limit: chunkSize,
|
limit: chunkSize,
|
||||||
updatedUntil: updatedTill,
|
updatedUntil: updatedTill,
|
||||||
lastId: lastAssetId,
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
);
|
);
|
||||||
if (assetsFromServer == null) {
|
if (assetsFromServer == null) {
|
||||||
@ -92,8 +92,8 @@ class AssetSyncService with LogMixin {
|
|||||||
final (toAdd, toUpdate, toRemove) = await _diffAssets(
|
final (toAdd, toUpdate, toRemove) = await _diffAssets(
|
||||||
newAssets,
|
newAssets,
|
||||||
existingAssets,
|
existingAssets,
|
||||||
compare: compare,
|
|
||||||
isRemoteSync: isRemoteSync,
|
isRemoteSync: isRemoteSync,
|
||||||
|
compare: compare,
|
||||||
);
|
);
|
||||||
|
|
||||||
final assetsToAdd = toAdd.followedBy(toUpdate);
|
final assetsToAdd = toAdd.followedBy(toUpdate);
|
||||||
@ -111,7 +111,7 @@ class AssetSyncService with LogMixin {
|
|||||||
}) async {
|
}) async {
|
||||||
// fast paths for trivial cases: reduces memory usage during initial sync etc.
|
// fast paths for trivial cases: reduces memory usage during initial sync etc.
|
||||||
if (newAssets.isEmpty && inDb.isEmpty) {
|
if (newAssets.isEmpty && inDb.isEmpty) {
|
||||||
return const (<Asset>[], <Asset>[], <Asset>[]);
|
return (<Asset>[], <Asset>[], <Asset>[]);
|
||||||
} else if (newAssets.isEmpty && isRemoteSync == null) {
|
} else if (newAssets.isEmpty && isRemoteSync == null) {
|
||||||
// remove all from database
|
// remove all from database
|
||||||
return (const <Asset>[], const <Asset>[], inDb);
|
return (const <Asset>[], const <Asset>[], inDb);
|
||||||
|
@ -88,7 +88,8 @@ class HashService with LogMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(hashesInDB.isEmpty, "All hashes should be processed at this point");
|
assert(hashesInDB.isEmpty, "All hashes should be processed at this point");
|
||||||
_assetHashRepository.deleteIds(orphanedHashes.map((e) => e.id!).toList());
|
await _assetHashRepository
|
||||||
|
.deleteIds(orphanedHashes.map((e) => e.id!).toList());
|
||||||
|
|
||||||
return hashedAssets;
|
return hashedAssets;
|
||||||
}
|
}
|
||||||
@ -103,21 +104,21 @@ class HashService with LogMixin {
|
|||||||
final hashedAssets = <Asset>[];
|
final hashedAssets = <Asset>[];
|
||||||
|
|
||||||
for (final (index, hash) in hashes.indexed) {
|
for (final (index, hash) in hashes.indexed) {
|
||||||
// ignore: avoid-unsafe-collection-methods
|
final asset = toBeHashed.elementAtOrNull(index)?.asset;
|
||||||
final asset = toBeHashed.elementAt(index).asset;
|
if (asset != null && hash?.length == 20) {
|
||||||
if (hash?.length == 20) {
|
|
||||||
hashedAssets.add(asset.copyWith(hash: base64.encode(hash!)));
|
hashedAssets.add(asset.copyWith(hash: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
log.w("Failed to hash file ${asset.localId ?? '<null>'}, skipping");
|
log.w("Failed to hash file ${asset?.localId ?? '<null>'}, skipping");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the cache for future retrieval
|
// Store the cache for future retrieval
|
||||||
_assetHashRepository.upsertAll(hashedAssets.map((a) => DeviceAssetToHash(
|
await _assetHashRepository
|
||||||
localId: a.localId!,
|
.upsertAll(hashedAssets.map((a) => DeviceAssetToHash(
|
||||||
hash: a.hash,
|
localId: a.localId!,
|
||||||
modifiedTime: a.modifiedTime,
|
hash: a.hash,
|
||||||
)));
|
modifiedTime: a.modifiedTime,
|
||||||
|
)));
|
||||||
|
|
||||||
log.v("Hashed ${hashedAssets.length}/${toBeHashed.length} assets");
|
log.v("Hashed ${hashedAssets.length}/${toBeHashed.length} assets");
|
||||||
return hashedAssets;
|
return hashedAssets;
|
||||||
@ -127,8 +128,7 @@ class HashService with LogMixin {
|
|||||||
/// Files that could not be hashed will have a `null` value
|
/// Files that could not be hashed will have a `null` value
|
||||||
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
||||||
try {
|
try {
|
||||||
final hashes = await _hostService.digestFiles(paths);
|
return await _hostService.digestFiles(paths);
|
||||||
return hashes;
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log.e("Error occured while hashing assets", e, s);
|
log.e("Error occured while hashing assets", e, s);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import 'package:immich_mobile/domain/entities/asset.entity.drift.dart';
|
import 'package:immich_mobile/domain/entities/asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||||
|
|
||||||
class DriftModelConverters {
|
abstract final class DriftModelConverters {
|
||||||
static Asset toAssetModel(AssetData asset) {
|
static Asset toAssetModel(AssetData asset) {
|
||||||
return Asset(
|
return Asset(
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
localId: asset.localId,
|
|
||||||
remoteId: asset.remoteId,
|
|
||||||
name: asset.name,
|
name: asset.name,
|
||||||
type: asset.type,
|
|
||||||
hash: asset.hash,
|
hash: asset.hash,
|
||||||
createdTime: asset.createdTime,
|
|
||||||
modifiedTime: asset.modifiedTime,
|
|
||||||
height: asset.height,
|
height: asset.height,
|
||||||
width: asset.width,
|
width: asset.width,
|
||||||
livePhotoVideoId: asset.livePhotoVideoId,
|
type: asset.type,
|
||||||
|
createdTime: asset.createdTime,
|
||||||
|
modifiedTime: asset.modifiedTime,
|
||||||
duration: asset.duration,
|
duration: asset.duration,
|
||||||
|
localId: asset.localId,
|
||||||
|
remoteId: asset.remoteId,
|
||||||
|
livePhotoVideoId: asset.livePhotoVideoId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,14 +26,14 @@ class _ImAppState extends State<ImApp> with WidgetsBindingObserver {
|
|||||||
builder: (_, appTheme, __) => _AppThemeBuilder(
|
builder: (_, appTheme, __) => _AppThemeBuilder(
|
||||||
theme: appTheme,
|
theme: appTheme,
|
||||||
builder: (ctx, lightTheme, darkTheme) => MaterialApp.router(
|
builder: (ctx, lightTheme, darkTheme) => MaterialApp.router(
|
||||||
debugShowCheckedModeBanner: false,
|
scaffoldMessengerKey: kScafMessengerKey,
|
||||||
locale: TranslationProvider.of(ctx).flutterLocale,
|
routerConfig: di<AppRouter>().config(),
|
||||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
|
||||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
|
||||||
theme: lightTheme,
|
theme: lightTheme,
|
||||||
darkTheme: darkTheme,
|
darkTheme: darkTheme,
|
||||||
routerConfig: di<AppRouter>().config(),
|
locale: TranslationProvider.of(ctx).flutterLocale,
|
||||||
scaffoldMessengerKey: kScafMessengerKey,
|
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||||
|
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -6,17 +6,17 @@ import 'package:immich_mobile/utils/log_manager.dart';
|
|||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
// DI Injection
|
// DI Injection
|
||||||
ServiceLocator.configureServices();
|
ServiceLocator.configureServices();
|
||||||
// Init logging
|
// Init logging
|
||||||
LogManager.I.init();
|
await LogManager.I.init();
|
||||||
LogManager.setGlobalErrorCallbacks();
|
LogManager.setGlobalErrorCallbacks();
|
||||||
// Init localization
|
// Init localization
|
||||||
LocaleSettings.useDeviceLocale();
|
LocaleSettings.useDeviceLocale();
|
||||||
// Clear photo_manager cache
|
// Clear photo_manager cache
|
||||||
PhotoManager.clearFileCache();
|
await PhotoManager.clearFileCache();
|
||||||
|
|
||||||
runApp(const ImApp());
|
runApp(const ImApp());
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ import 'package:pigeon/pigeon.dart';
|
|||||||
|
|
||||||
@ConfigurePigeon(PigeonOptions(
|
@ConfigurePigeon(PigeonOptions(
|
||||||
dartOut: 'lib/platform/messages.g.dart',
|
dartOut: 'lib/platform/messages.g.dart',
|
||||||
dartOptions: DartOptions(),
|
swiftOut: 'ios/Runner/Platform/Messages.g.swift',
|
||||||
|
swiftOptions: SwiftOptions(),
|
||||||
kotlinOut:
|
kotlinOut:
|
||||||
'android/app/src/main/kotlin/com/alextran/immich/platform/Messages.g.kt',
|
'android/app/src/main/kotlin/com/alextran/immich/platform/Messages.g.kt',
|
||||||
kotlinOptions: KotlinOptions(),
|
kotlinOptions: KotlinOptions(),
|
||||||
swiftOut: 'ios/Runner/Platform/Messages.g.swift',
|
dartOptions: DartOptions(),
|
||||||
swiftOptions: SwiftOptions(),
|
|
||||||
))
|
))
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class ImHostService {
|
abstract class ImHostService {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
|
class SkeletonizedFutureBuilder<T> extends StatelessWidget {
|
||||||
|
const SkeletonizedFutureBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.future,
|
||||||
|
required this.builder,
|
||||||
|
required this.loadingBuilder,
|
||||||
|
required this.errorBuilder,
|
||||||
|
this.emptyBuilder,
|
||||||
|
this.emptyWhen,
|
||||||
|
}) : assert(
|
||||||
|
(emptyBuilder == null && emptyWhen == null) ||
|
||||||
|
(emptyBuilder != null && emptyWhen != null),
|
||||||
|
"Both emptyBuilder and emptyWhen should be provided");
|
||||||
|
|
||||||
|
/// Future to listen to
|
||||||
|
final Future<T> future;
|
||||||
|
|
||||||
|
/// Callback when data is available
|
||||||
|
final Widget Function(BuildContext context, T? snapshot) builder;
|
||||||
|
|
||||||
|
/// Callback when future is loading. Expected a skeletonizer to be returned
|
||||||
|
final Widget Function(BuildContext context) loadingBuilder;
|
||||||
|
|
||||||
|
/// Callback when future resulted in an error
|
||||||
|
final Widget Function(BuildContext context, Object? error) errorBuilder;
|
||||||
|
|
||||||
|
/// Callback when data is available but is empty. Emptiness is determined based on the [emptyWhen] callback
|
||||||
|
final Widget Function(BuildContext context)? emptyBuilder;
|
||||||
|
|
||||||
|
/// Predicate to call [emptyBuilder] when the [data] passes the filter
|
||||||
|
final bool Function(T? data)? emptyWhen;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<T>(
|
||||||
|
future: future,
|
||||||
|
builder: (ctx, snap) {
|
||||||
|
final Widget child;
|
||||||
|
if (snap.isWaiting) {
|
||||||
|
child = loadingBuilder(ctx);
|
||||||
|
} else {
|
||||||
|
if (snap.hasError) {
|
||||||
|
child = errorBuilder(ctx, snap.error);
|
||||||
|
} else {
|
||||||
|
final isEmpty = emptyWhen?.call(snap.data) ?? false;
|
||||||
|
child = isEmpty ? emptyBuilder!(ctx) : builder(ctx, snap.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Skeletonizer.zone(
|
||||||
|
enabled: snap.isWaiting,
|
||||||
|
enableSwitchAnimation: true,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/immich_cached_network_image.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/transparent_image.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_image_url_helper.dart';
|
||||||
|
|
||||||
|
class ImUserAvatar extends StatelessWidget {
|
||||||
|
final User user;
|
||||||
|
final double? dimension;
|
||||||
|
final double? radius;
|
||||||
|
|
||||||
|
const ImUserAvatar({
|
||||||
|
super.key,
|
||||||
|
required this.user,
|
||||||
|
this.dimension,
|
||||||
|
this.radius,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
final textIcon = Text(
|
||||||
|
user.name[0].toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDarkTheme && user.avatarColor == UserAvatarColor.primary
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: user.avatarColor.toColor(),
|
||||||
|
radius: radius,
|
||||||
|
child: user.profileImagePath.isEmpty
|
||||||
|
? textIcon
|
||||||
|
: ClipOval(
|
||||||
|
child: ImCachedNetworkImage(
|
||||||
|
imageUrl: ImImageUrlHelper.getUserAvatarUrl(user),
|
||||||
|
cacheKey: user.profileImagePath,
|
||||||
|
height: dimension,
|
||||||
|
width: dimension,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (_, __) => Image.memory(
|
||||||
|
kTransparentImage,
|
||||||
|
semanticLabel: 'Transparent',
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
|
errorWidget: (_, error, stackTrace) => SizedBox.square(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -98,11 +98,11 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
required BoxConstraints? labelConstraints,
|
required BoxConstraints? labelConstraints,
|
||||||
required bool alwaysVisibleScrollThumb,
|
required bool alwaysVisibleScrollThumb,
|
||||||
}) {
|
}) {
|
||||||
var scrollThumbAndLabel = labelText == null
|
Widget scrollThumbAndLabel = labelText == null
|
||||||
? scrollThumb
|
? scrollThumb
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_ScrollLabel(
|
_ScrollLabel(
|
||||||
animation: labelAnimation,
|
animation: labelAnimation,
|
||||||
@ -145,8 +145,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(height),
|
topLeft: Radius.circular(height),
|
||||||
bottomLeft: Radius.circular(height),
|
|
||||||
topRight: const Radius.circular(4.0),
|
topRight: const Radius.circular(4.0),
|
||||||
|
bottomLeft: Radius.circular(height),
|
||||||
bottomRight: const Radius.circular(4.0),
|
bottomRight: const Radius.circular(4.0),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -195,9 +195,9 @@ class _ScrollLabel extends StatelessWidget {
|
|||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: constraints ?? _defaultConstraints,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
constraints: constraints ?? _defaultConstraints,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -231,8 +231,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
_currentItem = 0;
|
_currentItem = 0;
|
||||||
|
|
||||||
_thumbAnimationController = AnimationController(
|
_thumbAnimationController = AnimationController(
|
||||||
vsync: this,
|
|
||||||
duration: widget.scrollbarAnimationDuration,
|
duration: widget.scrollbarAnimationDuration,
|
||||||
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
_thumbAnimation = CurvedAnimation(
|
_thumbAnimation = CurvedAnimation(
|
||||||
@ -241,8 +241,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
);
|
);
|
||||||
|
|
||||||
_labelAnimationController = AnimationController(
|
_labelAnimationController = AnimationController(
|
||||||
vsync: this,
|
|
||||||
duration: widget.scrollbarAnimationDuration,
|
duration: widget.scrollbarAnimationDuration,
|
||||||
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
_labelAnimation = CurvedAnimation(
|
_labelAnimation = CurvedAnimation(
|
||||||
@ -291,16 +291,16 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
onVerticalDragEnd: _onVerticalDragEnd,
|
onVerticalDragEnd: _onVerticalDragEnd,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
margin: EdgeInsets.only(top: _barOffset),
|
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
|
margin: EdgeInsets.only(top: _barOffset),
|
||||||
child: widget.scrollThumbBuilder(
|
child: widget.scrollThumbBuilder(
|
||||||
widget.backgroundColor,
|
widget.backgroundColor,
|
||||||
widget.foregroundColor,
|
widget.foregroundColor,
|
||||||
_thumbAnimation,
|
_thumbAnimation,
|
||||||
_labelAnimation,
|
_labelAnimation,
|
||||||
widget.heightScrollThumb,
|
widget.heightScrollThumb,
|
||||||
labelText: labelText,
|
|
||||||
labelConstraints: widget.labelConstraints,
|
labelConstraints: widget.labelConstraints,
|
||||||
|
labelText: labelText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -356,7 +356,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
_thumbAnimationController.forward();
|
_thumbAnimationController.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
final lastItemPos = itemPos;
|
final lastItemPos = _itemPos;
|
||||||
if (lastItemPos < widget.maxItemCount) {
|
if (lastItemPos < widget.maxItemCount) {
|
||||||
_currentItem = lastItemPos;
|
_currentItem = lastItemPos;
|
||||||
}
|
}
|
||||||
@ -378,7 +378,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
widget.scrollStateListener(true);
|
widget.scrollStateListener(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
int get itemIndex {
|
int get _itemIndex {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
double minDiff = 1000;
|
double minDiff = 1000;
|
||||||
for (final pos in _positions) {
|
for (final pos in _positions) {
|
||||||
@ -391,21 +391,21 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get itemPos =>
|
int get _itemPos =>
|
||||||
((_barOffset / (_barMaxScrollExtent)) * widget.maxItemCount).toInt();
|
((_barOffset / (_barMaxScrollExtent)) * widget.maxItemCount).toInt();
|
||||||
|
|
||||||
void _jumpToBarPos() {
|
void _jumpToBarPos() {
|
||||||
final lastItemPos = itemPos;
|
final lastItemPos = _itemPos;
|
||||||
if (lastItemPos > widget.maxItemCount - 1) {
|
if (lastItemPos > widget.maxItemCount - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentItem = itemIndex;
|
_currentItem = _itemIndex;
|
||||||
widget.controller.sliverController.jumpToIndex(lastItemPos);
|
widget.controller.sliverController.jumpToIndex(lastItemPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer? _dragHaltTimer;
|
Timer? _dragHaltTimer;
|
||||||
int lastTimerPos = 0;
|
int _lastTimerPos = 0;
|
||||||
|
|
||||||
void _onVerticalDragUpdate(DragUpdateDetails details) {
|
void _onVerticalDragUpdate(DragUpdateDetails details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -418,9 +418,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
_barOffset =
|
_barOffset =
|
||||||
clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent);
|
clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent);
|
||||||
|
|
||||||
final lastItemPos = itemPos;
|
final lastItemPos = _itemPos;
|
||||||
if (lastItemPos != lastTimerPos) {
|
if (lastItemPos != _lastTimerPos) {
|
||||||
lastTimerPos = lastItemPos;
|
_lastTimerPos = lastItemPos;
|
||||||
_dragHaltTimer?.cancel();
|
_dragHaltTimer?.cancel();
|
||||||
widget.scrollStateListener(true);
|
widget.scrollStateListener(true);
|
||||||
|
|
||||||
|
@ -26,6 +26,17 @@ class AssetGridState {
|
|||||||
renderList: renderList ?? this.renderList,
|
renderList: renderList ?? this.renderList,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant AssetGridState other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.renderList == renderList &&
|
||||||
|
other.isDragScrolling == isDragScrolling;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => renderList.hashCode ^ isDragScrolling.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetGridCubit extends Cubit<AssetGridState> {
|
class AssetGridCubit extends Cubit<AssetGridState> {
|
||||||
@ -43,6 +54,9 @@ class AssetGridCubit extends Cubit<AssetGridState> {
|
|||||||
super(AssetGridState.empty()) {
|
super(AssetGridState.empty()) {
|
||||||
_renderListSubscription =
|
_renderListSubscription =
|
||||||
_renderListProvider.renderStreamProvider().listen((renderList) {
|
_renderListProvider.renderStreamProvider().listen((renderList) {
|
||||||
|
if (renderList == state.renderList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_bufOffset = 0;
|
_bufOffset = 0;
|
||||||
_buf = [];
|
_buf = [];
|
||||||
emit(state.copyWith(renderList: renderList));
|
emit(state.copyWith(renderList: renderList));
|
||||||
@ -87,8 +101,8 @@ class AssetGridCubit extends Cubit<AssetGridState> {
|
|||||||
|
|
||||||
// load the calculated batch (start:start+len) from the DB and put it into the buffer
|
// load the calculated batch (start:start+len) from the DB and put it into the buffer
|
||||||
_buf = await _renderListProvider.renderAssetProvider(
|
_buf = await _renderListProvider.renderAssetProvider(
|
||||||
offset: start,
|
|
||||||
limit: len,
|
limit: len,
|
||||||
|
offset: start,
|
||||||
);
|
);
|
||||||
_bufOffset = start;
|
_bufOffset = start;
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
part 'immich_asset_grid_header.widget.dart';
|
part 'immich_asset_grid_header.widget.dart';
|
||||||
|
part 'immich_asset_render_grid.widget.dart';
|
||||||
|
|
||||||
class ImAssetGrid extends StatefulWidget {
|
class ImAssetGrid extends StatefulWidget {
|
||||||
/// The padding for the grid
|
/// The padding for the grid
|
||||||
@ -76,7 +77,6 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final grid = FlutterListView(
|
final grid = FlutterListView(
|
||||||
controller: _controller,
|
|
||||||
delegate: FlutterListViewDelegate(
|
delegate: FlutterListViewDelegate(
|
||||||
(_, sectionIndex) {
|
(_, sectionIndex) {
|
||||||
// ignore: avoid-unsafe-collection-methods
|
// ignore: avoid-unsafe-collection-methods
|
||||||
@ -89,70 +89,46 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
RenderListMonthHeaderElement() =>
|
RenderListMonthHeaderElement() =>
|
||||||
_MonthHeader(text: section.header),
|
_MonthHeader(text: section.header),
|
||||||
RenderListDayHeaderElement() => Text(section.header),
|
RenderListDayHeaderElement() => Text(section.header),
|
||||||
RenderListAssetElement() => FutureBuilder(
|
RenderListAssetElement() => _StaticGrid(
|
||||||
future: context.read<AssetGridCubit>().loadAssets(
|
section: section,
|
||||||
section.assetOffset,
|
isDragging: state.isDragScrolling,
|
||||||
section.assetCount,
|
|
||||||
),
|
|
||||||
builder: (_, assetsSnap) {
|
|
||||||
final assets = assetsSnap.data;
|
|
||||||
return GridView.builder(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
shrinkWrap: true,
|
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
cacheExtent: 100,
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
gridDelegate:
|
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 4,
|
|
||||||
mainAxisSpacing: 3,
|
|
||||||
crossAxisSpacing: 3,
|
|
||||||
),
|
|
||||||
itemBuilder: (_, i) {
|
|
||||||
final asset = assetsSnap.isWaiting || assets == null
|
|
||||||
? null
|
|
||||||
: assets.elementAtOrNull(i);
|
|
||||||
return SizedBox.square(
|
|
||||||
dimension: 200,
|
|
||||||
// Show Placeholder when drag scrolled
|
|
||||||
child: asset == null || state.isDragScrolling
|
|
||||||
? const ImImagePlaceholder()
|
|
||||||
: ImThumbnail(asset),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: section.assetCount,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
childCount: elements.length,
|
childCount: elements.length,
|
||||||
addAutomaticKeepAlives: false,
|
addAutomaticKeepAlives: false,
|
||||||
),
|
),
|
||||||
|
controller: _controller,
|
||||||
);
|
);
|
||||||
|
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
if (widget.topPadding != null) {
|
if (widget.topPadding == null) {
|
||||||
padding = EdgeInsets.only(top: widget.topPadding!);
|
|
||||||
} else {
|
|
||||||
padding = null;
|
padding = null;
|
||||||
|
} else {
|
||||||
|
padding = EdgeInsets.only(top: widget.topPadding!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DraggableScrollbar(
|
return DraggableScrollbar(
|
||||||
foregroundColor: context.colorScheme.onSurface,
|
|
||||||
backgroundColor: context.colorScheme.surfaceContainerHighest,
|
|
||||||
scrollStateListener:
|
|
||||||
context.read<AssetGridCubit>().setDragScrolling,
|
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
maxItemCount: elements.length,
|
maxItemCount: elements.length,
|
||||||
|
scrollStateListener:
|
||||||
|
context.read<AssetGridCubit>().setDragScrolling,
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerHighest,
|
||||||
|
foregroundColor: context.colorScheme.onSurface,
|
||||||
|
padding: padding,
|
||||||
|
scrollbarAnimationDuration: Durations.medium2,
|
||||||
|
scrollbarTimeToFade: Durations.extralong4,
|
||||||
labelTextBuilder: (int position) =>
|
labelTextBuilder: (int position) =>
|
||||||
_labelBuilder(elements, position),
|
_labelBuilder(elements, position),
|
||||||
labelConstraints: const BoxConstraints(maxHeight: 36),
|
labelConstraints: const BoxConstraints(maxHeight: 36),
|
||||||
scrollbarAnimationDuration: Durations.medium2,
|
|
||||||
scrollbarTimeToFade: Durations.extralong4,
|
|
||||||
padding: padding,
|
|
||||||
child: grid,
|
child: grid,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// no.of elements are not equal or is modified
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
(previous.renderList.elements.length !=
|
||||||
|
current.renderList.elements.length) ||
|
||||||
|
!previous.renderList.modifiedTime
|
||||||
|
.isAtSameMomentAs(current.renderList.modifiedTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ class _HeaderText extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 32.0,
|
|
||||||
left: 16.0,
|
left: 16.0,
|
||||||
|
top: 32.0,
|
||||||
right: 24.0,
|
right: 24.0,
|
||||||
bottom: 16.0,
|
bottom: 16.0,
|
||||||
),
|
),
|
||||||
@ -40,9 +40,9 @@ class _MonthHeader extends StatelessWidget {
|
|||||||
return _HeaderText(
|
return _HeaderText(
|
||||||
text: text,
|
text: text,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
fontSize: 24.0,
|
fontSize: 24.0,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: context.colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
part of 'immich_asset_grid.widget.dart';
|
||||||
|
|
||||||
|
class _StaticGrid extends StatelessWidget {
|
||||||
|
final RenderListAssetElement section;
|
||||||
|
final bool isDragging;
|
||||||
|
|
||||||
|
const _StaticGrid({required this.section, required this.isDragging});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: context.read<AssetGridCubit>().loadAssets(
|
||||||
|
section.assetOffset,
|
||||||
|
section.assetCount,
|
||||||
|
),
|
||||||
|
builder: (_, assetsSnap) {
|
||||||
|
final assets = assetsSnap.data;
|
||||||
|
return GridView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
mainAxisSpacing: 3,
|
||||||
|
crossAxisSpacing: 3,
|
||||||
|
),
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final asset = assetsSnap.isWaiting || assets == null
|
||||||
|
? null
|
||||||
|
: assets.elementAtOrNull(i);
|
||||||
|
return SizedBox.square(
|
||||||
|
dimension: 200,
|
||||||
|
// Show Placeholder when drag scrolled
|
||||||
|
child: asset == null || isDragging
|
||||||
|
? const ImImagePlaceholder()
|
||||||
|
: ImThumbnail(asset),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: section.assetCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
cacheExtent: 100,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,8 @@ class ImRemoteThumbnailCacheManager extends CacheManager {
|
|||||||
: super(
|
: super(
|
||||||
Config(
|
Config(
|
||||||
kCacheThumbnailsKey,
|
kCacheThumbnailsKey,
|
||||||
maxNrOfCacheObjects: kCacheMaxNrOfThumbnails,
|
|
||||||
stalePeriod: const Duration(days: kCacheStalePeriod),
|
stalePeriod: const Duration(days: kCacheStalePeriod),
|
||||||
|
maxNrOfCacheObjects: kCacheMaxNrOfThumbnails,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -33,8 +33,8 @@ class ImRemoteImageCacheManager extends CacheManager {
|
|||||||
: super(
|
: super(
|
||||||
Config(
|
Config(
|
||||||
kCacheFullImagesKey,
|
kCacheFullImagesKey,
|
||||||
maxNrOfCacheObjects: kCacheMaxNrOfFullImages,
|
|
||||||
stalePeriod: const Duration(days: kCacheStalePeriod),
|
stalePeriod: const Duration(days: kCacheStalePeriod),
|
||||||
|
maxNrOfCacheObjects: kCacheMaxNrOfFullImages,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class ImageLoadingException implements Exception {
|
|||||||
///
|
///
|
||||||
/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart)
|
/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart)
|
||||||
/// for this wonderful implementation of their image loader
|
/// for this wonderful implementation of their image loader
|
||||||
class ImageLoader {
|
abstract final class ImageLoader {
|
||||||
static Future<ui.Codec> loadImageFromCache(
|
static Future<ui.Codec> loadImageFromCache(
|
||||||
String uri, {
|
String uri, {
|
||||||
required CacheManager cache,
|
required CacheManager cache,
|
||||||
@ -28,8 +28,8 @@ class ImageLoader {
|
|||||||
}) async {
|
}) async {
|
||||||
final stream = cache.getFileStream(
|
final stream = cache.getFileStream(
|
||||||
uri,
|
uri,
|
||||||
withProgress: chunkEvents != null,
|
|
||||||
headers: di<ImApiClient>().headers,
|
headers: di<ImApiClient>().headers,
|
||||||
|
withProgress: chunkEvents != null,
|
||||||
);
|
);
|
||||||
|
|
||||||
await for (final result in stream) {
|
await for (final result in stream) {
|
||||||
@ -44,8 +44,7 @@ class ImageLoader {
|
|||||||
} else if (result is FileInfo) {
|
} else if (result is FileInfo) {
|
||||||
// We have the file
|
// We have the file
|
||||||
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
||||||
final decoded = await decode(buffer);
|
return await decode(buffer);
|
||||||
return decoded;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
|
|
||||||
|
class ImCachedNetworkImage extends CachedNetworkImage {
|
||||||
|
ImCachedNetworkImage({
|
||||||
|
super.key,
|
||||||
|
required super.imageUrl,
|
||||||
|
super.cacheKey,
|
||||||
|
super.height,
|
||||||
|
super.width,
|
||||||
|
super.fit,
|
||||||
|
super.placeholder,
|
||||||
|
super.fadeInDuration,
|
||||||
|
super.errorWidget,
|
||||||
|
}) : super(httpHeaders: di<ImApiClient>().headers);
|
||||||
|
}
|
@ -12,9 +12,9 @@ class ImImagePlaceholder extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
color: context.colorScheme.surfaceContainerHighest,
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
color: context.colorScheme.surfaceContainerHighest,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,13 +63,8 @@ class ImImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return OctoImage(
|
return OctoImage(
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
|
||||||
fadeOutDuration: Durations.short4,
|
|
||||||
placeholderBuilder: (_) => placeholder,
|
|
||||||
image: ImImage.imageProvider(asset: asset),
|
image: ImImage.imageProvider(asset: asset),
|
||||||
width: width,
|
placeholderBuilder: (_) => placeholder,
|
||||||
height: height,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, error, stackTrace) {
|
errorBuilder: (_, error, stackTrace) {
|
||||||
if (error is PlatformException &&
|
if (error is PlatformException &&
|
||||||
error.code == "The asset not found!") {
|
error.code == "The asset not found!") {
|
||||||
@ -86,6 +81,11 @@ class ImImage extends StatelessWidget {
|
|||||||
color: context.colorScheme.primary,
|
color: context.colorScheme.primary,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
fadeOutDuration: Durations.short4,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,13 @@ import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
|||||||
|
|
||||||
class ImLogo extends StatelessWidget {
|
class ImLogo extends StatelessWidget {
|
||||||
const ImLogo({
|
const ImLogo({
|
||||||
this.width,
|
this.dimension,
|
||||||
this.filterQuality = FilterQuality.high,
|
this.filterQuality = FilterQuality.high,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The width of the image.
|
/// The dimension of the image.
|
||||||
final double? width;
|
final double? dimension;
|
||||||
|
|
||||||
/// The rendering quality
|
/// The rendering quality
|
||||||
final FilterQuality filterQuality;
|
final FilterQuality filterQuality;
|
||||||
@ -18,11 +18,12 @@ class ImLogo extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Image(
|
return Image(
|
||||||
width: width,
|
|
||||||
filterQuality: filterQuality,
|
|
||||||
semanticLabel: 'Immich Logo',
|
|
||||||
image: Assets.images.immichLogo.provider(),
|
image: Assets.images.immichLogo.provider(),
|
||||||
|
semanticLabel: 'Immich Logo',
|
||||||
|
width: dimension,
|
||||||
|
height: dimension,
|
||||||
isAntiAlias: true,
|
isAntiAlias: true,
|
||||||
|
filterQuality: filterQuality,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,10 +44,10 @@ class ImLogoText extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Image(
|
return Image(
|
||||||
semanticLabel: 'Immich Logo Text',
|
|
||||||
image: (context.isDarkTheme
|
image: (context.isDarkTheme
|
||||||
? Assets.images.immichTextDark.provider
|
? Assets.images.immichTextDark.provider
|
||||||
: Assets.images.immichTextLight.provider)(),
|
: Assets.images.immichTextLight.provider)(),
|
||||||
|
semanticLabel: 'Immich Logo Text',
|
||||||
width: fontSize * 4,
|
width: fontSize * 4,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
);
|
);
|
||||||
|
@ -77,9 +77,9 @@ class _PadAlignedIcon extends StatelessWidget {
|
|||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
size: 20,
|
||||||
fill: (filled != null && filled!) ? 1 : null,
|
fill: (filled != null && filled!) ? 1 : null,
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -49,12 +49,12 @@ class ImLocalImageProvider extends ImageProvider<ImLocalImageProvider> {
|
|||||||
// Load a small thumbnail
|
// Load a small thumbnail
|
||||||
final thumbBytes =
|
final thumbBytes =
|
||||||
await di<IDeviceAssetRepository>().getThumbnail(a.localId!);
|
await di<IDeviceAssetRepository>().getThumbnail(a.localId!);
|
||||||
if (thumbBytes != null) {
|
if (thumbBytes == null) {
|
||||||
|
debugPrint("Loading thumb for ${a.name} failed");
|
||||||
|
} else {
|
||||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
final codec = await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
yield codec;
|
yield codec;
|
||||||
} else {
|
|
||||||
debugPrint("Loading thumb for ${a.name} failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
|
@ -56,12 +56,12 @@ class ImLocalThumbnailProvider extends ImageProvider<ImLocalThumbnailProvider> {
|
|||||||
// Load a small thumbnail
|
// Load a small thumbnail
|
||||||
final thumbBytes = await di<IDeviceAssetRepository>()
|
final thumbBytes = await di<IDeviceAssetRepository>()
|
||||||
.getThumbnail(a.localId!, width: 32, height: 32, quality: 75);
|
.getThumbnail(a.localId!, width: 32, height: 32, quality: 75);
|
||||||
if (thumbBytes != null) {
|
if (thumbBytes == null) {
|
||||||
|
debugPrint("Loading thumb for ${a.name} failed");
|
||||||
|
} else {
|
||||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
final codec = await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
yield codec;
|
yield codec;
|
||||||
} else {
|
|
||||||
debugPrint("Loading thumb for ${a.name} failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final normalThumbBytes = await di<IDeviceAssetRepository>()
|
final normalThumbBytes = await di<IDeviceAssetRepository>()
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
final Uint8List kTransparentImage = Uint8List.fromList([
|
||||||
|
0x89,
|
||||||
|
0x50,
|
||||||
|
0x4E,
|
||||||
|
0x47,
|
||||||
|
0x0D,
|
||||||
|
0x0A,
|
||||||
|
0x1A,
|
||||||
|
0x0A,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x0D,
|
||||||
|
0x49,
|
||||||
|
0x48,
|
||||||
|
0x44,
|
||||||
|
0x52,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x08,
|
||||||
|
0x06,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x1F,
|
||||||
|
0x15,
|
||||||
|
0xC4,
|
||||||
|
0x89,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x0A,
|
||||||
|
0x49,
|
||||||
|
0x44,
|
||||||
|
0x41,
|
||||||
|
0x54,
|
||||||
|
0x78,
|
||||||
|
0x9C,
|
||||||
|
0x63,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x05,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x0D,
|
||||||
|
0x0A,
|
||||||
|
0x2D,
|
||||||
|
0xB4,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x49,
|
||||||
|
0x45,
|
||||||
|
0x4E,
|
||||||
|
0x44,
|
||||||
|
0xAE,
|
||||||
|
]);
|
@ -40,33 +40,33 @@ class ImPasswordFormField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ImPasswordFormFieldState extends State<ImPasswordFormField> {
|
class _ImPasswordFormFieldState extends State<ImPasswordFormField> {
|
||||||
final showPassword = ValueNotifier(false);
|
final _showPassword = ValueNotifier(false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
showPassword.dispose();
|
_showPassword.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: showPassword,
|
valueListenable: _showPassword,
|
||||||
builder: (_, showPass, child) => ImTextFormField(
|
builder: (_, showPass, child) => ImTextFormField(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
shouldObscure: !showPass,
|
shouldObscure: !showPass,
|
||||||
hint: widget.hint,
|
|
||||||
label: widget.label,
|
|
||||||
focusNode: widget.focusNode,
|
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
onPressed: () => _showPassword.value = !_showPassword.value,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
showPassword.value
|
_showPassword.value
|
||||||
? Symbols.visibility_off_rounded
|
? Symbols.visibility_off_rounded
|
||||||
: Symbols.visibility_rounded,
|
: Symbols.visibility_rounded,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
label: widget.label,
|
||||||
|
hint: widget.hint,
|
||||||
autoFillHints: const [AutofillHints.password],
|
autoFillHints: const [AutofillHints.password],
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
textInputAction: widget.textInputAction,
|
textInputAction: widget.textInputAction,
|
||||||
|
@ -29,18 +29,27 @@ class ImSwitchListTile<T> extends StatefulWidget {
|
|||||||
|
|
||||||
class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
|
class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
|
||||||
// Actual switch list state
|
// Actual switch list state
|
||||||
late bool isEnabled;
|
late bool _isEnabled;
|
||||||
final AppSettingService _appSettingService = di();
|
final AppSettingService _appSettingService = di();
|
||||||
|
|
||||||
Future<void> set(bool enabled) async {
|
Future<void> _set(bool enabled) async {
|
||||||
if (isEnabled == enabled) return;
|
if (_isEnabled == enabled) return;
|
||||||
|
|
||||||
final value = T != bool ? widget.toAppSetting!(enabled) : enabled as T;
|
final value = T == bool ? enabled as T : widget.toAppSetting!(enabled);
|
||||||
if (value != null &&
|
if (value != null &&
|
||||||
await _appSettingService.upsert(widget.setting, value) &&
|
await _appSettingService.upsert(widget.setting, value) &&
|
||||||
context.mounted) {
|
context.mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isEnabled = enabled;
|
_isEnabled = enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initSetting() async {
|
||||||
|
final value = await _appSettingService.get(widget.setting);
|
||||||
|
if (context.mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isEnabled = T == bool ? value as bool : widget.fromAppSetting!(value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,20 +57,14 @@ class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_appSettingService.get(widget.setting).then((value) {
|
_initSetting().ignore();
|
||||||
if (context.mounted) {
|
|
||||||
setState(() {
|
|
||||||
isEnabled = T != bool ? widget.fromAppSetting!(value) : value as bool;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
value: isEnabled,
|
value: _isEnabled,
|
||||||
onChanged: (value) => set(value),
|
onChanged: (value) => unawaited(_set(value)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,21 +66,21 @@ class ImTextFormField extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onChanged: onChanged,
|
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
obscureText: shouldObscure,
|
|
||||||
validator: validator,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
suffixIcon: suffixIcon,
|
suffixIcon: suffixIcon,
|
||||||
),
|
),
|
||||||
autofillHints: autoFillHints,
|
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
readOnly: isDisabled,
|
readOnly: isDisabled,
|
||||||
|
obscureText: shouldObscure,
|
||||||
|
onChanged: onChanged,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onFieldSubmitted: onSubmitted,
|
onFieldSubmitted: onSubmitted,
|
||||||
|
validator: validator,
|
||||||
|
autofillHints: autoFillHints,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,44 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
class ImAdaptiveRoutePrimaryAppBar extends StatelessWidget
|
class ImAdaptiveRouteAppBar extends StatelessWidget
|
||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
const ImAdaptiveRoutePrimaryAppBar({super.key});
|
final String? title;
|
||||||
|
final bool isPrimary;
|
||||||
|
|
||||||
@override
|
/// Passed to [AppBar] actions
|
||||||
Widget build(BuildContext context) {
|
final List<Widget>? actions;
|
||||||
return AppBar(
|
|
||||||
leading: BackButton(onPressed: () => context.router.root.maybePop()),
|
const ImAdaptiveRouteAppBar({
|
||||||
);
|
super.key,
|
||||||
}
|
this.title,
|
||||||
|
this.isPrimary = true,
|
||||||
|
this.actions,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: prefer-single-widget-per-file
|
|
||||||
class ImAdaptiveRouteSecondaryAppBar extends StatelessWidget
|
|
||||||
implements PreferredSizeWidget {
|
|
||||||
const ImAdaptiveRouteSecondaryAppBar({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final Widget leading;
|
||||||
|
if (isPrimary) {
|
||||||
|
leading = BackButton(
|
||||||
|
onPressed: () => unawaited(context.router.root.maybePop()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
leading = context.isTablet
|
||||||
|
? CloseButton(onPressed: () => unawaited(context.maybePop()))
|
||||||
|
: BackButton(onPressed: () => unawaited(context.maybePop()));
|
||||||
|
}
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: context.isTablet
|
leading: leading,
|
||||||
? CloseButton(onPressed: () => context.maybePop())
|
title: title == null ? null : Text(title!),
|
||||||
: BackButton(onPressed: () => context.maybePop()),
|
actions: actions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class ImAdaptiveRouteWrapper extends StatelessWidget {
|
|||||||
return ImAdaptiveScaffoldBody(
|
return ImAdaptiveScaffoldBody(
|
||||||
primaryBody: primaryBody,
|
primaryBody: primaryBody,
|
||||||
secondaryBody:
|
secondaryBody:
|
||||||
ctx.topRoute.name != primaryRoute ? (_) => child : null,
|
ctx.topRoute.name == primaryRoute ? null : (_) => child,
|
||||||
bodyRatio: bodyRatio,
|
bodyRatio: bodyRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,11 @@ class ImAdaptiveScaffoldBody extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AdaptiveLayout(
|
return AdaptiveLayout(
|
||||||
internalAnimations: false,
|
|
||||||
transitionDuration: Durations.medium2,
|
|
||||||
bodyRatio: bodyRatio,
|
|
||||||
body: SlotLayout(
|
body: SlotLayout(
|
||||||
config: {
|
config: {
|
||||||
Breakpoints.standard: SlotLayout.from(
|
Breakpoints.standard: SlotLayout.from(
|
||||||
key: const Key('ImAdaptiveScaffold Body Standard'),
|
|
||||||
builder: primaryBody,
|
builder: primaryBody,
|
||||||
|
key: const Key('ImAdaptiveScaffold Body Standard'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -37,11 +34,14 @@ class ImAdaptiveScaffoldBody extends StatelessWidget {
|
|||||||
/// No secondary body in mobile layouts
|
/// No secondary body in mobile layouts
|
||||||
Breakpoints.small: SlotLayoutConfig.empty(),
|
Breakpoints.small: SlotLayoutConfig.empty(),
|
||||||
Breakpoints.mediumAndUp: SlotLayout.from(
|
Breakpoints.mediumAndUp: SlotLayout.from(
|
||||||
key: const Key('ImAdaptiveScaffold Secondary Body Medium'),
|
|
||||||
builder: secondaryBody,
|
builder: secondaryBody,
|
||||||
|
key: const Key('ImAdaptiveScaffold Secondary Body Medium'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
bodyRatio: bodyRatio,
|
||||||
|
transitionDuration: Durations.medium2,
|
||||||
|
internalAnimations: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final gridHasPadding = !context.isTablet && _showAppBar.value;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: BlocProvider(
|
body: BlocProvider(
|
||||||
create: (_) => AssetGridCubit(
|
create: (_) => AssetGridCubit(
|
||||||
@ -33,32 +35,35 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
ImAssetGrid(
|
ImAssetGrid(
|
||||||
topPadding: kToolbarHeight + context.mediaQueryPadding.top - 8,
|
topPadding: gridHasPadding
|
||||||
),
|
? kToolbarHeight + context.mediaQueryPadding.top - 8
|
||||||
ValueListenableBuilder(
|
: null,
|
||||||
valueListenable: _showAppBar,
|
|
||||||
builder: (_, shouldShow, appBar) {
|
|
||||||
final Duration duration;
|
|
||||||
if (shouldShow) {
|
|
||||||
// Animate out app bar slower
|
|
||||||
duration = Durations.short3;
|
|
||||||
} else {
|
|
||||||
// Animate in app bar faster
|
|
||||||
duration = Durations.medium2;
|
|
||||||
}
|
|
||||||
return AnimatedPositioned(
|
|
||||||
duration: duration,
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: shouldShow
|
|
||||||
? 0
|
|
||||||
: -(kToolbarHeight + context.mediaQueryPadding.top),
|
|
||||||
child: appBar!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const ImAppBar(),
|
|
||||||
),
|
),
|
||||||
|
if (!context.isTablet)
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: _showAppBar,
|
||||||
|
builder: (_, shouldShow, appBar) {
|
||||||
|
final Duration duration;
|
||||||
|
if (shouldShow) {
|
||||||
|
// Animate out app bar slower
|
||||||
|
duration = Durations.short3;
|
||||||
|
} else {
|
||||||
|
// Animate in app bar faster
|
||||||
|
duration = Durations.medium2;
|
||||||
|
}
|
||||||
|
return AnimatedPositioned(
|
||||||
|
left: 0,
|
||||||
|
top: shouldShow
|
||||||
|
? 0
|
||||||
|
: -(kToolbarHeight + context.mediaQueryPadding.top),
|
||||||
|
right: 0,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
duration: duration,
|
||||||
|
child: appBar!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const ImAppBar(),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -55,15 +57,15 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
|
|
||||||
void _onLoginPageStateChange(BuildContext context, LoginPageState state) {
|
void _onLoginPageStateChange(BuildContext context, LoginPageState state) {
|
||||||
if (state.isLoginSuccessful) {
|
if (state.isLoginSuccessful) {
|
||||||
context.replaceRoute(const TabControllerRoute());
|
unawaited(context.replaceRoute(const TabControllerRoute()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final PreferredSizeWidget? appBar;
|
final PreferredSizeWidget? appBar;
|
||||||
late final Widget primaryBody;
|
final Widget primaryBody;
|
||||||
late final Widget secondaryBody;
|
final Widget secondaryBody;
|
||||||
|
|
||||||
Widget rotatingLogo = GestureDetector(
|
Widget rotatingLogo = GestureDetector(
|
||||||
onDoubleTap: _populateDemoCredentials,
|
onDoubleTap: _populateDemoCredentials,
|
||||||
@ -73,7 +75,7 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
children: [
|
children: [
|
||||||
RotationTransition(
|
RotationTransition(
|
||||||
turns: _animationController,
|
turns: _animationController,
|
||||||
child: const ImLogo(width: 100),
|
child: const ImLogo(dimension: 100),
|
||||||
),
|
),
|
||||||
const SizedGap.lh(),
|
const SizedGap.lh(),
|
||||||
const ImLogoText(),
|
const ImLogoText(),
|
||||||
@ -104,7 +106,7 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.navigateRoot(const LogsRoute()),
|
onPressed: () => unawaited(context.navigateRoot(const LogsRoute())),
|
||||||
child: const Text('Logs'),
|
child: const Text('Logs'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -122,7 +124,9 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => launchUrl(Uri.parse(_serverUrlController.text)),
|
onTap: () => unawaited(
|
||||||
|
launchUrl(Uri.parse(_serverUrlController.text)),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_serverUrlController.text,
|
_serverUrlController.text,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -157,12 +161,12 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
bottom,
|
bottom,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
secondaryBody = const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BlocListener<LoginPageCubit, LoginPageState>(
|
return BlocListener<LoginPageCubit, LoginPageState>(
|
||||||
listener: _onLoginPageStateChange,
|
listener: _onLoginPageStateChange,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ImAdaptiveScaffoldBody(
|
child: ImAdaptiveScaffoldBody(
|
||||||
@ -170,6 +174,7 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
secondaryBody: (_) => secondaryBody,
|
secondaryBody: (_) => secondaryBody,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -182,13 +187,14 @@ class _MobileAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
scrolledUnderElevation: 0.0,
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => context.navigateRoot(const SettingsRoute()),
|
onPressed: () =>
|
||||||
|
unawaited(context.navigateRoot(const SettingsRoute())),
|
||||||
icon: const Icon(Symbols.settings),
|
icon: const Icon(Symbols.settings),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
scrolledUnderElevation: 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogMixin {
|
|||||||
// Check for /.well-known/immich
|
// Check for /.well-known/immich
|
||||||
url = await loginService.resolveEndpoint(uri);
|
url = await loginService.resolveEndpoint(uri);
|
||||||
|
|
||||||
di<IStoreRepository>().upsert(StoreKey.serverEndpoint, url);
|
await di<IStoreRepository>().upsert(StoreKey.serverEndpoint, url);
|
||||||
await di<LoginService>().handlePostUrlResolution(url);
|
await di<LoginService>().handlePostUrlResolution(url);
|
||||||
|
|
||||||
emit(state.copyWith(isServerValidated: true));
|
emit(state.copyWith(isServerValidated: true));
|
||||||
|
@ -34,14 +34,14 @@ class LoginForm extends StatelessWidget {
|
|||||||
builder: (_, isServerValidated) => SingleChildScrollView(
|
builder: (_, isServerValidated) => SingleChildScrollView(
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: Durations.medium1,
|
duration: Durations.medium1,
|
||||||
|
layoutBuilder: (current, previous) =>
|
||||||
|
current ?? (previous.lastOrNull ?? const SizedBox.shrink()),
|
||||||
child: isServerValidated
|
child: isServerValidated
|
||||||
? _CredentialsForm(
|
? _CredentialsForm(
|
||||||
emailController: emailController,
|
emailController: emailController,
|
||||||
passwordController: passwordController,
|
passwordController: passwordController,
|
||||||
)
|
)
|
||||||
: _ServerForm(controller: serverUrlController),
|
: _ServerForm(controller: serverUrlController),
|
||||||
layoutBuilder: (current, previous) =>
|
|
||||||
current ?? (previous.lastOrNull ?? const SizedBox.shrink()),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -75,13 +75,13 @@ class _ServerFormState extends State<_ServerForm> {
|
|||||||
child: BlocSelector<LoginPageCubit, LoginPageState, bool>(
|
child: BlocSelector<LoginPageCubit, LoginPageState, bool>(
|
||||||
selector: (model) => model.isValidationInProgress,
|
selector: (model) => model.isValidationInProgress,
|
||||||
builder: (_, isValidationInProgress) => Column(
|
builder: (_, isValidationInProgress) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
ImTextFormField(
|
ImTextFormField(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
label: context.t.login.label.endpoint,
|
|
||||||
validator: context.read<LoginPageCubit>().validateServerUrl,
|
validator: context.read<LoginPageCubit>().validateServerUrl,
|
||||||
|
label: context.t.login.label.endpoint,
|
||||||
autoFillHints: const [AutofillHints.url],
|
autoFillHints: const [AutofillHints.url],
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
textInputAction: TextInputAction.go,
|
textInputAction: TextInputAction.go,
|
||||||
@ -89,10 +89,10 @@ class _ServerFormState extends State<_ServerForm> {
|
|||||||
),
|
),
|
||||||
const SizedGap.mh(),
|
const SizedGap.mh(),
|
||||||
ImFilledButton(
|
ImFilledButton(
|
||||||
label: context.t.login.label.next_button,
|
|
||||||
icon: Symbols.arrow_forward_rounded,
|
icon: Symbols.arrow_forward_rounded,
|
||||||
onPressed: () => unawaited(_validateForm(context)),
|
onPressed: () => unawaited(_validateForm(context)),
|
||||||
isDisabled: isValidationInProgress,
|
isDisabled: isValidationInProgress,
|
||||||
|
label: context.t.login.label.next_button,
|
||||||
),
|
),
|
||||||
const SizedGap.mh(),
|
const SizedGap.mh(),
|
||||||
if (isValidationInProgress) const ImLoadingIndicator(),
|
if (isValidationInProgress) const ImLoadingIndicator(),
|
||||||
@ -117,11 +117,11 @@ class _CredentialsForm extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CredentialsFormState extends State<_CredentialsForm> {
|
class _CredentialsFormState extends State<_CredentialsForm> {
|
||||||
final passwordFocusNode = FocusNode();
|
final _passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
passwordFocusNode.dispose();
|
_passwordFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,28 +134,27 @@ class _CredentialsFormState extends State<_CredentialsForm> {
|
|||||||
: ValueListenableBuilder(
|
: ValueListenableBuilder(
|
||||||
valueListenable: di<ServerFeatureConfigProvider>(),
|
valueListenable: di<ServerFeatureConfigProvider>(),
|
||||||
builder: (_, state, __) => Column(
|
builder: (_, state, __) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (state.features.hasPasswordLogin) ...[
|
if (state.features.hasPasswordLogin) ...[
|
||||||
ImTextFormField(
|
ImTextFormField(
|
||||||
controller: widget.emailController,
|
controller: widget.emailController,
|
||||||
label: context.t.login.label.email,
|
label: context.t.login.label.email,
|
||||||
isDisabled: isValidationInProgress,
|
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onSubmitted: (_) => passwordFocusNode.requestFocus(),
|
isDisabled: isValidationInProgress,
|
||||||
|
onSubmitted: (_) => _passwordFocusNode.requestFocus(),
|
||||||
),
|
),
|
||||||
const SizedGap.mh(),
|
const SizedGap.mh(),
|
||||||
ImPasswordFormField(
|
ImPasswordFormField(
|
||||||
controller: widget.passwordController,
|
controller: widget.passwordController,
|
||||||
|
focusNode: _passwordFocusNode,
|
||||||
label: context.t.login.label.password,
|
label: context.t.login.label.password,
|
||||||
focusNode: passwordFocusNode,
|
|
||||||
isDisabled: isValidationInProgress,
|
|
||||||
textInputAction: TextInputAction.go,
|
textInputAction: TextInputAction.go,
|
||||||
|
isDisabled: isValidationInProgress,
|
||||||
),
|
),
|
||||||
const SizedGap.mh(),
|
const SizedGap.mh(),
|
||||||
ImFilledButton(
|
ImFilledButton(
|
||||||
label: context.t.login.label.login_button,
|
|
||||||
icon: Symbols.login_rounded,
|
icon: Symbols.login_rounded,
|
||||||
onPressed: () => unawaited(
|
onPressed: () => unawaited(
|
||||||
context.read<LoginPageCubit>().passwordLogin(
|
context.read<LoginPageCubit>().passwordLogin(
|
||||||
@ -163,31 +162,32 @@ class _CredentialsFormState extends State<_CredentialsForm> {
|
|||||||
password: widget.passwordController.text,
|
password: widget.passwordController.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
label: context.t.login.label.login_button,
|
||||||
),
|
),
|
||||||
// Divider when both password and oAuth login is enabled
|
// Divider when both password and oAuth login is enabled
|
||||||
if (state.features.hasOAuthLogin) const Divider(),
|
if (state.features.hasOAuthLogin) const Divider(),
|
||||||
],
|
],
|
||||||
if (state.features.hasOAuthLogin)
|
if (state.features.hasOAuthLogin)
|
||||||
ImFilledButton(
|
ImFilledButton(
|
||||||
label: state.config.oauthButtonText ??
|
|
||||||
context.t.login.label.oauth_button,
|
|
||||||
icon: Symbols.pin_rounded,
|
icon: Symbols.pin_rounded,
|
||||||
onPressed: () => unawaited(
|
onPressed: () => unawaited(
|
||||||
context.read<LoginPageCubit>().oAuthLogin(),
|
context.read<LoginPageCubit>().oAuthLogin(),
|
||||||
),
|
),
|
||||||
|
label: state.config.oauthButtonText ??
|
||||||
|
context.t.login.label.oauth_button,
|
||||||
),
|
),
|
||||||
if (!state.features.hasPasswordLogin &&
|
if (!state.features.hasPasswordLogin &&
|
||||||
!state.features.hasOAuthLogin)
|
!state.features.hasOAuthLogin)
|
||||||
ImFilledButton(
|
ImFilledButton(
|
||||||
label: context.t.login.label.login_disabled,
|
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
|
label: context.t.login.label.login_disabled,
|
||||||
),
|
),
|
||||||
const SizedGap.sh(),
|
const SizedGap.sh(),
|
||||||
ImTextButton(
|
ImTextButton(
|
||||||
label: context.t.login.label.back_button,
|
|
||||||
icon: Symbols.arrow_back_rounded,
|
icon: Symbols.arrow_back_rounded,
|
||||||
onPressed:
|
onPressed:
|
||||||
context.read<LoginPageCubit>().resetServerValidation,
|
context.read<LoginPageCubit>().resetServerValidation,
|
||||||
|
label: context.t.login.label.back_button,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class LogsPage extends StatelessWidget {
|
|
||||||
const LogsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Scaffold(body: Center(child: Text("Logs Page")));
|
|
||||||
}
|
|
||||||
}
|
|
201
mobile-v2/lib/presentation/modules/logs/pages/logs.page.dart
Normal file
201
mobile-v2/lib/presentation/modules/logs/pages/logs.page.dart
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.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/skeletonized_future_builder.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_appbar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/presentation/theme/app_typography.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/color.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/log_manager.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LogsWrapperPage extends StatelessWidget {
|
||||||
|
const LogsWrapperPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ImAdaptiveRouteWrapper(
|
||||||
|
primaryRoute: LogsRoute.name,
|
||||||
|
primaryBody: (_) => const LogsPage(),
|
||||||
|
bodyRatio: RatioConstants.oneThird,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LogsPage extends StatefulWidget {
|
||||||
|
const LogsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _LogsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogsPageState extends State<LogsPage> {
|
||||||
|
void _onClearLogs() {
|
||||||
|
// refetch logs on clear
|
||||||
|
setState(() {
|
||||||
|
unawaited(LogManager.I.clearLogs());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: ImAdaptiveRouteAppBar(
|
||||||
|
title: context.t.logs.title,
|
||||||
|
isPrimary: true,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _onClearLogs,
|
||||||
|
icon: Icon(Symbols.delete_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SkeletonizedFutureBuilder(
|
||||||
|
future: di<ILogRepository>().getAll(),
|
||||||
|
builder: (_, data) => _LogList(logs: data!),
|
||||||
|
loadingBuilder: (_) => const _LogListShimmer(),
|
||||||
|
errorBuilder: (_, __) => const _LogListEmpty(),
|
||||||
|
emptyBuilder: (_) => const _LogListEmpty(),
|
||||||
|
emptyWhen: (data) => data == null || data.isEmpty,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogLevelIndicator extends StatelessWidget {
|
||||||
|
final LogLevel level;
|
||||||
|
|
||||||
|
const _LogLevelIndicator({required this.level});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: switch (level) {
|
||||||
|
LogLevel.info => context.colorScheme.primary,
|
||||||
|
LogLevel.error ||
|
||||||
|
LogLevel.wtf =>
|
||||||
|
Colors.redAccent.harmonizeWith(context.colorScheme.primary),
|
||||||
|
LogLevel.warning =>
|
||||||
|
Colors.orangeAccent.harmonizeWith(context.colorScheme.primary),
|
||||||
|
LogLevel.verbose ||
|
||||||
|
LogLevel.debug =>
|
||||||
|
Colors.grey.harmonizeWith(context.colorScheme.primary),
|
||||||
|
},
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogList extends StatelessWidget {
|
||||||
|
final List<LogMessage> logs;
|
||||||
|
|
||||||
|
const _LogList({required this.logs});
|
||||||
|
|
||||||
|
/// Truncate the log message to a [maxLines]] number of lines
|
||||||
|
String _truncateLogMessage(String message) {
|
||||||
|
final msg = message.split("\n").firstOrNull;
|
||||||
|
return msg?.substring(0, 75.clamp(0, msg.length)) ?? message;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTileColor(BuildContext context, LogLevel level) {
|
||||||
|
return switch (level) {
|
||||||
|
LogLevel.info => Colors.transparent,
|
||||||
|
LogLevel.error || LogLevel.wtf => Colors.redAccent
|
||||||
|
.harmonizeWith(context.colorScheme.primary)
|
||||||
|
.withOpacity(RatioConstants.halfQuarter),
|
||||||
|
LogLevel.warning => Colors.orangeAccent
|
||||||
|
.harmonizeWith(context.colorScheme.primary)
|
||||||
|
.withOpacity(RatioConstants.halfQuarter),
|
||||||
|
LogLevel.verbose || LogLevel.debug => context.colorScheme.primary
|
||||||
|
.harmonizeWith(context.colorScheme.primary)
|
||||||
|
.withOpacity(RatioConstants.halfQuarter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.separated(
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final log = logs[i];
|
||||||
|
return ListTile(
|
||||||
|
leading: _LogLevelIndicator(level: log.level),
|
||||||
|
title: Text(
|
||||||
|
_truncateLogMessage(log.content),
|
||||||
|
style: AppTypography.bodyMedium,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"at ${DateFormat("HH:mm:ss.SSS").format(log.createdAt)} in ${log.logger ?? "<NA>"}",
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
color: context.colorScheme.onSurface
|
||||||
|
.darken(amount: RatioConstants.oneThird),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const Icon(Symbols.arrow_forward_ios_rounded, size: 18),
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tileColor: _getTileColor(context, log.level),
|
||||||
|
minLeadingWidth: 10,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => Divider(height: 0),
|
||||||
|
itemCount: logs.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogListShimmer extends StatelessWidget {
|
||||||
|
const _LogListShimmer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.separated(
|
||||||
|
itemBuilder: (_, __) => ListTile(
|
||||||
|
leading: Bone.circle(size: 20),
|
||||||
|
title: Bone.text(words: 3),
|
||||||
|
subtitle: Bone.text(words: 1),
|
||||||
|
),
|
||||||
|
separatorBuilder: (_, __) => Divider(height: 5, thickness: 0.5),
|
||||||
|
itemCount: 15,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogListEmpty extends StatelessWidget {
|
||||||
|
const _LogListEmpty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.comments_disabled_rounded,
|
||||||
|
size: 50,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedGap.mh(),
|
||||||
|
Text(context.t.logs.no_logs),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,18 +5,18 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
|
|
||||||
enum SettingSection {
|
enum SettingSection {
|
||||||
general._(
|
general._(
|
||||||
icon: Symbols.interests_rounded,
|
|
||||||
labelKey: 'settings.sections.general',
|
labelKey: 'settings.sections.general',
|
||||||
|
icon: Symbols.interests_rounded,
|
||||||
destination: GeneralSettingsRoute(),
|
destination: GeneralSettingsRoute(),
|
||||||
),
|
),
|
||||||
advance._(
|
advance._(
|
||||||
icon: Symbols.build_rounded,
|
|
||||||
labelKey: 'settings.sections.advance',
|
labelKey: 'settings.sections.advance',
|
||||||
|
icon: Symbols.build_rounded,
|
||||||
destination: AdvanceSettingsRoute(),
|
destination: AdvanceSettingsRoute(),
|
||||||
),
|
),
|
||||||
about._(
|
about._(
|
||||||
icon: Symbols.help_rounded,
|
|
||||||
labelKey: 'settings.sections.about',
|
labelKey: 'settings.sections.about',
|
||||||
|
icon: Symbols.help_rounded,
|
||||||
destination: AboutSettingsRoute(),
|
destination: AboutSettingsRoute(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,14 +12,14 @@ class AboutSettingsPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const ImAdaptiveRouteSecondaryAppBar(),
|
appBar: const ImAdaptiveRouteAppBar(isPrimary: false),
|
||||||
body: ListTile(
|
body: ListTile(
|
||||||
title: Text(context.t.settings.about.third_party_title),
|
title: Text(context.t.settings.about.third_party_title),
|
||||||
subtitle: Text(context.t.settings.about.third_party_sub_title),
|
subtitle: Text(context.t.settings.about.third_party_sub_title),
|
||||||
onTap: () => showLicensePage(
|
onTap: () => showLicensePage(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: context.t.immich,
|
applicationName: context.t.immich,
|
||||||
applicationIcon: const ImLogo(width: SizeConstants.xl),
|
applicationIcon: const ImLogo(dimension: SizeConstants.xl),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ class AdvanceSettingsPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
appBar: ImAdaptiveRouteSecondaryAppBar(),
|
appBar: ImAdaptiveRouteAppBar(isPrimary: false),
|
||||||
body: Center(child: Text('Advanced Settings')),
|
body: Center(child: Text('Advanced Settings')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ class GeneralSettingsPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
appBar: ImAdaptiveRouteSecondaryAppBar(),
|
appBar: ImAdaptiveRouteAppBar(isPrimary: false),
|
||||||
body: Center(child: Text('General Settings')),
|
body: Center(child: Text('General Settings')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
@ -15,32 +17,31 @@ class SettingsWrapperPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ImAdaptiveRouteWrapper(
|
return ImAdaptiveRouteWrapper(
|
||||||
primaryBody: (_) => const SettingsPage(),
|
|
||||||
primaryRoute: SettingsRoute.name,
|
primaryRoute: SettingsRoute.name,
|
||||||
bodyRatio: BodyRatioConstants.oneThird,
|
primaryBody: (_) => const SettingsPage(),
|
||||||
|
bodyRatio: RatioConstants.oneThird,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: prefer-single-widget-per-file
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const ImAdaptiveRoutePrimaryAppBar(),
|
appBar: const ImAdaptiveRouteAppBar(isPrimary: true),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: SettingSection.values.length,
|
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final section = SettingSection.values.elementAt(index);
|
final section = SettingSection.values.elementAt(index);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(context.t[section.labelKey]),
|
|
||||||
onTap: () => context.navigateRoot(section.destination),
|
|
||||||
leading: Icon(section.icon),
|
leading: Icon(section.icon),
|
||||||
|
title: Text(context.t[section.labelKey]),
|
||||||
|
onTap: () => unawaited(context.navigateRoot(section.destination)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
itemCount: SettingSection.values.length,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ class SplashScreenWrapperPage extends AutoRouter implements AutoRouteWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: prefer-single-widget-per-file
|
|
||||||
class SplashScreenPage extends StatefulWidget {
|
class SplashScreenPage extends StatefulWidget {
|
||||||
const SplashScreenPage({super.key});
|
const SplashScreenPage({super.key});
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ class _SplashScreenState extends State<SplashScreenPage>
|
|||||||
future: di.allReady(),
|
future: di.allReady(),
|
||||||
builder: (_, snap) {
|
builder: (_, snap) {
|
||||||
if (snap.hasData) {
|
if (snap.hasData) {
|
||||||
_tryLogin();
|
unawaited(_tryLogin());
|
||||||
} else if (snap.hasError) {
|
} else if (snap.hasError) {
|
||||||
log.wtf(
|
log.wtf(
|
||||||
"Error while initializing the app",
|
"Error while initializing the app",
|
||||||
@ -75,7 +74,7 @@ class _SplashScreenState extends State<SplashScreenPage>
|
|||||||
return Center(
|
return Center(
|
||||||
child: RotationTransition(
|
child: RotationTransition(
|
||||||
turns: _animationController,
|
turns: _animationController,
|
||||||
child: const ImLogo(width: 100),
|
child: const ImLogo(dimension: 100),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,14 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/common/immich_navigation_rail.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -24,7 +31,7 @@ class TabControllerPage extends StatelessWidget {
|
|||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: tabsRouter.activeIndex == 0,
|
canPop: tabsRouter.activeIndex == 0,
|
||||||
onPopInvokedWithResult: (didPop, _) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
didPop ? null : tabsRouter.setActiveIndex(0),
|
||||||
child: _TabControllerAdaptiveScaffold(
|
child: _TabControllerAdaptiveScaffold(
|
||||||
body: (ctxx) => child,
|
body: (ctxx) => child,
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
@ -80,53 +87,117 @@ class _TabControllerAdaptiveScaffold extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: AdaptiveLayout(
|
body: AdaptiveLayout(
|
||||||
// No animation on layout change
|
|
||||||
transitionDuration: Duration.zero,
|
|
||||||
primaryNavigation: SlotLayout(
|
primaryNavigation: SlotLayout(
|
||||||
config: <Breakpoint, SlotLayoutConfig>{
|
config: <Breakpoint, SlotLayoutConfig>{
|
||||||
Breakpoints.mediumAndUp: SlotLayout.from(
|
Breakpoints.mediumAndUp: SlotLayout.from(
|
||||||
key: const Key(
|
builder: (_) => _ImNavigationRailBuilder(
|
||||||
'_TabControllerAdaptiveScaffold Primary Navigation Medium',
|
|
||||||
),
|
|
||||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
|
||||||
selectedIndex: selectedIndex,
|
|
||||||
destinations: destinations
|
destinations: destinations
|
||||||
.map((NavigationDestination destination) =>
|
.map((NavigationDestination destination) =>
|
||||||
AdaptiveScaffold.toRailDestination(destination))
|
AdaptiveScaffold.toRailDestination(destination))
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: onSelectedIndexChange,
|
selectedIndex: selectedIndex,
|
||||||
backgroundColor: navRailTheme.backgroundColor,
|
backgroundColor: navRailTheme.backgroundColor,
|
||||||
|
leading: ImUserAvatar(
|
||||||
|
user: di<CurrentUserProvider>().value,
|
||||||
|
dimension: SizeConstants.m,
|
||||||
|
radius: SizeConstants.m,
|
||||||
|
),
|
||||||
|
trailing: ImLogo(dimension: SizeConstants.xm),
|
||||||
|
onDestinationSelected: onSelectedIndexChange,
|
||||||
selectedIconTheme: navRailTheme.selectedIconTheme,
|
selectedIconTheme: navRailTheme.selectedIconTheme,
|
||||||
unselectedIconTheme: navRailTheme.unselectedIconTheme,
|
unselectedIconTheme: navRailTheme.unselectedIconTheme,
|
||||||
selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle,
|
selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle,
|
||||||
unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle,
|
unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle,
|
||||||
),
|
),
|
||||||
|
key: const Key(
|
||||||
|
'_TabControllerAdaptiveScaffold Primary Navigation Medium',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SlotLayout(
|
body: SlotLayout(
|
||||||
config: {
|
config: {
|
||||||
Breakpoints.standard: SlotLayout.from(
|
Breakpoints.standard: SlotLayout.from(
|
||||||
key: const Key('_TabControllerAdaptiveScaffold Body'),
|
|
||||||
builder: body,
|
builder: body,
|
||||||
|
key: const Key('_TabControllerAdaptiveScaffold Body'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// No animation on layout change
|
||||||
|
transitionDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
bottomNavigationBar: SlotLayout(
|
bottomNavigationBar: SlotLayout(
|
||||||
config: <Breakpoint, SlotLayoutConfig>{
|
config: <Breakpoint, SlotLayoutConfig>{
|
||||||
Breakpoints.small: SlotLayout.from(
|
Breakpoints.small: SlotLayout.from(
|
||||||
|
builder: (_) => AdaptiveScaffold.standardBottomNavigationBar(
|
||||||
|
destinations: destinations,
|
||||||
|
currentIndex: selectedIndex,
|
||||||
|
onDestinationSelected: onSelectedIndexChange,
|
||||||
|
),
|
||||||
key: const Key(
|
key: const Key(
|
||||||
'_TabControllerAdaptiveScaffold Bottom Navigation Small',
|
'_TabControllerAdaptiveScaffold Bottom Navigation Small',
|
||||||
),
|
),
|
||||||
builder: (_) => AdaptiveScaffold.standardBottomNavigationBar(
|
|
||||||
currentIndex: selectedIndex,
|
|
||||||
destinations: destinations,
|
|
||||||
onDestinationSelected: onSelectedIndexChange,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ImNavigationRailBuilder extends StatelessWidget {
|
||||||
|
final List<NavigationRailDestination> destinations;
|
||||||
|
final int? selectedIndex;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Function(int)? onDestinationSelected;
|
||||||
|
final IconThemeData? selectedIconTheme;
|
||||||
|
final IconThemeData? unselectedIconTheme;
|
||||||
|
final TextStyle? selectedLabelTextStyle;
|
||||||
|
final TextStyle? unSelectedLabelTextStyle;
|
||||||
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
const _ImNavigationRailBuilder({
|
||||||
|
required this.destinations,
|
||||||
|
this.selectedIndex,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
this.onDestinationSelected,
|
||||||
|
this.selectedIconTheme,
|
||||||
|
this.unselectedIconTheme,
|
||||||
|
this.selectedLabelTextStyle,
|
||||||
|
this.unSelectedLabelTextStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Builder(builder: (BuildContext _) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 72,
|
||||||
|
height: context.height,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext _, BoxConstraints constraints) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: ImNavigationRail(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
leading: leading,
|
||||||
|
trailing: trailing,
|
||||||
|
destinations: destinations,
|
||||||
|
selectedIndex: selectedIndex,
|
||||||
|
onDestinationSelected: onDestinationSelected,
|
||||||
|
labelType: NavigationRailLabelType.none,
|
||||||
|
unselectedLabelTextStyle: unSelectedLabelTextStyle,
|
||||||
|
selectedLabelTextStyle: selectedLabelTextStyle,
|
||||||
|
unselectedIconTheme: unselectedIconTheme,
|
||||||
|
selectedIconTheme: selectedIconTheme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:immich_mobile/presentation/modules/home/pages/home.page.dart';
|
import 'package:immich_mobile/presentation/modules/home/pages/home.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/library/pages/library.page.dart';
|
import 'package:immich_mobile/presentation/modules/library/pages/library.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/pages/login.page.dart';
|
import 'package:immich_mobile/presentation/modules/login/pages/login.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/logs/pages/log.page.dart';
|
import 'package:immich_mobile/presentation/modules/logs/pages/logs.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/search/pages/search.page.dart';
|
import 'package:immich_mobile/presentation/modules/search/pages/search.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/settings/pages/about_settings.page.dart';
|
import 'package:immich_mobile/presentation/modules/settings/pages/about_settings.page.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/settings/pages/advance_settings.page.dart';
|
import 'package:immich_mobile/presentation/modules/settings/pages/advance_settings.page.dart';
|
||||||
@ -29,12 +29,15 @@ class AppRouter extends RootStackRouter {
|
|||||||
List<AutoRoute> get routes => [
|
List<AutoRoute> get routes => [
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SplashScreenWrapperRoute.page,
|
page: SplashScreenWrapperRoute.page,
|
||||||
initial: true,
|
|
||||||
children: [
|
children: [
|
||||||
AutoRoute(page: SplashScreenRoute.page, initial: true),
|
AutoRoute(page: SplashScreenRoute.page, initial: true),
|
||||||
AutoRoute(page: LoginRoute.page),
|
AutoRoute(page: LoginRoute.page),
|
||||||
],
|
],
|
||||||
|
initial: true,
|
||||||
),
|
),
|
||||||
|
AutoRoute(page: LogsWrapperRoute.page, children: [
|
||||||
|
AutoRoute(page: LogsRoute.page),
|
||||||
|
]),
|
||||||
AutoRoute(page: LogsRoute.page),
|
AutoRoute(page: LogsRoute.page),
|
||||||
AutoRoute(page: TabControllerRoute.page, children: [
|
AutoRoute(page: TabControllerRoute.page, children: [
|
||||||
AutoRoute(page: HomeRoute.page),
|
AutoRoute(page: HomeRoute.page),
|
||||||
|
@ -19,7 +19,7 @@ class AppThemeProvider extends ValueNotifier<AppTheme> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_appSettingSubscription.cancel();
|
unawaited(_appSettingSubscription.cancel());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
@ -17,7 +18,7 @@ enum GalleryPermissionStatus {
|
|||||||
|
|
||||||
class GalleryPermissionProvider extends ValueNotifier<GalleryPermissionStatus> {
|
class GalleryPermissionProvider extends ValueNotifier<GalleryPermissionStatus> {
|
||||||
GalleryPermissionProvider() : super(GalleryPermissionStatus.yetToRequest) {
|
GalleryPermissionProvider() : super(GalleryPermissionStatus.yetToRequest) {
|
||||||
checkPermission();
|
unawaited(checkPermission());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasPermission => value.isGranted || value.isLimited;
|
bool get hasPermission => value.isGranted || value.isLimited;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
abstract class AppColors {
|
abstract final class AppColors {
|
||||||
const AppColors();
|
const AppColors();
|
||||||
|
|
||||||
/// Blue color
|
/// Blue color
|
||||||
@ -25,9 +25,9 @@ abstract class AppColors {
|
|||||||
onErrorContainer: Color(0xff410002),
|
onErrorContainer: Color(0xff410002),
|
||||||
surface: Color(0xFFF0EFF4),
|
surface: Color(0xFFF0EFF4),
|
||||||
onSurface: Color(0xff1a1b21),
|
onSurface: Color(0xff1a1b21),
|
||||||
onSurfaceVariant: Color(0xff444651),
|
|
||||||
surfaceContainer: Color(0xfffefbff),
|
surfaceContainer: Color(0xfffefbff),
|
||||||
surfaceContainerHighest: Color(0xffe0e2ef),
|
surfaceContainerHighest: Color(0xffe0e2ef),
|
||||||
|
onSurfaceVariant: Color(0xff444651),
|
||||||
outline: Color(0xff747782),
|
outline: Color(0xff747782),
|
||||||
outlineVariant: Color(0xffc4c6d3),
|
outlineVariant: Color(0xffc4c6d3),
|
||||||
shadow: Color(0xff000000),
|
shadow: Color(0xff000000),
|
||||||
@ -58,9 +58,9 @@ abstract class AppColors {
|
|||||||
onErrorContainer: Color(0xffffb4ab),
|
onErrorContainer: Color(0xffffb4ab),
|
||||||
surface: Color(0xFF15181C),
|
surface: Color(0xFF15181C),
|
||||||
onSurface: Color(0xffe2e2e9),
|
onSurface: Color(0xffe2e2e9),
|
||||||
onSurfaceVariant: Color(0xffc2c6d2),
|
|
||||||
surfaceContainer: Color(0xff1a1e22),
|
surfaceContainer: Color(0xff1a1e22),
|
||||||
surfaceContainerHighest: Color(0xff424852),
|
surfaceContainerHighest: Color(0xff424852),
|
||||||
|
onSurfaceVariant: Color(0xffc2c6d2),
|
||||||
outline: Color(0xff8c919c),
|
outline: Color(0xff8c919c),
|
||||||
outlineVariant: Color(0xff424751),
|
outlineVariant: Color(0xff424751),
|
||||||
shadow: Color(0xff000000),
|
shadow: Color(0xff000000),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/presentation/theme/app_colors.dart';
|
import 'package:immich_mobile/presentation/theme/app_colors.dart';
|
||||||
import 'package:immich_mobile/presentation/theme/app_typography.dart';
|
import 'package:immich_mobile/presentation/theme/app_typography.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
|
import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
enum AppTheme {
|
enum AppTheme {
|
||||||
blue._(AppColors.blueLight, AppColors.blueDark),
|
blue._(AppColors.blueLight, AppColors.blueDark),
|
||||||
@ -15,9 +16,58 @@ enum AppTheme {
|
|||||||
|
|
||||||
static ThemeData generateThemeData(ColorScheme color) {
|
static ThemeData generateThemeData(ColorScheme color) {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: color.error),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: color.primary),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: color.error),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: color.outlineVariant),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
colorScheme: color,
|
colorScheme: color,
|
||||||
primaryColor: color.primary,
|
primaryColor: color.primary,
|
||||||
iconTheme: const IconThemeData(weight: 500, opticalSize: 24),
|
scaffoldBackgroundColor: color.surface,
|
||||||
|
iconTheme: const IconThemeData(size: 24, weight: 500, opticalSize: 24),
|
||||||
|
textTheme: TextTheme(
|
||||||
|
displayLarge: AppTypography.displayLarge,
|
||||||
|
displayMedium: AppTypography.displayMedium,
|
||||||
|
displaySmall: AppTypography.displaySmall,
|
||||||
|
headlineLarge: AppTypography.headlineLarge,
|
||||||
|
headlineMedium: AppTypography.headlineMedium,
|
||||||
|
headlineSmall: AppTypography.headlineSmall,
|
||||||
|
titleLarge: AppTypography.titleLarge,
|
||||||
|
titleMedium: AppTypography.titleMedium,
|
||||||
|
titleSmall: AppTypography.titleSmall,
|
||||||
|
bodyLarge: AppTypography.bodyLarge,
|
||||||
|
bodyMedium: AppTypography.bodyMedium,
|
||||||
|
bodySmall: AppTypography.bodySmall,
|
||||||
|
labelLarge: AppTypography.labelLarge,
|
||||||
|
labelMedium: AppTypography.labelMedium,
|
||||||
|
labelSmall: AppTypography.labelSmall,
|
||||||
|
),
|
||||||
|
actionIconTheme: ActionIconThemeData(
|
||||||
|
backButtonIconBuilder: (_) => Icon(Symbols.arrow_back_rounded),
|
||||||
|
closeButtonIconBuilder: (_) => Icon(Symbols.close_rounded),
|
||||||
|
),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
iconTheme: IconThemeData(size: 22, color: color.onSurface),
|
||||||
|
titleTextStyle:
|
||||||
|
AppTypography.titleLarge.copyWith(color: color.onSurface),
|
||||||
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: color.surfaceContainer,
|
backgroundColor: color.surfaceContainer,
|
||||||
indicatorColor: color.primary,
|
indicatorColor: color.primary,
|
||||||
@ -30,78 +80,38 @@ enum AppTheme {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: color.surface,
|
|
||||||
navigationRailTheme: NavigationRailThemeData(
|
navigationRailTheme: NavigationRailThemeData(
|
||||||
backgroundColor: color.surfaceContainer,
|
backgroundColor: color.surfaceContainer,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
indicatorColor: color.primary,
|
|
||||||
selectedIconTheme:
|
|
||||||
IconThemeData(weight: 500, opticalSize: 24, color: color.onPrimary),
|
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
weight: 500,
|
weight: 500,
|
||||||
opticalSize: 24,
|
opticalSize: 24,
|
||||||
color: color.onSurface.withAlpha(175),
|
color: color.onSurface.withAlpha(175),
|
||||||
),
|
),
|
||||||
|
selectedIconTheme:
|
||||||
|
IconThemeData(weight: 500, opticalSize: 24, color: color.onPrimary),
|
||||||
|
indicatorColor: color.primary,
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: color.primary),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: color.outlineVariant),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
|
||||||
),
|
|
||||||
errorBorder: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
|
||||||
borderSide: BorderSide(color: color.error),
|
|
||||||
),
|
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
|
||||||
borderSide: BorderSide(color: color.error),
|
|
||||||
),
|
|
||||||
hintStyle: const TextStyle(
|
|
||||||
fontSize: 16.0,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textSelectionTheme: TextSelectionThemeData(cursorColor: color.primary),
|
|
||||||
sliderTheme: SliderThemeData(
|
sliderTheme: SliderThemeData(
|
||||||
valueIndicatorColor:
|
valueIndicatorColor:
|
||||||
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
||||||
.withAlpha(240),
|
.withAlpha(240),
|
||||||
),
|
),
|
||||||
textTheme: TextTheme(
|
|
||||||
titleLarge: AppTypography.titleLarge,
|
|
||||||
titleMedium: AppTypography.titleMedium,
|
|
||||||
titleSmall: AppTypography.titleSmall,
|
|
||||||
displayLarge: AppTypography.displayLarge,
|
|
||||||
displayMedium: AppTypography.displayMedium,
|
|
||||||
displaySmall: AppTypography.displaySmall,
|
|
||||||
headlineLarge: AppTypography.headlineLarge,
|
|
||||||
headlineMedium: AppTypography.headlineMedium,
|
|
||||||
headlineSmall: AppTypography.headlineSmall,
|
|
||||||
bodyLarge: AppTypography.bodyLarge,
|
|
||||||
bodyMedium: AppTypography.bodyMedium,
|
|
||||||
bodySmall: AppTypography.bodySmall,
|
|
||||||
labelLarge: AppTypography.labelLarge,
|
|
||||||
labelMedium: AppTypography.labelMedium,
|
|
||||||
labelSmall: AppTypography.labelSmall,
|
|
||||||
),
|
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
elevation: 4,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
|
||||||
),
|
|
||||||
insetPadding: const EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 25.0),
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
||||||
.withAlpha(240),
|
.withAlpha(240),
|
||||||
actionTextColor: color.inversePrimary,
|
actionTextColor: color.inversePrimary,
|
||||||
contentTextStyle: TextStyle(color: color.onInverseSurface),
|
contentTextStyle: TextStyle(color: color.onInverseSurface),
|
||||||
|
elevation: 4,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
insetPadding: const EdgeInsets.fromLTRB(20.0, 5.0, 20.0, 25.0),
|
||||||
closeIconColor: color.onInverseSurface,
|
closeIconColor: color.onInverseSurface,
|
||||||
),
|
),
|
||||||
|
textSelectionTheme: TextSelectionThemeData(cursorColor: color.primary),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppTypography {
|
abstract final class AppTypography {
|
||||||
const AppTypography();
|
const AppTypography();
|
||||||
|
|
||||||
static const TextStyle displayLarge = TextStyle(
|
static const TextStyle displayLarge = TextStyle(
|
||||||
@ -30,16 +30,16 @@ class AppTypography {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle titleLarge = TextStyle(
|
static const TextStyle titleLarge = TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.normal,
|
||||||
);
|
);
|
||||||
static const TextStyle titleMedium = TextStyle(
|
static const TextStyle titleMedium = TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.normal,
|
||||||
);
|
);
|
||||||
static const TextStyle titleSmall = TextStyle(
|
static const TextStyle titleSmall = TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.normal,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const TextStyle bodyLarge = TextStyle(
|
static const TextStyle bodyLarge = TextStyle(
|
||||||
|
@ -47,7 +47,7 @@ import 'package:immich_mobile/utils/immich_api_client.dart';
|
|||||||
|
|
||||||
final di = GetIt.I;
|
final di = GetIt.I;
|
||||||
|
|
||||||
class ServiceLocator {
|
abstract final class ServiceLocator {
|
||||||
const ServiceLocator._internal();
|
const ServiceLocator._internal();
|
||||||
|
|
||||||
static void _registerFactory<T extends Object>(T Function() factoryFun) {
|
static void _registerFactory<T extends Object>(T Function() factoryFun) {
|
||||||
@ -118,19 +118,19 @@ class ServiceLocator {
|
|||||||
/// API Repos
|
/// API Repos
|
||||||
_registerFactory<IAlbumETagRepository>(() => AlbumETagRepository(db: di()));
|
_registerFactory<IAlbumETagRepository>(() => AlbumETagRepository(db: di()));
|
||||||
_registerFactory<ISyncApiRepository>(
|
_registerFactory<ISyncApiRepository>(
|
||||||
() => SyncApiRepository(syncApi: di<ImApiClient>().getSyncApi()),
|
() => SyncApiRepository(syncApi: di<ImApiClient>().syncApi),
|
||||||
);
|
);
|
||||||
_registerFactory<IServerApiRepository>(
|
_registerFactory<IServerApiRepository>(
|
||||||
() => ServerApiRepository(serverApi: di<ImApiClient>().getServerApi()),
|
() => ServerApiRepository(serverApi: di<ImApiClient>().serverApi),
|
||||||
);
|
);
|
||||||
_registerFactory<IAuthenticationApiRepository>(
|
_registerFactory<IAuthenticationApiRepository>(
|
||||||
() => AuthenticationApiRepository(
|
() => AuthenticationApiRepository(
|
||||||
authenticationApi: di<ImApiClient>().getAuthenticationApi(),
|
authenticationApi: di<ImApiClient>().authenticationApi,
|
||||||
oAuthApi: di<ImApiClient>().getOAuthApi(),
|
oAuthApi: di<ImApiClient>().oAuthApi,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_registerFactory<IUserApiRepository>(
|
_registerFactory<IUserApiRepository>(
|
||||||
() => UserApiRepository(usersApi: di<ImApiClient>().getUsersApi()),
|
() => UserApiRepository(usersApi: di<ImApiClient>().usersApi),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,9 +144,9 @@ class ServiceLocator {
|
|||||||
_registerFactory<LoginService>(() => const LoginService());
|
_registerFactory<LoginService>(() => const LoginService());
|
||||||
_registerFactory<HashService>(() => HashService(
|
_registerFactory<HashService>(() => HashService(
|
||||||
hostService: di(),
|
hostService: di(),
|
||||||
assetToHashRepo: di(),
|
|
||||||
deviceAlbumRepo: di(),
|
|
||||||
deviceAssetRepo: di(),
|
deviceAssetRepo: di(),
|
||||||
|
deviceAlbumRepo: di(),
|
||||||
|
assetToHashRepo: di(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class CollectionUtil {
|
abstract final class CollectionUtil {
|
||||||
const CollectionUtil();
|
const CollectionUtil();
|
||||||
|
|
||||||
static int compareToNullable<T extends Comparable>(T? a, T? b) {
|
static int compareToNullable<T extends Comparable>(T? a, T? b) {
|
||||||
@ -18,22 +18,22 @@ class CollectionUtil {
|
|||||||
/// Find the difference between the two lists [first] and [second]
|
/// Find the difference between the two lists [first] and [second]
|
||||||
/// Results are passed as callbacks back to the caller during the comparison
|
/// Results are passed as callbacks back to the caller during the comparison
|
||||||
static FutureOr<bool> diffLists<T>(
|
static FutureOr<bool> diffLists<T>(
|
||||||
List<T> first,
|
List<T> firstList,
|
||||||
List<T> second, {
|
List<T> secondList, {
|
||||||
required int Function(T a, T b) compare,
|
required int Function(T a, T b) compare,
|
||||||
required FutureOr<bool> Function(T a, T b) both,
|
required FutureOr<bool> Function(T a, T b) both,
|
||||||
required FutureOr<void> Function(T a) onlyFirst,
|
required FutureOr<void> Function(T a) onlyFirst,
|
||||||
required FutureOr<void> Function(T b) onlySecond,
|
required FutureOr<void> Function(T b) onlySecond,
|
||||||
}) async {
|
}) async {
|
||||||
first.sort(compare);
|
final first =
|
||||||
first.uniqueConsecutive(compare);
|
_uniqueConsecutive(List.of(firstList)..sort(compare), compare);
|
||||||
second.sort(compare);
|
final second =
|
||||||
second.uniqueConsecutive(compare);
|
_uniqueConsecutive(List.of(secondList)..sort(compare), compare);
|
||||||
|
|
||||||
bool diff = false;
|
bool diff = false;
|
||||||
int i = 0, j = 0;
|
int i = 0, j = 0;
|
||||||
|
|
||||||
for (; i < first.length && j < second.length;) {
|
while (i < first.length && j < second.length) {
|
||||||
final int order = compare(first[i], second[j]);
|
final int order = compare(first[i], second[j]);
|
||||||
if (order == 0) {
|
if (order == 0) {
|
||||||
diff |= await both(first[i++], second[j++]);
|
diff |= await both(first[i++], second[j++]);
|
||||||
@ -49,27 +49,25 @@ class CollectionUtil {
|
|||||||
diff |= i < first.length || j < second.length;
|
diff |= i < first.length || j < second.length;
|
||||||
|
|
||||||
for (; i < first.length; i++) {
|
for (; i < first.length; i++) {
|
||||||
onlyFirst(first[i]);
|
await onlyFirst(first[i]);
|
||||||
}
|
}
|
||||||
for (; j < second.length; j++) {
|
for (; j < second.length; j++) {
|
||||||
onlySecond(second[j]);
|
await onlySecond(second[j]);
|
||||||
}
|
}
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _ListExtension<T> on List<T> {
|
List<T> _uniqueConsecutive<T>(List<T> list, int Function(T a, T b) compare) {
|
||||||
List<T> uniqueConsecutive(int Function(T a, T b) compare) {
|
if (list.isEmpty) return list;
|
||||||
int i = 1, j = 1;
|
|
||||||
for (; i < length; i++) {
|
List<T> unique = [];
|
||||||
if (compare(this[i - 1], this[i]) != 0) {
|
unique.add(list.first);
|
||||||
if (i != j) {
|
|
||||||
this[j] = this[i];
|
for (int i = 1; i < list.length; i++) {
|
||||||
}
|
if (compare(list[i], list[i - 1]) != 0) {
|
||||||
j++;
|
unique.add(list[i]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
length = length == 0 ? 0 : j;
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
return unique;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class SizeConstants {
|
abstract final class SizeConstants {
|
||||||
const SizeConstants._();
|
const SizeConstants._();
|
||||||
|
|
||||||
static const s = 8.0;
|
static const s = 8.0;
|
||||||
static const m = 16.0;
|
static const m = 16.0;
|
||||||
|
static const xm = 25.0;
|
||||||
static const l = 32.0;
|
static const l = 32.0;
|
||||||
static const xl = 64.0;
|
static const xl = 64.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BodyRatioConstants {
|
abstract final class RatioConstants {
|
||||||
const BodyRatioConstants._();
|
const RatioConstants._();
|
||||||
|
|
||||||
|
// 0.3
|
||||||
static const oneThird = 1 / 3;
|
static const oneThird = 1 / 3;
|
||||||
|
// 0.25
|
||||||
|
static const quarter = 1 / 4;
|
||||||
|
// 0.15
|
||||||
|
static const halfQuarter = 3 / 20;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
extension SortIterable<T> on Iterable<T> {
|
extension SortIterable<T> on Iterable<T> {
|
||||||
Iterable<T> sortedBy(Comparable Function(T k) key) =>
|
Iterable<T> sortedBy(Comparable Function(T k) key) =>
|
||||||
toList()..sort((a, b) => key(a).compareTo(key(b)));
|
List.of(this)..sort((a, b) => key(a).compareTo(key(b)));
|
||||||
}
|
}
|
||||||
|
@ -66,9 +66,9 @@ class ImApiClient extends ApiClient with LogMixin {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
UsersApi getUsersApi() => UsersApi(this);
|
UsersApi get usersApi => UsersApi(this);
|
||||||
ServerApi getServerApi() => ServerApi(this);
|
ServerApi get serverApi => ServerApi(this);
|
||||||
AuthenticationApi getAuthenticationApi() => AuthenticationApi(this);
|
AuthenticationApi get authenticationApi => AuthenticationApi(this);
|
||||||
OAuthApi getOAuthApi() => OAuthApi(this);
|
OAuthApi get oAuthApi => OAuthApi(this);
|
||||||
SyncApi getSyncApi() => SyncApi(this);
|
SyncApi get syncApi => SyncApi(this);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
|
|
||||||
@ -11,11 +12,15 @@ enum AssetMediaSize {
|
|||||||
final String value;
|
final String value;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImImageUrlHelper {
|
abstract final class ImImageUrlHelper {
|
||||||
const ImImageUrlHelper();
|
const ImImageUrlHelper();
|
||||||
|
|
||||||
static String get _serverUrl => di<ImApiClient>().basePath;
|
static String get _serverUrl => di<ImApiClient>().basePath;
|
||||||
|
|
||||||
|
static String getUserAvatarUrl(final User user) {
|
||||||
|
return '$_serverUrl/users/${user.id}/profile-image';
|
||||||
|
}
|
||||||
|
|
||||||
static String getThumbnailUrl(
|
static String getThumbnailUrl(
|
||||||
final Asset asset, {
|
final Asset asset, {
|
||||||
AssetMediaSize type = AssetMediaSize.thumbnail,
|
AssetMediaSize type = AssetMediaSize.thumbnail,
|
||||||
|
@ -40,7 +40,7 @@ class IsolateHelper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void postIsolateHandling() {
|
Future<void> postIsolateHandling() async {
|
||||||
assert(_clientData != null);
|
assert(_clientData != null);
|
||||||
// Reconstruct client from cached data
|
// Reconstruct client from cached data
|
||||||
final client = ImApiClient(endpoint: _clientData!.endpoint);
|
final client = ImApiClient(endpoint: _clientData!.endpoint);
|
||||||
@ -55,7 +55,7 @@ class IsolateHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Init log manager to continue listening to log events
|
// Init log manager to continue listening to log events
|
||||||
LogManager.I.init(shouldBuffer: false);
|
await LogManager.I.init(shouldBuffer: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<T> run<T>(FutureOr<T> Function() computation) async {
|
static Future<T> run<T>(FutureOr<T> Function() computation) async {
|
||||||
@ -70,10 +70,10 @@ class IsolateHelper {
|
|||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
// Delay to ensure the isolate is ready
|
// Delay to ensure the isolate is ready
|
||||||
await Future.delayed(Durations.short2);
|
await Future.delayed(Durations.short2);
|
||||||
helper.postIsolateHandling();
|
await helper.postIsolateHandling();
|
||||||
try {
|
try {
|
||||||
final result = await computation();
|
final result = await computation();
|
||||||
// Delay to ensure the isolate is not killed prematurely
|
// Wait for isolate to end; i.e, logs to be flushed
|
||||||
await Future.delayed(Durations.short2);
|
await Future.delayed(Durations.short2);
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: avoid-collection-mutating-methods
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -13,6 +15,7 @@ import 'package:logging/logging.dart' as logging;
|
|||||||
/// in the class.
|
/// in the class.
|
||||||
class LogManager {
|
class LogManager {
|
||||||
LogManager._();
|
LogManager._();
|
||||||
|
|
||||||
static final LogManager _instance = LogManager._();
|
static final LogManager _instance = LogManager._();
|
||||||
static final Map<String, Logger> _loggers = <String, Logger>{};
|
static final Map<String, Logger> _loggers = <String, Logger>{};
|
||||||
|
|
||||||
@ -40,10 +43,10 @@ class LogManager {
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
final lm = LogMessage(
|
final lm = LogMessage(
|
||||||
logger: record.loggerName,
|
|
||||||
content: record.message,
|
content: record.message,
|
||||||
level: record.level.toLogLevel(),
|
level: record.level.toLogLevel(),
|
||||||
createdAt: record.time,
|
createdAt: record.time,
|
||||||
|
logger: record.loggerName,
|
||||||
error: record.error?.toString(),
|
error: record.error?.toString(),
|
||||||
stack: record.stackTrace?.toString(),
|
stack: record.stackTrace?.toString(),
|
||||||
);
|
);
|
||||||
@ -53,7 +56,7 @@ class LogManager {
|
|||||||
_timer ??=
|
_timer ??=
|
||||||
Timer(const Duration(seconds: 5), () => _flushBufferToDatabase());
|
Timer(const Duration(seconds: 5), () => _flushBufferToDatabase());
|
||||||
} else {
|
} else {
|
||||||
di<ILogRepository>().create(lm);
|
unawaited(di<ILogRepository>().create(lm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +64,13 @@ class LogManager {
|
|||||||
_timer = null;
|
_timer = null;
|
||||||
final buffer = _msgBuffer;
|
final buffer = _msgBuffer;
|
||||||
_msgBuffer = [];
|
_msgBuffer = [];
|
||||||
di<ILogRepository>().createAll(buffer);
|
unawaited(di<ILogRepository>().createAll(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
void init({bool? shouldBuffer}) {
|
Future<void> init({bool? shouldBuffer}) async {
|
||||||
_shouldBuffer = shouldBuffer ?? _shouldBuffer;
|
_shouldBuffer = shouldBuffer ?? _shouldBuffer;
|
||||||
_subscription = logging.Logger.root.onRecord.listen(_onLogRecord);
|
_subscription = logging.Logger.root.onRecord.listen(_onLogRecord);
|
||||||
|
await di<ILogRepository>().truncate();
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger get(String? loggerName) => _loggers.putIfAbsent(
|
Logger get(String? loggerName) => _loggers.putIfAbsent(
|
||||||
@ -80,14 +84,14 @@ class LogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription.cancel();
|
unawaited(_subscription.cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearLogs() {
|
Future<void> clearLogs() async {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
_msgBuffer.clear();
|
_msgBuffer.clear();
|
||||||
di<ILogRepository>().deleteAll();
|
await di<ILogRepository>().deleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setGlobalErrorCallbacks() {
|
static void setGlobalErrorCallbacks() {
|
||||||
|
@ -35,10 +35,10 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefault(dynamic value, String keys, dynamic defaultValue) {
|
addDefault(Map value, String keys, dynamic defaultValue) {
|
||||||
// Loop through the keys and assign the default value if the key is not present
|
// Loop through the keys and assign the default value if the key is not present
|
||||||
List<String> keyList = keys.split('.');
|
List<String> keyList = keys.split('.');
|
||||||
dynamic current = value;
|
Map current = value;
|
||||||
|
|
||||||
for (int i = 0; i < keyList.length - 1; i++) {
|
for (int i = 0; i < keyList.length - 1; i++) {
|
||||||
if (current[keyList[i]] == null) {
|
if (current[keyList[i]] == null) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||||
|
|
||||||
class SnackbarManager {
|
abstract final class SnackbarManager {
|
||||||
const SnackbarManager();
|
const SnackbarManager();
|
||||||
|
|
||||||
static ScaffoldMessengerState? get _s => kScafMessengerKey.currentState;
|
static ScaffoldMessengerState? get _s => kScafMessengerKey.currentState;
|
||||||
|
@ -976,6 +976,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
skeletonizer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: skeletonizer
|
||||||
|
sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -54,6 +54,7 @@ dependencies:
|
|||||||
flutter_list_view: ^1.1.28
|
flutter_list_view: ^1.1.28
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
flutter_cache_manager: ^3.4.1
|
flutter_cache_manager: ^3.4.1
|
||||||
|
skeletonizer: ^1.4.2
|
||||||
|
|
||||||
openapi:
|
openapi:
|
||||||
path: openapi
|
path: openapi
|
||||||
|
@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user