Merge branch 'main' into feat/custom-date-range

This commit is contained in:
Yaros
2026-05-13 18:46:08 +02:00
426 changed files with 18698 additions and 4342 deletions
+2
View File
@@ -34,6 +34,7 @@ linter:
unrelated_type_equality_checks: true
prefer_const_constructors: true
always_use_package_imports: true
always_put_control_body_on_new_line: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -50,6 +51,7 @@ analyzer:
# - custom_lint
errors:
unawaited_futures: warning
always_put_control_body_on_new_line: warning
custom_lint:
rules:
+8 -1
View File
@@ -64,8 +64,15 @@ android {
}
release {
signingConfig signingConfigs.release
def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0
signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
def prNumber = System.getenv("PR_NUMBER")
if (prNumber) {
applicationIdSuffix ".pr${prNumber}"
versionNameSuffix "-pr${prNumber}"
}
}
}
namespace 'app.alextran.immich'
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
}
}
}
fun onAndroidUpload(callback: (Result<Unit>) -> Unit)
fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
channel.send(listOf(maxMinutesArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This method acts as a bridge between the native Android background task system and Flutter.
*/
override fun onInitialized() {
flutterApi?.onAndroidUpload { handleHostResult(it) }
flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
}
// TODO: Move this to a separate NotificationManager class
+9 -3
View File
@@ -217,7 +217,9 @@ List<TranslationParam> _extractParams(String value) {
final icuType = match.group(2)!;
final icuContent = match.group(3) ?? '';
if (params.containsKey(name)) continue;
if (params.containsKey(name)) {
continue;
}
String type;
if (icuType == 'plural' || icuType == 'number') {
@@ -238,7 +240,9 @@ List<TranslationParam> _extractParams(String value) {
for (var i = 0; i < value.length; i++) {
if (value[i] == '{') {
if (depth == 0) icuStart = i;
if (depth == 0) {
icuStart = i;
}
depth++;
} else if (value[i] == '}') {
depth--;
@@ -256,7 +260,9 @@ List<TranslationParam> _extractParams(String value) {
for (final match in simpleRegex.allMatches(cleanedValue)) {
final name = match.group(1)!;
if (params.containsKey(name)) continue;
if (params.containsKey(name)) {
continue;
}
String type;
if (_kIntParamNames.contains(name.toLowerCase())) {
+117 -117
View File
@@ -1003,20 +1003,6 @@
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
"unique": false,
"columns": []
}
},
{
"id": 12,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "UQ_remote_assets_owner_checksum",
@@ -1026,7 +1012,7 @@
}
},
{
"id": 13,
"id": 12,
"references": [
1
],
@@ -1040,7 +1026,7 @@
}
},
{
"id": 14,
"id": 13,
"references": [
1
],
@@ -1054,7 +1040,7 @@
}
},
{
"id": 15,
"id": 14,
"references": [
1
],
@@ -1067,36 +1053,22 @@
"columns": []
}
},
{
"id": 15,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
"unique": false,
"columns": []
}
},
{
"id": 16,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_day",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 17,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_month",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 18,
"references": [],
"type": "table",
"data": {
@@ -1226,7 +1198,7 @@
}
},
{
"id": 19,
"id": 17,
"references": [
0
],
@@ -1301,7 +1273,7 @@
}
},
{
"id": 20,
"id": 18,
"references": [
0
],
@@ -1388,7 +1360,7 @@
}
},
{
"id": 21,
"id": 19,
"references": [
1
],
@@ -1644,7 +1616,7 @@
}
},
{
"id": 22,
"id": 20,
"references": [
1,
4
@@ -1718,7 +1690,7 @@
}
},
{
"id": 23,
"id": 21,
"references": [
4,
0
@@ -1806,7 +1778,7 @@
}
},
{
"id": 24,
"id": 22,
"references": [
1
],
@@ -1902,7 +1874,7 @@
}
},
{
"id": 25,
"id": 23,
"references": [
0
],
@@ -2066,10 +2038,10 @@
}
},
{
"id": 26,
"id": 24,
"references": [
1,
25
23
],
"type": "table",
"data": {
@@ -2140,7 +2112,7 @@
}
},
{
"id": 27,
"id": 25,
"references": [
0
],
@@ -2284,10 +2256,10 @@
}
},
{
"id": 28,
"id": 26,
"references": [
1,
27
25
],
"type": "table",
"data": {
@@ -2461,7 +2433,7 @@
}
},
{
"id": 29,
"id": 27,
"references": [],
"type": "table",
"data": {
@@ -2509,7 +2481,7 @@
}
},
{
"id": 30,
"id": 28,
"references": [],
"type": "table",
"data": {
@@ -2684,7 +2656,7 @@
}
},
{
"id": 31,
"id": 29,
"references": [
1
],
@@ -2778,7 +2750,7 @@
}
},
{
"id": 32,
"id": 30,
"references": [],
"type": "table",
"data": {
@@ -2826,13 +2798,13 @@
}
},
{
"id": 33,
"id": 31,
"references": [
20
18
],
"type": "index",
"data": {
"on": 20,
"on": 18,
"name": "idx_partner_shared_with_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
"unique": false,
@@ -2840,19 +2812,47 @@
}
},
{
"id": 34,
"id": 32,
"references": [
21
19
],
"type": "index",
"data": {
"on": 21,
"on": 19,
"name": "idx_lat_lng",
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
"unique": false,
"columns": []
}
},
{
"id": 33,
"references": [
19
],
"type": "index",
"data": {
"on": 19,
"name": "idx_remote_exif_city",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 34,
"references": [
20
],
"type": "index",
"data": {
"on": 20,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{
"id": 35,
"references": [
@@ -2861,20 +2861,6 @@
"type": "index",
"data": {
"on": 22,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{
"id": 36,
"references": [
24
],
"type": "index",
"data": {
"on": 24,
"name": "idx_remote_asset_cloud_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
"unique": false,
@@ -2882,13 +2868,13 @@
}
},
{
"id": 37,
"id": 36,
"references": [
27
25
],
"type": "index",
"data": {
"on": 27,
"on": 25,
"name": "idx_person_owner_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
"unique": false,
@@ -2896,13 +2882,13 @@
}
},
{
"id": 38,
"id": 37,
"references": [
28
26
],
"type": "index",
"data": {
"on": 28,
"on": 26,
"name": "idx_asset_face_person_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
"unique": false,
@@ -2910,13 +2896,13 @@
}
},
{
"id": 39,
"id": 38,
"references": [
28
26
],
"type": "index",
"data": {
"on": 28,
"on": 26,
"name": "idx_asset_face_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
"unique": false,
@@ -2924,13 +2910,27 @@
}
},
{
"id": 40,
"id": 39,
"references": [
30
26
],
"type": "index",
"data": {
"on": 30,
"on": 26,
"name": "idx_asset_face_visible_person",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 40,
"references": [
28
],
"type": "index",
"data": {
"on": 28,
"name": "idx_trashed_local_asset_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
"unique": false,
@@ -2940,11 +2940,11 @@
{
"id": 41,
"references": [
30
28
],
"type": "index",
"data": {
"on": 30,
"on": 28,
"name": "idx_trashed_local_asset_album",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
"unique": false,
@@ -2954,11 +2954,11 @@
{
"id": 42,
"references": [
31
29
],
"type": "index",
"data": {
"on": 31,
"on": 29,
"name": "idx_asset_edit_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
"unique": false,
@@ -3066,15 +3066,6 @@
}
]
},
{
"name": "idx_remote_asset_owner_checksum",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
}
]
},
{
"name": "UQ_remote_assets_owner_checksum",
"sql": [
@@ -3112,20 +3103,11 @@
]
},
{
"name": "idx_remote_asset_local_date_time_day",
"name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))"
}
]
},
{
"name": "idx_remote_asset_local_date_time_month",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
}
]
},
@@ -3282,6 +3264,15 @@
}
]
},
{
"name": "idx_remote_exif_city",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
}
]
},
{
"name": "idx_remote_album_asset_album_asset",
"sql": [
@@ -3327,6 +3318,15 @@
}
]
},
{
"name": "idx_asset_face_visible_person",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
}
]
},
{
"name": "idx_trashed_local_asset_checksum",
"sql": [
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol {
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
}
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
}
}
}
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) {
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage(nil) { response in
channel.sendMessage([maxMinutesArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
@@ -61,8 +61,12 @@ class RemoteAlbum {
@override
bool operator ==(Object other) {
if (other is! RemoteAlbum) return false;
if (identical(this, other)) return true;
if (other is! RemoteAlbum) {
return false;
}
if (identical(this, other)) {
return true;
}
return id == other.id &&
name == other.name &&
ownerId == other.ownerId &&
@@ -49,8 +49,12 @@ class LocalAlbum {
@override
bool operator ==(Object other) {
if (other is! LocalAlbum) return false;
if (identical(this, other)) return true;
if (other is! LocalAlbum) {
return false;
}
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.name == name &&
@@ -51,12 +51,18 @@ sealed class BaseAsset {
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video;
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
if (isVideo) {
return AssetPlaybackStyle.video;
}
if (isMotionPhoto) {
return AssetPlaybackStyle.livePhoto;
}
if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated;
}
if (isImage) return AssetPlaybackStyle.image;
if (isImage) {
return AssetPlaybackStyle.image;
}
return AssetPlaybackStyle.unknown;
}
@@ -98,7 +104,9 @@ sealed class BaseAsset {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is BaseAsset) {
return name == other.name &&
type == other.type &&
@@ -74,8 +74,12 @@ class LocalAsset extends BaseAsset {
// Not checking for remoteId here
@override
bool operator ==(Object other) {
if (other is! LocalAsset) return false;
if (identical(this, other)) return true;
if (other is! LocalAsset) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other &&
id == other.id &&
cloudId == other.cloudId &&
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
final AssetVisibility visibility;
final String ownerId;
final String? stackId;
final DateTime? uploadedAt;
const RemoteAsset({
required this.id,
@@ -20,6 +21,7 @@ class RemoteAsset extends BaseAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
this.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -55,6 +57,7 @@ class RemoteAsset extends BaseAsset {
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
uploadedAt: ${uploadedAt ?? "<NA>"},
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
@@ -71,14 +74,19 @@ class RemoteAsset extends BaseAsset {
// Not checking for localId here
@override
bool operator ==(Object other) {
if (other is! RemoteAsset) return false;
if (identical(this, other)) return true;
if (other is! RemoteAsset) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other &&
id == other.id &&
ownerId == other.ownerId &&
thumbHash == other.thumbHash &&
visibility == other.visibility &&
stackId == other.stackId;
stackId == other.stackId &&
uploadedAt == other.uploadedAt;
}
@override
@@ -89,7 +97,8 @@ class RemoteAsset extends BaseAsset {
localId.hashCode ^
thumbHash.hashCode ^
visibility.hashCode ^
stackId.hashCode;
stackId.hashCode ^
uploadedAt.hashCode;
RemoteAsset copyWith({
String? id,
@@ -100,6 +109,7 @@ class RemoteAsset extends BaseAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -119,6 +129,7 @@ class RemoteAsset extends BaseAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
@@ -144,6 +155,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
super.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -158,8 +170,12 @@ class RemoteAssetExif extends RemoteAsset {
@override
bool operator ==(Object other) {
if (other is! RemoteAssetExif) return false;
if (identical(this, other)) return true;
if (other is! RemoteAssetExif) {
return false;
}
if (identical(this, other)) {
return true;
}
return super == other && exifInfo == other.exifInfo;
}
@@ -176,6 +192,7 @@ class RemoteAssetExif extends RemoteAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -196,6 +213,7 @@ class RemoteAssetExif extends RemoteAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
@@ -68,7 +68,9 @@ class AssetFace {
@override
bool operator ==(covariant AssetFace other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.assetId == assetId &&
@@ -1,18 +1,58 @@
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/image_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
class AppConfig {
final ThemeConfig theme;
final CleanupConfig cleanup;
final MapConfig map;
final TimelineConfig timeline;
final ImageConfig image;
final ViewerConfig viewer;
const AppConfig({this.theme = const ThemeConfig()});
const AppConfig({
this.theme = const .new(),
this.cleanup = const .new(),
this.map = const .new(),
this.timeline = const .new(),
this.image = const .new(),
this.viewer = const .new(),
});
AppConfig copyWith({ThemeConfig? theme}) => .new(theme: theme ?? this.theme);
AppConfig copyWith({
ThemeConfig? theme,
CleanupConfig? cleanup,
MapConfig? map,
TimelineConfig? timeline,
ImageConfig? image,
ViewerConfig? viewer,
}) => .new(
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
map: map ?? this.map,
timeline: timeline ?? this.timeline,
image: image ?? this.image,
viewer: viewer ?? this.viewer,
);
@override
bool operator ==(Object other) => identical(this, other) || (other is AppConfig && other.theme == theme);
bool operator ==(Object other) =>
identical(this, other) ||
(other is AppConfig &&
other.theme == theme &&
other.cleanup == cleanup &&
other.map == map &&
other.timeline == timeline &&
other.image == image &&
other.viewer == viewer);
@override
int get hashCode => theme.hashCode;
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer);
@override
String toString() => 'AppConfig(theme: $theme)';
String toString() =>
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)';
}
@@ -0,0 +1,48 @@
import 'package:immich_mobile/constants/enums.dart';
class CleanupConfig {
final bool keepFavorites;
final AssetKeepType keepMediaType;
final List<String> keepAlbumIds;
final int cutoffDaysAgo;
final bool defaultsInitialized;
const CleanupConfig({
this.keepFavorites = true,
this.keepMediaType = AssetKeepType.none,
this.keepAlbumIds = const [],
this.cutoffDaysAgo = -1,
this.defaultsInitialized = false,
});
CleanupConfig copyWith({
bool? keepFavorites,
AssetKeepType? keepMediaType,
List<String>? keepAlbumIds,
int? cutoffDaysAgo,
bool? defaultsInitialized,
}) => .new(
keepFavorites: keepFavorites ?? this.keepFavorites,
keepMediaType: keepMediaType ?? this.keepMediaType,
keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
cutoffDaysAgo: cutoffDaysAgo ?? this.cutoffDaysAgo,
defaultsInitialized: defaultsInitialized ?? this.defaultsInitialized,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CleanupConfig &&
other.keepFavorites == keepFavorites &&
other.keepMediaType == keepMediaType &&
other.keepAlbumIds == keepAlbumIds &&
other.cutoffDaysAgo == cutoffDaysAgo &&
other.defaultsInitialized == defaultsInitialized);
@override
int get hashCode => Object.hash(keepFavorites, keepMediaType, keepAlbumIds, cutoffDaysAgo, defaultsInitialized);
@override
String toString() =>
'CleanupConfig(keepFavorites: $keepFavorites, keepMediaType: $keepMediaType, keepAlbumIds: $keepAlbumIds, cutoffDaysAgo: $cutoffDaysAgo, defaultsInitialized: $defaultsInitialized)';
}
@@ -0,0 +1,20 @@
class ImageConfig {
final bool preferRemote;
final bool loadOriginal;
const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
@override
int get hashCode => Object.hash(preferRemote, loadOriginal);
@override
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
}
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class MapConfig {
final int relativeDays;
final bool favoritesOnly;
final bool includeArchived;
final ThemeMode themeMode;
final bool withPartners;
const MapConfig({
this.relativeDays = 0,
this.favoritesOnly = false,
this.includeArchived = false,
this.themeMode = ThemeMode.system,
this.withPartners = false,
});
MapConfig copyWith({
int? relativeDays,
bool? favoritesOnly,
bool? includeArchived,
ThemeMode? themeMode,
bool? withPartners,
}) => MapConfig(
relativeDays: relativeDays ?? this.relativeDays,
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
includeArchived: includeArchived ?? this.includeArchived,
themeMode: themeMode ?? this.themeMode,
withPartners: withPartners ?? this.withPartners,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MapConfig &&
other.relativeDays == relativeDays &&
other.favoritesOnly == favoritesOnly &&
other.includeArchived == includeArchived &&
other.themeMode == themeMode &&
other.withPartners == withPartners);
@override
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
@override
String toString() =>
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
}
@@ -0,0 +1,30 @@
import 'package:immich_mobile/domain/models/timeline.model.dart';
class TimelineConfig {
final int tilesPerRow;
final GroupAssetsBy groupAssetsBy;
final bool storageIndicator;
const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true});
TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig(
tilesPerRow: tilesPerRow ?? this.tilesPerRow,
groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy,
storageIndicator: storageIndicator ?? this.storageIndicator,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TimelineConfig &&
other.tilesPerRow == tilesPerRow &&
other.groupAssetsBy == groupAssetsBy &&
other.storageIndicator == storageIndicator);
@override
int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator);
@override
String toString() =>
'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)';
}
@@ -0,0 +1,37 @@
class ViewerConfig {
final bool loopVideo;
final bool loadOriginalVideo;
final bool autoPlayVideo;
final bool tapToNavigate;
const ViewerConfig({
this.loopVideo = true,
this.loadOriginalVideo = false,
this.autoPlayVideo = true,
this.tapToNavigate = false,
});
ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) =>
ViewerConfig(
loopVideo: loopVideo ?? this.loopVideo,
loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo,
autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo,
tapToNavigate: tapToNavigate ?? this.tapToNavigate,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ViewerConfig &&
other.loopVideo == loopVideo &&
other.loadOriginalVideo == loadOriginalVideo &&
other.autoPlayVideo == autoPlayVideo &&
other.tapToNavigate == tapToNavigate);
@override
int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate);
@override
String toString() =>
'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)';
}
+3 -1
View File
@@ -69,7 +69,9 @@ class ExifInfo {
@override
bool operator ==(covariant ExifInfo other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.fileSize == fileSize &&
other.description == description &&
+3 -1
View File
@@ -20,7 +20,9 @@ class LogMessage {
@override
bool operator ==(covariant LogMessage other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.message == message &&
other.level == level &&
+3 -1
View File
@@ -8,7 +8,9 @@ class Marker {
@override
bool operator ==(covariant Marker other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.location == location && other.assetId == assetId;
}
+6 -3
View File
@@ -2,7 +2,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
enum MemoryTypeEnum {
@@ -36,7 +35,9 @@ class MemoryData {
@override
bool operator ==(covariant MemoryData other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.year == year;
}
@@ -132,7 +133,9 @@ class DriftMemory {
@override
bool operator ==(covariant DriftMemory other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return other.id == id &&
+80 -4
View File
@@ -1,9 +1,13 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'),
@@ -15,13 +19,52 @@ enum MetadataDomain<T extends Object> {
enum MetadataKey<T extends Object> {
// Theme
primaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themePrimaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
dynamicTheme<bool>(.appConfig, 'theme.dynamicTheme', false),
colorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
// Image
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
// Viewer
viewerLoopVideo<bool>(.appConfig, 'viewer.loopVideo', true),
viewerLoadOriginalVideo<bool>(.appConfig, 'viewer.loadOriginalVideo', false),
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
// Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>(
.appConfig,
'timeline.groupAssetsBy',
GroupAssetsBy.day,
_EnumCodec(GroupAssetsBy.values),
),
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
// Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
// Map
mapShowFavoriteOnly<bool>(.appConfig, 'map.showFavoriteOnly', false),
mapRelativeDate<int>(.appConfig, 'map.relativeDate', 0),
mapIncludeArchived<bool>(.appConfig, 'map.includeArchived', false),
mapThemeMode<ThemeMode>(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
mapWithPartners<bool>(.appConfig, 'map.withPartners', false),
// Cleanup
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
cleanupKeepMediaType<AssetKeepType>(
.appConfig,
'cleanup.keepMediaType',
AssetKeepType.none,
_EnumCodec(AssetKeepType.values),
),
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false);
final MetadataDomain domain;
final String name;
@@ -88,6 +131,39 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
DateTime? decode(String raw) => DateTime.tryParse(raw);
}
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
final _MetadataCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return null;
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return null;
}
final element = _elementCodec.decode(item);
if (element == null) {
return null;
}
result.add(element);
}
return result;
} on FormatException {
return null;
}
}
}
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
final T? Function(String) _parse;
+6 -2
View File
@@ -69,7 +69,9 @@ class PersonDto {
@override
bool operator ==(covariant PersonDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.birthDate == birthDate &&
@@ -160,7 +162,9 @@ class DriftPerson {
@override
bool operator ==(covariant DriftPerson other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.createdAt == createdAt &&
@@ -12,7 +12,9 @@ class SearchResult {
@override
bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
@@ -1,13 +1,6 @@
import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> {
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
enableBackup<bool>(StoreKey.enableBackup, false);
+6 -2
View File
@@ -37,7 +37,9 @@ class Stack {
@override
bool operator ==(covariant Stack other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.createdAt == createdAt &&
@@ -61,7 +63,9 @@ class StackResponse {
@override
bool operator ==(covariant StackResponse other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds;
}
+23 -45
View File
@@ -19,35 +19,13 @@ enum StoreKey<T> {
backgroundBackup<bool>._(14),
sslClientCertData<String>._(15),
sslClientPasswd<String>._(16),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>._(100),
loadOriginal<bool>._(101),
tilesPerRow<int>._(103),
dynamicLayout<bool>._(104),
groupAssetsBy<int>._(105),
uploadErrorNotificationGracePeriod<int>._(106),
backgroundBackupTotalProgress<bool>._(107),
backgroundBackupSingleProgress<bool>._(108),
storageIndicator<bool>._(109),
thumbnailCacheSize<int>._(110),
imageCacheSize<int>._(111),
albumThumbnailCacheSize<int>._(112),
selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114),
preferRemoteImage<bool>._(116),
loopVideo<bool>._(117),
// map related settings
mapShowFavoriteOnly<bool>._(118),
mapRelativeDate<int>._(119),
selfSignedCert<bool>._(120),
mapIncludeArchived<bool>._(121),
ignoreIcloudAssets<bool>._(122),
selectedAlbumSortReverse<bool>._(123),
mapThemeMode<int>._(124),
mapwithPartners<bool>._(125),
enableHapticFeedback<bool>._(126),
customHeaders<String>._(127),
syncAlbums<bool>._(131),
// Auto endpoint switching
@@ -56,48 +34,46 @@ enum StoreKey<T> {
localEndpoint<String>._(134),
externalEndpointList<String>._(135),
// Video settings
loadOriginalVideo<bool>._(136),
manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings
readonlyModeEnabled<bool>._(138),
autoPlayVideo<bool>._(139),
albumGridView<bool>._(140),
// Image viewer navigation settings
tapToNavigate<bool>._(141),
loadOriginal<bool>._(101),
// Map custom time range settings
mapCustomFrom<String>._(142),
mapCustomTo<String>._(143),
// Experimental stuff
photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002),
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007),
// Free up space
cleanupKeepFavorites<bool>._(1008),
cleanupKeepMediaType<int>._(1009),
cleanupKeepAlbumIds<String>._(1010),
cleanupCutoffDaysAgo<int>._(1011),
cleanupDefaultsInitialized<bool>._(1012),
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store
legacyLoopVideo<bool>._(117),
legacyLoadOriginalVideo<bool>._(136),
legacyAutoPlayVideo<bool>._(139),
legacyTapToNavigate<bool>._(141),
legacyPreferRemoteImage<bool>._(116),
legacyLoadOriginal<bool>._(101),
legacyPrimaryColor<String>._(128),
legacyDynamicTheme<bool>._(129),
legacyColorfulInterface<bool>._(130),
legacyThemeMode<String>._(102),
legacyCleanupKeepFavorites<bool>._(1008),
legacyCleanupKeepMediaType<int>._(1009),
legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012),
legacyTilesPerRow<int>._(103),
legacyGroupAssetsBy<int>._(105),
legacyStorageIndicator<bool>._(109),
legacyMapRelativeDate<int>._(119),
legacyMapShowFavoriteOnly<bool>._(118),
legacyMapIncludeArchived<bool>._(121),
legacyMapThemeMode<int>._(124),
legacyMapwithPartners<bool>._(125),
legacyLogLevel<int>._(115);
const StoreKey._(this.id);
@@ -122,7 +98,9 @@ StoreDto: {
@override
bool operator ==(covariant StoreDto<T> other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.key == key && other.value == value;
}
+3 -1
View File
@@ -13,7 +13,9 @@ class Tag {
@override
bool operator ==(covariant Tag other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id && other.value == value;
}
@@ -2,6 +2,8 @@ enum GroupAssetsBy { day, month, auto, none }
enum HeaderType { none, month, day, monthAndDay }
enum SortAssetsBy { taken, uploaded }
class Bucket {
final int assetCount;
+6 -2
View File
@@ -125,7 +125,9 @@ profileChangedAt: $profileChangedAt
@override
bool operator ==(covariant UserDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
((updatedAt == null && other.updatedAt == null) ||
@@ -219,7 +221,9 @@ class PartnerUserDto {
@override
bool operator ==(covariant PartnerUserDto other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.email == email &&
@@ -35,7 +35,9 @@ isOnboarded: $isOnboarded,
@override
bool operator ==(covariant Onboarding other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return isOnboarded == other.isOnboarded;
}
@@ -132,7 +134,9 @@ showSupportBadge: $showSupportBadge,
@override
bool operator ==(covariant Preferences other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled &&
@@ -199,7 +203,9 @@ licenseKey: $licenseKey,
@override
bool operator ==(covariant License other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey;
}
@@ -251,7 +257,9 @@ license: ${license ?? "<NA>"},
@override
bool operator ==(covariant UserMetadata other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.userId == userId &&
other.key == key &&
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
@override
Future<void> onAndroidUpload() async {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
try {
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
await _handleBackup();
} catch (error, stack) {
_logger.severe("Failed to complete Android background processing", error, stack);
} finally {
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
Future<void> onAndroidUpload(int? maxMinutes) async {
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
return _backgroundLoop(
hashTimeout: hashTimeout,
backupTimeout: backupTimeout,
debugLabel: 'Android background upload',
);
}
@override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
}
Future<void> _backgroundLoop({
required Duration hashTimeout,
required Duration? backupTimeout,
required String debugLabel,
}) async {
_logger.info(
'$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
);
final sw = Stopwatch()..start();
try {
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
if (!await _syncAssets(hashTimeout: timeout)) {
if (!await _syncAssets(hashTimeout: hashTimeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
final backupFuture = _handleBackup();
if (maxSeconds != null) {
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
} else {
Timer? cancelTimer;
if (backupTimeout != null) {
cancelTimer = Timer(backupTimeout, () {
if (!_cancellationToken.isCompleted) {
_logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
_cancellationToken.complete();
}
});
}
try {
await backupFuture;
} finally {
cancelTimer?.cancel();
}
} catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack);
_logger.severe("Failed to complete $debugLabel", error, stack);
} finally {
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
_logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
}
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_logger.info("Cleaning up background worker");
_cancellationToken.complete();
if (!_cancellationToken.isCompleted) {
_cancellationToken.complete();
}
final cleanupFutures = [
nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async {
@@ -93,8 +93,7 @@ class LocalSyncService {
if (CurrentPlatform.isIOS) {
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
// remove the albums from the local database from the previous sync
// does not include changes for cloud albums.
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
for (final album in cloudAlbums) {
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
@@ -184,7 +184,9 @@ class RemoteAlbumService {
List<RemoteAlbum> albums, {
required AssetDateAggregation aggregation,
}) async {
if (albums.isEmpty) return [];
if (albums.isEmpty) {
return [];
}
final albumIds = albums.map((e) => e.id).toList();
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
@@ -72,7 +72,9 @@ class StoreService {
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) return;
if (_cache[key.id] == value) {
return;
}
await _storeRepository.upsert(key, value);
_cache[key.id] = value;
}
@@ -24,6 +24,7 @@ enum SyncMigrationTask {
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
}
class SyncStreamService {
@@ -132,6 +133,13 @@ class SyncStreamService {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
}
}
Future<void> _runPostSyncTasks(List<String> migrations) async {
@@ -318,7 +326,9 @@ class SyncStreamService {
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
@@ -359,7 +369,9 @@ class SyncStreamService {
}
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
if (batchData.isEmpty) {
return;
}
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
@@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@@ -35,18 +34,21 @@ enum TimelineOrigin {
deepLink,
albumActivities,
folder,
recentlyAdded,
}
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
final SettingsService _settingsService;
final MetadataRepository _metadataRepository;
const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService})
: _timelineRepository = timelineRepository,
_settingsService = settingsService;
const TimelineFactory({
required DriftTimelineRepository timelineRepository,
required MetadataRepository metadataRepository,
}) : _timelineRepository = timelineRepository,
_metadataRepository = metadataRepository;
GroupAssetsBy get groupBy {
final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
}
@@ -61,6 +63,8 @@ class TimelineFactory {
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
TimelineService recentlyAdded(String userId) => TimelineService(_timelineRepository.recentlyAdded(userId, groupBy));
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
+3 -1
View File
@@ -30,7 +30,9 @@ class UserService {
Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
if (user == null) {
return null;
}
await _storeService.put(StoreKey.currentUser, user);
return user;
}
@@ -11,6 +11,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
@@ -33,6 +34,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
+6 -2
View File
@@ -94,8 +94,12 @@ class SnapScrollPhysics extends ScrollPhysics {
bool get allowUserScrolling => false;
static double target(ScrollMetrics position, double velocity, double snapOffset) {
if (velocity > _minFlingVelocity) return snapOffset;
if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset;
if (velocity > _minFlingVelocity) {
return snapOffset;
}
if (velocity < -_minFlingVelocity) {
return position.pixels < snapOffset ? 0.0 : snapOffset;
}
return position.pixels < minSnapDistance ? 0.0 : snapOffset;
}
}
@@ -5,6 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person
ON asset_face_entity (person_id, asset_id)
WHERE is_visible = 1 AND deleted_at IS NULL
''')
class AssetFaceEntity extends Table with DriftDefaultsMixin {
const AssetFaceEntity();
@@ -1350,3 +1350,7 @@ i0.Index get idxAssetFaceAssetId => i0.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
i0.Index get idxAssetFaceVisiblePerson => i0.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
@@ -6,6 +6,10 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_exif_city
ON remote_exif_entity (city) WHERE city IS NOT NULL
''')
class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity();
@@ -1883,3 +1883,8 @@ class RemoteExifEntityCompanion
.toString();
}
}
i0.Index get idxRemoteExifCity => i0.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
@@ -4,7 +4,7 @@ import 'local_asset.entity.dart';
import 'local_album.entity.dart';
import 'local_album_asset.entity.dart';
mergedAsset:
mergedAsset:
SELECT
rae.id as remote_id,
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
@@ -27,7 +27,8 @@ SELECT
NULL as longitude,
NULL as adjustmentTime,
rae.is_edited,
0 as playback_style
0 as playback_style,
rae.uploaded_at
FROM
remote_asset_entity rae
LEFT JOIN
@@ -65,7 +66,8 @@ SELECT
lae.longitude,
lae.adjustment_time,
0 as is_edited,
lae.playback_style
lae.playback_style,
NULL as uploaded_at
FROM
local_asset_entity lae
WHERE NOT EXISTS (
+4 -1
View File
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables,
@@ -68,6 +68,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
isEdited: row.read<bool>('is_edited'),
playbackStyle: row.read<int>('playback_style'),
uploadedAt: row.readNullable<DateTime>('uploaded_at'),
),
);
}
@@ -141,6 +142,7 @@ class MergedAssetResult {
final DateTime? adjustmentTime;
final bool isEdited;
final int playbackStyle;
final DateTime? uploadedAt;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -164,6 +166,7 @@ class MergedAssetResult {
this.adjustmentTime,
required this.isEdited,
required this.playbackStyle,
this.uploadedAt,
});
}
@@ -5,9 +5,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql(
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
)
@TableIndex.sql('''
CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum
ON remote_asset_entity (owner_id, checksum)
@@ -20,12 +17,10 @@ WHERE (library_id IS NOT NULL);
''')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
@TableIndex.sql(
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
)
@TableIndex.sql(
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
)
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
''')
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const RemoteAssetEntity();
@@ -43,6 +38,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
DateTimeColumn get deletedAt => dateTime().nullable()();
DateTimeColumn get uploadedAt => dateTime().nullable()();
TextColumn get livePhotoVideoId => text().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()();
@@ -66,6 +63,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
uploadedAt: uploadedAt,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<DateTime?> uploadedAt,
i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility,
i0.Value<String?> stackId,
@@ -49,6 +50,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<DateTime?> uploadedAt,
i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility,
i0.Value<String?> stackId,
@@ -177,6 +179,11 @@ class $$RemoteAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column),
@@ -305,6 +312,11 @@ class $$RemoteAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column),
@@ -412,6 +424,11 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => column,
);
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => column,
@@ -507,6 +524,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility =
const i0.Value.absent(),
@@ -528,6 +546,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -550,6 +569,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
@@ -570,6 +590,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -645,9 +666,9 @@ typedef $$RemoteAssetEntityTableProcessedTableManager =
i1.RemoteAssetEntityData,
i0.PrefetchHooks Function({bool ownerId})
>;
i0.Index get idxRemoteAssetOwnerChecksum => i0.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
@@ -818,6 +839,18 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _uploadedAtMeta = const i0.VerificationMeta(
'uploadedAt',
);
@override
late final i0.GeneratedColumn<DateTime> uploadedAt =
i0.GeneratedColumn<DateTime>(
'uploaded_at',
aliasedName,
true,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
@@ -894,6 +927,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
localDateTime,
thumbHash,
deletedAt,
uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -998,6 +1032,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
);
}
if (data.containsKey('uploaded_at')) {
context.handle(
_uploadedAtMeta,
uploadedAt.isAcceptableOrUnknown(data['uploaded_at']!, _uploadedAtMeta),
);
}
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
@@ -1095,6 +1135,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}deleted_at'],
),
uploadedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}uploaded_at'],
),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id'],
@@ -1153,6 +1197,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? localDateTime;
final String? thumbHash;
final DateTime? deletedAt;
final DateTime? uploadedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility;
final String? stackId;
@@ -1173,6 +1218,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.localDateTime,
this.thumbHash,
this.deletedAt,
this.uploadedAt,
this.livePhotoVideoId,
required this.visibility,
this.stackId,
@@ -1212,6 +1258,9 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
}
if (!nullToAbsent || uploadedAt != null) {
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt);
}
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
}
@@ -1252,6 +1301,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
thumbHash: serializer.fromJson<String?>(json['thumbHash']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
uploadedAt: serializer.fromJson<DateTime?>(json['uploadedAt']),
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromJson(
serializer.fromJson<int>(json['visibility']),
@@ -1281,6 +1331,7 @@ class RemoteAssetEntityData extends i0.DataClass
'localDateTime': serializer.toJson<DateTime?>(localDateTime),
'thumbHash': serializer.toJson<String?>(thumbHash),
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'uploadedAt': serializer.toJson<DateTime?>(uploadedAt),
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility),
@@ -1306,6 +1357,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
@@ -1328,6 +1380,7 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1358,6 +1411,9 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
uploadedAt: data.uploadedAt.present
? data.uploadedAt.value
: this.uploadedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1387,6 +1443,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('uploadedAt: $uploadedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
@@ -1412,6 +1469,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime,
thumbHash,
deletedAt,
uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -1436,6 +1494,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt &&
other.uploadedAt == this.uploadedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility &&
other.stackId == this.stackId &&
@@ -1459,6 +1518,7 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime?> localDateTime;
final i0.Value<String?> thumbHash;
final i0.Value<DateTime?> deletedAt;
final i0.Value<DateTime?> uploadedAt;
final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility;
final i0.Value<String?> stackId;
@@ -1479,6 +1539,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(),
this.stackId = const i0.Value.absent(),
@@ -1500,6 +1561,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
this.stackId = const i0.Value.absent(),
@@ -1526,6 +1588,7 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? localDateTime,
i0.Expression<String>? thumbHash,
i0.Expression<DateTime>? deletedAt,
i0.Expression<DateTime>? uploadedAt,
i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility,
i0.Expression<String>? stackId,
@@ -1547,6 +1610,7 @@ class RemoteAssetEntityCompanion
if (localDateTime != null) 'local_date_time': localDateTime,
if (thumbHash != null) 'thumb_hash': thumbHash,
if (deletedAt != null) 'deleted_at': deletedAt,
if (uploadedAt != null) 'uploaded_at': uploadedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility,
if (stackId != null) 'stack_id': stackId,
@@ -1570,6 +1634,7 @@ class RemoteAssetEntityCompanion
i0.Value<DateTime?>? localDateTime,
i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt,
i0.Value<DateTime?>? uploadedAt,
i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility,
i0.Value<String?>? stackId,
@@ -1591,6 +1656,7 @@ class RemoteAssetEntityCompanion
localDateTime: localDateTime ?? this.localDateTime,
thumbHash: thumbHash ?? this.thumbHash,
deletedAt: deletedAt ?? this.deletedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility,
stackId: stackId ?? this.stackId,
@@ -1646,6 +1712,9 @@ class RemoteAssetEntityCompanion
if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
}
if (uploadedAt.present) {
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt.value);
}
if (livePhotoVideoId.present) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
}
@@ -1683,6 +1752,7 @@ class RemoteAssetEntityCompanion
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('uploadedAt: $uploadedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
@@ -1693,10 +1763,6 @@ class RemoteAssetEntityCompanion
}
}
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
i0.Index get uQRemoteAssetsOwnerLibraryChecksum => i0.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
@@ -1709,11 +1775,7 @@ i0.Index get idxRemoteAssetStackId => i0.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
i0.Index get idxRemoteAssetLocalDateTimeDay => i0.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
i0.Index get idxRemoteAssetLocalDateTimeMonth => i0.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
i0.Index get idxRemoteAssetOwnerVisibilityDeletedCreated => i0.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
@@ -2,15 +2,15 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:ui' as ui;
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
part 'local_image_request.dart';
part 'thumbhash_image_request.dart';
part 'remote_image_request.dart';
part 'thumbhash_image_request.dart';
abstract class ImageRequest {
static int _nextRequestId = 0;
@@ -74,7 +74,9 @@ abstract class ImageRequest {
Future<ui.FrameInfo?> _fromEncodedPlatformImage(int address, int length) async {
final result = await _codecFromEncodedPlatformImage(address, length);
if (result == null) return null;
if (result == null) {
return null;
}
final (codec, descriptor) = result;
if (_isCancelled) {
@@ -46,7 +46,9 @@ class LocalImageRequest extends ImageRequest {
isVideo: assetType == AssetType.video,
preferEncoded: true,
);
if (info == null) return null;
if (info == null) {
return null;
}
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
@@ -29,7 +29,9 @@ class RemoteImageRequest extends ImageRequest {
}
final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: true);
if (info == null) return null;
if (info == null) {
return null;
}
final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null);
return codec;
@@ -5,7 +5,9 @@ class ApiRepository {
Future<T> checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw const NoResponseDtoError();
if (response == null) {
throw const NoResponseDtoError();
}
return response;
}
}
@@ -98,7 +98,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 25;
int get schemaVersion => 26;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -266,6 +266,15 @@ class Drift extends $Drift {
},
from24To25: (m, v25) async {
await m.createTable(v25.metadata);
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
await m.createIndex(v25.idxRemoteExifCity);
await m.createIndex(v25.idxAssetFaceVisiblePerson);
},
from25To26: (m, v26) async {
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
},
),
);
@@ -113,13 +113,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
i4.idxLocalAssetChecksum,
i4.idxLocalAssetCloudId,
i3.idxStackPrimaryAssetId,
i2.idxRemoteAssetOwnerChecksum,
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
i2.idxRemoteAssetChecksum,
i2.idxRemoteAssetStackId,
i2.idxRemoteAssetLocalDateTimeDay,
i2.idxRemoteAssetLocalDateTimeMonth,
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
@@ -137,11 +135,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
metadataEntity,
i10.idxPartnerSharedWithId,
i11.idxLatLng,
i11.idxRemoteExifCity,
i12.idxRemoteAlbumAssetAlbumAsset,
i14.idxRemoteAssetCloudId,
i17.idxPersonOwnerId,
i18.idxAssetFacePersonId,
i18.idxAssetFaceAssetId,
i18.idxAssetFaceVisiblePerson,
i20.idxTrashedLocalAssetChecksum,
i20.idxTrashedLocalAssetAlbum,
i21.idxAssetEditAssetId,
+618 -14
View File
@@ -12390,13 +12390,11 @@ final class Schema25 extends i0.VersionedSchema {
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetLocalDateTimeDay,
idxRemoteAssetLocalDateTimeMonth,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
@@ -12414,11 +12412,13 @@ final class Schema25 extends i0.VersionedSchema {
metadata,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
@@ -12583,10 +12583,6 @@ final class Schema25 extends i0.VersionedSchema {
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
@@ -12603,13 +12599,9 @@ final class Schema25 extends i0.VersionedSchema {
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
@@ -12883,6 +12875,10 @@ final class Schema25 extends i0.VersionedSchema {
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
@@ -12903,6 +12899,10 @@ final class Schema25 extends i0.VersionedSchema {
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
@@ -12943,6 +12943,602 @@ i1.GeneratedColumn<String> _column_211(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL',
);
final class Schema26 extends i0.VersionedSchema {
Schema26({required super.database}) : super(version: 26);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
metadata,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 metadata = Shape49(
source: i0.VersionedTable(
entityName: 'metadata',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_211, _column_115],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
}
class Shape50 extends i0.VersionedTable {
Shape50({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationMs =>
columnsByName['duration_ms']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localDateTime =>
columnsByName['local_date_time']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get thumbHash =>
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uploadedAt =>
columnsByName['uploaded_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get livePhotoVideoId =>
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get visibility =>
columnsByName['visibility']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get stackId =>
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get libraryId =>
columnsByName['library_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isEdited =>
columnsByName['is_edited']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_212(String aliasedName) =>
i1.GeneratedColumn<String>(
'uploaded_at',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -12968,6 +13564,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -13091,6 +13688,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from24To25(migrator, schema);
return 25;
case 25:
final schema = Schema26(database: database);
final migrator = i1.Migrator(database, schema);
await from25To26(migrator, schema);
return 26;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -13122,6 +13724,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -13148,5 +13751,6 @@ i1.OnUpgrade stepByStep({
from22To23: from22To23,
from23To24: from23To24,
from24To25: from24To25,
from25To26: from25To26,
),
);
@@ -48,7 +48,9 @@ class MetadataRepository extends DriftDatabaseRepository {
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (_read(key) == value) return;
if (_read(key) == value) {
return;
}
await _db
.into(_db.metadataEntity)
@@ -79,13 +81,17 @@ class MetadataRepository extends DriftDatabaseRepository {
final keyMap = MetadataKey.asKeyMap();
for (final row in rows) {
final key = keyMap[row.key];
if (key == null) continue;
if (key == null) {
continue;
}
_updateCache(key, key.decode(row.value));
}
}
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
if (_cache[key] == value) return;
if (_cache[key] == value) {
return;
}
_cache[key] = value;
key.domain.rebuild(this);
}
@@ -103,9 +109,35 @@ extension<T extends Object> on MetadataDomain<T> {
repo._appConfig = .new(
theme: .new(
mode: repo._read(.themeMode),
primaryColor: repo._read(.primaryColor),
dynamicTheme: repo._read(.dynamicTheme),
colorfulInterface: repo._read(.colorfulInterface),
primaryColor: repo._read(.themePrimaryColor),
dynamicTheme: repo._read(.themeDynamic),
colorfulInterface: repo._read(.themeColorfulInterface),
),
cleanup: .new(
keepFavorites: repo._read(.cleanupKeepFavorites),
keepMediaType: repo._read(.cleanupKeepMediaType),
keepAlbumIds: repo._read(.cleanupKeepAlbumIds),
cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo),
defaultsInitialized: repo._read(.cleanupDefaultsInitialized),
),
map: .new(
relativeDays: repo._read(.mapRelativeDate),
favoritesOnly: repo._read(.mapShowFavoriteOnly),
includeArchived: repo._read(.mapIncludeArchived),
themeMode: repo._read(.mapThemeMode),
withPartners: repo._read(.mapWithPartners),
),
timeline: .new(
tilesPerRow: repo._read(.timelineTilesPerRow),
groupAssetsBy: repo._read(.timelineGroupAssetsBy),
storageIndicator: repo._read(.timelineStorageIndicator),
),
image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)),
viewer: .new(
loopVideo: repo._read(.viewerLoopVideo),
loadOriginalVideo: repo._read(.viewerLoadOriginalVideo),
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
tapToNavigate: repo._read(.viewerTapToNavigate),
),
);
case .systemConfig:
@@ -360,7 +360,9 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
if (albumIds.isEmpty) return [];
if (albumIds.isEmpty) {
return [];
}
final jsonIds = jsonEncode(albumIds);
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
@@ -164,6 +164,16 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
});
}
Future<void> emptyTrash(String ownerId) async {
await _db.remoteAssetEntity.deleteWhere((t) => t.deletedAt.isNotNull() & t.ownerId.equals(ownerId));
}
Future<void> restoreAllTrash(String ownerId) async {
await (_db.remoteAssetEntity.update()..where((t) => t.deletedAt.isNotNull() & t.ownerId.equals(ownerId))).write(
const RemoteAssetEntityCompanion(deletedAt: Value(null)),
);
}
Future<void> delete(List<String> ids) {
return _db.batch((batch) {
for (final id in ids) {
@@ -3,9 +3,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
@@ -38,7 +36,6 @@ class SyncApiRepository {
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
final request = http.Request('POST', Uri.parse(endpoint));
request.headers.addAll(headers);
request.body = jsonEncode(
@@ -77,7 +74,6 @@ class SyncApiRepository {
? SyncRequestType.assetFacesV2
: SyncRequestType.assetFacesV1,
],
reset: shouldReset,
).toJson(),
);
@@ -101,9 +97,6 @@ class SyncApiRepository {
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
}
// Reset after successful stream start
await Store.put(StoreKey.shouldResetSync, false);
await for (final chunk in response.stream.transform(utf8.decoder)) {
if (shouldAbort) {
break;
@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.da
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -45,25 +46,35 @@ class SyncStreamRepository extends DriftDatabaseRepository {
// foreign_keys PRAGMA is no-op within transactions
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
await _db.customStatement('PRAGMA foreign_keys = OFF');
await transaction(() async {
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.authUserEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
});
await _db.customStatement('PRAGMA foreign_keys = ON');
try {
await transaction(() async {
// FK cascade (ON DELETE SET NULL) does not fire while foreign_keys = OFF,
// so null linkedRemoteAlbumId manually to avoid dangling pointers in local_album_entity.
await _db.localAlbumEntity.update().write(
const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)),
);
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.authUserEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
});
} finally {
// re-enable FK even if the transaction throws, otherwise the connection
// would be left with foreign_keys = OFF, silently disabling cascades.
await _db.customStatement('PRAGMA foreign_keys = ON');
}
});
} catch (error, stack) {
_logger.severe('Error: SyncResetV1', error, stack);
@@ -191,6 +202,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
uploadedAt: Value(asset.createdAt),
durationMs: Value(asset.duration?.toDuration()?.inMilliseconds ?? 0),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
@@ -229,6 +241,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
uploadedAt: Value(asset.createdAt),
durationMs: Value(asset.duration),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
@@ -82,6 +82,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
uploadedAt: row.uploadedAt,
thumbHash: row.thumbHash,
width: row.width,
height: row.height,
@@ -247,17 +248,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
// Correlated subquery picks the first matching local asset by checksum,
// avoiding fan-out when the same photo exists in multiple device albums (#23273).
final localId = subqueryExpression<String>(
_db.localAssetEntity.selectOnly()
..addColumns([_db.localAssetEntity.id])
..where(_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum))
..limit(1),
);
final query = _db.remoteAssetEntity.select().addColumns([localId]).join([
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId));
if (isAscending) {
@@ -268,9 +273,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
query.limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(localId))).get();
}
TimelineQuery fromAssets(List<BaseAsset> assets, TimelineOrigin origin) => (
@@ -318,6 +321,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
origin: TimelineOrigin.remoteAssets,
);
TimelineQuery recentlyAdded(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.uploadedAt.isNotNull() &
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
(row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
origin: TimelineOrigin.recentlyAdded,
groupBy: groupBy,
sortBy: SortAssetsBy.uploaded,
);
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
@@ -425,23 +439,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}
Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
final idQuery = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId])
..where(
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
);
if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id.count()])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline),
);
return query.map((row) {
@@ -455,20 +468,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.id.isInQuery(idQuery) &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
_db.remoteAssetEntity.deletedAt.isNull(),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
@@ -486,26 +490,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required int offset,
required int count,
}) {
final query =
_db.remoteAssetEntity.select().join([
innerJoin(
_db.assetFaceEntity,
_db.assetFaceEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
final idQuery = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId])
..where(
_db.assetFaceEntity.personId.equals(personId) &
_db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(),
);
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.id.isInQuery(idQuery) &
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
@@ -634,9 +638,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required TimelineOrigin origin,
GroupAssetsBy groupBy = GroupAssetsBy.day,
bool joinLocal = false,
SortAssetsBy sortBy = SortAssetsBy.taken,
}) {
return (
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
assetSource: (offset, count) =>
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
origin: origin,
@@ -646,6 +651,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
Stream<List<Bucket>> _watchRemoteBucket({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day,
SortAssetsBy sortBy = SortAssetsBy.taken,
}) {
if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.count(where: filter);
@@ -653,7 +659,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy);
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy, sortBy: sortBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
@@ -729,8 +735,13 @@ extension on Expression<DateTime> {
}
extension on $RemoteAssetEntityTable {
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy) =>
coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy, {SortAssetsBy sortBy = SortAssetsBy.taken}) {
if (sortBy == SortAssetsBy.uploaded) {
return uploadedAt.dateFmt(groupBy, toLocal: true);
}
return coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
}
}
extension on String {
@@ -12,7 +12,9 @@ class DriftAuthUserRepository extends DriftDatabaseRepository {
Future<UserDto?> get(String id) async {
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
if (user == null) return null;
if (user == null) {
return null;
}
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(id));
final metadata = await query.map((row) => row.toDto()).get();
@@ -12,7 +12,9 @@ class UserApiRepository extends ApiRepository {
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) = await (_api.getMyUser(), _api.getMyPreferences()).wait;
if (adminDto == null) return null;
if (adminDto == null) {
return null;
}
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
+29 -8
View File
@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/constants/locales.dart';
@@ -163,6 +164,13 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
}
}
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
await FlutterLocalNotificationsPlugin().initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@drawable/notification_icon'),
iOS: DarwinInitializationSettings(),
),
);
}
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
@@ -171,19 +179,32 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
PageRouteInfo? route;
if (deepLink.uri.scheme == "immich") {
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart);
return proposedRoute;
route = await deepLinkHandler.handleScheme(deepLink, ref);
} else if (deepLink.uri.host == "my.immich.app") {
route = await deepLinkHandler.handleMyImmichApp(deepLink, ref);
} else {
return DeepLink.path(deepLink.path);
}
if (deepLink.uri.host == "my.immich.app") {
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart);
return proposedRoute;
if (route == null) {
return isColdStart ? DeepLink.defaultPath : DeepLink.none;
}
return DeepLink.path(deepLink.path);
// We need to replace the route if the destination is the current route
if (!isColdStart) {
unawaited(
ref.read(appRouterProvider).pushAndPopUntil(route, predicate: (r) => r.settings.name != route!.routeName),
);
return DeepLink.none;
}
return DeepLink([
// we need something to segue back to if the app was cold started
if (isColdStart) const TabShellRoute(children: [MainTimelineRoute()]),
route,
]);
}
@override
@@ -44,7 +44,9 @@ class Activity {
@override
bool operator ==(covariant Activity other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.id == id &&
other.assetId == assetId &&
+3 -1
View File
@@ -44,7 +44,9 @@ class AuthState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is AuthState &&
other.deviceId == deviceId &&
@@ -16,7 +16,9 @@ class AuxilaryEndpoint {
@override
bool operator ==(covariant AuxilaryEndpoint other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.url == url && other.status == status;
}
@@ -53,7 +55,9 @@ class AuxCheckStatus {
@override
bool operator ==(covariant AuxCheckStatus other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.name == name;
}
@@ -19,7 +19,9 @@ class BiometricStatus {
@override
bool operator ==(covariant BiometricStatus other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.availableBiometrics, availableBiometrics) && other.canAuthenticate == canAuthenticate;
@@ -67,7 +67,9 @@ class CastManagerState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is CastManagerState &&
other.isCasting == isCasting &&
@@ -41,7 +41,9 @@ class DownloadInfo {
@override
bool operator ==(covariant DownloadInfo other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.fileName == fileName && other.progress == progress && other.status == status;
}
@@ -71,7 +73,9 @@ class DownloadState {
@override
bool operator ==(covariant DownloadState other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
final mapEquals = const DeepCollectionEquality().equals;
return other.downloadStatus == downloadStatus &&
@@ -32,7 +32,9 @@ class LivePhotosMetadata {
@override
bool operator ==(covariant LivePhotosMetadata other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.part == part && other.id == id;
}
+3 -1
View File
@@ -17,7 +17,9 @@ class MapMarker {
@override
bool operator ==(covariant MapMarker other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.latLng == latLng && other.assetRemoteId == assetRemoteId;
}
+3 -1
View File
@@ -51,7 +51,9 @@ class MapState {
@override
bool operator ==(covariant MapState other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.themeMode == themeMode &&
other.showFavoriteOnly == showFavoriteOnly &&
@@ -42,7 +42,9 @@ class SearchCuratedContent {
@override
bool operator ==(covariant SearchCuratedContent other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.label == label && other.subtitle == subtitle && other.id == id;
}
@@ -36,7 +36,9 @@ class SearchLocationFilter {
@override
bool operator ==(covariant SearchLocationFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.country == country && other.state == state && other.city == city;
}
@@ -75,7 +77,9 @@ class SearchCameraFilter {
@override
bool operator ==(covariant SearchCameraFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.make == make && other.model == model;
}
@@ -117,7 +121,9 @@ class SearchDateFilter {
@override
bool operator ==(covariant SearchDateFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.takenBefore == takenBefore && other.takenAfter == takenAfter;
}
@@ -152,7 +158,9 @@ class SearchRatingFilter {
@override
bool operator ==(covariant SearchRatingFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.rating == rating;
}
@@ -198,7 +206,9 @@ class SearchDisplayFilters {
@override
bool operator ==(covariant SearchDisplayFilters other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.isNotInAlbum == isNotInAlbum && other.isArchive == isArchive && other.isFavorite == isFavorite;
}
@@ -305,7 +315,9 @@ class SearchFilter {
@override
bool operator ==(covariant SearchFilter other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.context == context &&
other.filename == filename &&
@@ -38,7 +38,9 @@ class ServerConfig {
@override
bool operator ==(covariant ServerConfig other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.trashDays == trashDays &&
other.oauthButtonText == oauthButtonText &&
@@ -35,7 +35,9 @@ class ServerDiskInfo {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is ServerDiskInfo &&
other.diskAvailable == diskAvailable &&
@@ -50,7 +50,9 @@ class ServerFeatures {
@override
bool operator ==(covariant ServerFeatures other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.trash == trash &&
other.map == map &&
@@ -60,7 +60,9 @@ class ServerInfo {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is ServerInfo &&
other.serverVersion == serverVersion &&
@@ -88,7 +88,9 @@ class ShareIntentAttachment {
@override
bool operator ==(covariant ShareIntentAttachment other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.path == path && other.type == type;
}
@@ -418,7 +418,9 @@ class _PreparingStatusState extends ConsumerState {
}
void _startPollingIfNeeded() {
if (_pollingTimer != null) return;
if (_pollingTimer != null) {
return;
}
_pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async {
final currentUser = ref.read(currentUserProvider);
@@ -83,7 +83,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
final albumCount = albums.length;
// Filter albums based on search query
final filteredAlbums = albums.where((album) {
if (_searchQuery.isEmpty) return true;
if (_searchQuery.isEmpty) {
return true;
}
return album.name.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
@@ -40,7 +40,9 @@ class _DriftUploadDetailPageState extends ConsumerState<DriftUploadDetailPage> {
}
for (final item in uploadingItems) {
if (_taskSlotAssignments.containsKey(item.taskId)) continue;
if (_taskSlotAssignments.containsKey(item.taskId)) {
continue;
}
for (int i = 0; i < _maxSlots; i++) {
if (slots[i] == null) {
@@ -93,7 +93,9 @@ class HeaderSettingsPage extends HookConsumerWidget {
final key = header.key.trim();
final value = header.value.trim();
if (key.isEmpty || value.isEmpty) continue;
if (key.isEmpty || value.isEmpty) {
continue;
}
headersMap[key] = value;
}
@@ -35,7 +35,7 @@ class BootstrapErrorWidget extends StatelessWidget {
@override
Widget build(BuildContext _) {
final immichTheme = MetadataKey.primaryColor.defaultValue.themeOfPreset;
final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset;
return EasyLocalization(
supportedLocales: locales.values.toList(),
@@ -31,7 +31,9 @@ RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder t
if (folder.subfolders.isNotEmpty) {
final found = _findFolderInStructure(folder, targetFolder);
if (found != null) return found;
if (found != null) {
return found;
}
}
}
return null;
@@ -113,7 +115,9 @@ class FolderContent extends HookConsumerWidget {
// Initial asset fetch
useEffect(() {
if (folder == null) return;
if (folder == null) {
return;
}
ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder);
return null;
}, [folder]);
@@ -20,7 +20,9 @@ class SharedLinkPage extends HookConsumerWidget {
useEffect(() {
ref.read(sharedLinksStateProvider.notifier).fetchLinks();
return () {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
ref.invalidate(sharedLinksStateProvider);
};
}, []);
+4 -2
View File
@@ -277,7 +277,7 @@ abstract class BackgroundWorkerFlutterApi {
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
Future<void> onAndroidUpload();
Future<void> onAndroidUpload(int? maxMinutes);
Future<void> cancel();
@@ -323,8 +323,10 @@ abstract class BackgroundWorkerFlutterApi {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int? arg_maxMinutes = args[0] as int?;
try {
await api.onAndroidUpload();
await api.onAndroidUpload(arg_maxMinutes);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
@@ -191,8 +191,12 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
}
String _getAssetTypeTitle(BaseAsset asset) {
if (asset is LocalAsset) return 'Local Asset';
if (asset is RemoteAsset) return 'Remote Asset';
if (asset is LocalAsset) {
return 'Local Asset';
}
if (asset is RemoteAsset) {
return 'Remote Asset';
}
return 'Base Asset';
}
}
@@ -0,0 +1,32 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage()
class DriftRecentlyAddedPage extends StatelessWidget {
const DriftRecentlyAddedPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access recently taken');
}
final timelineService = ref.watch(timelineFactoryProvider).recentlyAdded(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_added'.t())),
);
}
}
@@ -245,7 +245,9 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
}
Future<void> _handleSave() async {
if (formKey.currentState?.validate() != true) return;
if (formKey.currentState?.validate() != true) {
return;
}
try {
final newTitle = titleController.text.trim();
@@ -1,13 +1,18 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@RoutePage()
class DriftTrashPage extends StatelessWidget {
@@ -36,6 +41,7 @@ class DriftTrashPage extends StatelessWidget {
pinned: true,
centerTitle: true,
elevation: 0,
actions: [const _TrashKebabMenu()],
),
topSliverWidgetHeight: 24,
topSliverWidget: Consumer(
@@ -53,3 +59,89 @@ class DriftTrashPage extends StatelessWidget {
);
}
}
class _TrashKebabMenu extends ConsumerWidget {
const _TrashKebabMenu();
Future<void> _confirmAndRun(
BuildContext context,
WidgetRef ref, {
required String title,
required String content,
required Future<ActionResult> Function(String userId) action,
required String Function(int count) successMsg,
}) async {
await showDialog<bool>(
context: context,
builder: (_) => ConfirmDialog(
title: title,
content: content,
onOk: () async {
final user = ref.read(currentUserProvider);
if (user == null) {
return;
}
final result = await action(user.id);
if (!context.mounted) {
return;
}
ImmichToast.show(
context: context,
msg: result.success ? successMsg(result.count) : context.t.scaffold_body_error_occurred,
toastType: result.success ? ToastType.success : ToastType.error,
);
},
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
),
menuChildren: [
BaseActionButton(
label: context.t.empty_trash,
iconData: Icons.delete_forever_outlined,
onPressed: () => _confirmAndRun(
context,
ref,
title: context.t.empty_trash,
content: context.t.empty_trash_confirmation,
action: ref.read(actionProvider.notifier).emptyTrash,
successMsg: (count) => context.t.assets_permanently_deleted_count(count: count),
),
menuItem: true,
),
BaseActionButton(
label: context.t.restore_all,
iconData: Icons.restore_outlined,
onPressed: () => _confirmAndRun(
context,
ref,
title: context.t.restore_all,
content: context.t.assets_restore_confirmation,
action: ref.read(actionProvider.notifier).restoreAllTrash,
successMsg: (count) => context.t.assets_restored_count(count: count),
),
menuItem: true,
),
],
builder: (context, controller, child) {
return IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
);
},
);
}
}
@@ -95,7 +95,9 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
return PopScope(
canPop: !hasUnsavedEdits,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (didPop) {
return;
}
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
if (shouldDiscard && mounted) {
Navigator.of(context).pop();
@@ -179,7 +179,9 @@ class EditorState {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is EditorState &&
other.isApplyingEdits == isApplyingEdits &&
@@ -58,7 +58,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
}
Future<void> _handleDone() async {
if (_isLoading) return;
if (_isLoading) {
return;
}
setState(() {
_isLoading = true;
@@ -72,7 +74,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
.read(uploadProfileImageProvider.notifier)
.upload(xFile, fileName: 'profile-picture.png');
if (!context.mounted) return;
if (!context.mounted) {
return;
}
if (success) {
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
@@ -102,7 +106,9 @@ class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage>
);
}
} catch (e) {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
ImmichToast.show(
context: context,
@@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
@@ -708,7 +709,9 @@ class _SearchResultGrid extends ConsumerWidget {
bool _onScrollUpdateNotification(ScrollNotification notification) {
final metrics = notification.metrics;
if (metrics.axis != Axis.vertical) return false;
if (metrics.axis != Axis.vertical) {
return false;
}
final isBottomSheet = notification.context?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() != null;
final remaining = metrics.maxScrollExtent - metrics.pixels;
@@ -735,7 +738,9 @@ class _SearchResultGrid extends ConsumerWidget {
final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null));
if (hasMore) return null;
if (hasMore) {
return null;
}
return SliverToBoxAdapter(
child: Padding(
@@ -875,6 +880,12 @@ class _QuickLinkList extends StatelessWidget {
isTop: true,
onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()),
),
_QuickLink(
title: context.t.recently_added,
icon: Icons.upload_outlined,
isTop: true,
onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()),
),
_QuickLink(
title: 'videos'.t(context: context),
icon: Icons.play_circle_outline_rounded,
@@ -44,7 +44,9 @@ class PaginatedSearchNotifier extends StateNotifier<SearchState> {
Stream<int> get assetCount => _assetCountController.stream;
Future<void> search(SearchFilter filter) async {
if (state.nextPage == null || state.isLoading) return;
if (state.nextPage == null || state.isLoading) {
return;
}
state = SearchState(assets: state.assets, nextPage: state.nextPage, isLoading: true);

Some files were not shown because too many files have changed in this diff Show More