From 01edf6533bc630926345f7a353356e7bd7c75ad5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 Aug 2025 10:46:40 -0500 Subject: [PATCH 01/31] fix: shared album asset count query (#21157) --- .../repositories/remote_album.repository.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 41ce131871..44a288787e 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -17,7 +17,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { const DriftRemoteAlbumRepository(this._db) : super(_db); Future> getAll({Set sortBy = const {SortRemoteAlbumsBy.updatedAt}}) { - final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true); final query = _db.remoteAlbumEntity.select().join([ leftOuterJoin( @@ -41,7 +41,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ..where(_db.remoteAssetEntity.deletedAt.isNull()) ..addColumns([assetCount]) ..addColumns([_db.userEntity.name]) - ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)]) ..groupBy([_db.remoteAlbumEntity.id]); if (sortBy.isNotEmpty) { @@ -62,14 +62,14 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .toDto( assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, ), ) .get(); } Future get(String albumId) { - final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); + final assetCount = _db.remoteAlbumAssetEntity.assetId.count(distinct: true); final query = _db.remoteAlbumEntity.select().join([ @@ -97,7 +97,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull()) ..addColumns([assetCount]) ..addColumns([_db.userEntity.name]) - ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)]) ..groupBy([_db.remoteAlbumEntity.id]); return query @@ -107,7 +107,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .toDto( assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, ), ) .getSingleOrNull(); @@ -282,7 +282,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ]) ..where(_db.remoteAlbumEntity.id.equals(albumId)) ..addColumns([_db.userEntity.name]) - ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)]) ..groupBy([_db.remoteAlbumEntity.id]); return query.map((row) { @@ -290,7 +290,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .readTable(_db.remoteAlbumEntity) .toDto( ownerName: row.read(_db.userEntity.name)!, - isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2, ); return album; }).watchSingleOrNull(); From 13c8a6e61de5a28bdcd369cc3dd223853267b796 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 Aug 2025 11:02:24 -0500 Subject: [PATCH 02/31] fix: parse correct metadata to userDto for SQlite store implmentation (#21154) --- .../domain/models/user_metadata.model.dart | 19 ++++---- .../repositories/user.repository.dart | 43 +++++++++++++++++-- .../user_metadata.repository.dart | 2 +- .../pages/dev/main_timeline.page.dart | 3 -- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index 1c371a9d3e..c477be1a41 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -74,7 +74,6 @@ isOnboarded: $isOnboarded, int get hashCode => isOnboarded.hashCode; } -// TODO: wait to be overwritten class Preferences { final bool foldersEnabled; final bool memoriesEnabled; @@ -133,17 +132,17 @@ class Preferences { factory Preferences.fromMap(Map map) { return Preferences( - foldersEnabled: map["folders-Enabled"] as bool? ?? false, - memoriesEnabled: map["memories-Enabled"] as bool? ?? true, - peopleEnabled: map["people-Enabled"] as bool? ?? true, - ratingsEnabled: map["ratings-Enabled"] as bool? ?? false, - sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true, - tagsEnabled: map["tags-Enabled"] as bool? ?? false, + foldersEnabled: (map["folders"] as Map?)?["enabled"] as bool? ?? false, + memoriesEnabled: (map["memories"] as Map?)?["enabled"] as bool? ?? true, + peopleEnabled: (map["people"] as Map?)?["enabled"] as bool? ?? true, + ratingsEnabled: (map["ratings"] as Map?)?["enabled"] as bool? ?? false, + sharedLinksEnabled: (map["sharedLinks"] as Map?)?["enabled"] as bool? ?? true, + tagsEnabled: (map["tags"] as Map?)?["enabled"] as bool? ?? false, userAvatarColor: AvatarColor.values.firstWhere( - (e) => e.value == map["avatar-Color"] as String?, + (e) => e.value == (map["avatar"] as Map?)?["color"] as String?, orElse: () => AvatarColor.primary, ), - showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true, + showSupportBadge: (map["purchase"] as Map?)?["showSupportBadge"] as bool? ?? true, ); } @@ -213,7 +212,7 @@ class License { factory License.fromMap(Map map) { return License( - activatedAt: map["activatedAt"] as DateTime, + activatedAt: DateTime.parse(map["activatedAt"] as String), activationKey: map["activationKey"] as String, licenseKey: map["licenseKey"] as String, ); diff --git a/mobile/lib/infrastructure/repositories/user.repository.dart b/mobile/lib/infrastructure/repositories/user.repository.dart index 1caab462cd..3081aee1a9 100644 --- a/mobile/lib/infrastructure/repositories/user.repository.dart +++ b/mobile/lib/infrastructure/repositories/user.repository.dart @@ -1,9 +1,11 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; import 'package:isar/isar.dart'; class IsarUserRepository extends IsarDatabaseRepository { @@ -70,8 +72,16 @@ class DriftUserRepository extends DriftDatabaseRepository { final Drift _db; const DriftUserRepository(super.db) : _db = db; - Future get(String id) => - _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto()); + Future get(String id) async { + final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull(); + + 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(); + + return user.toDto(metadata); + } Future upsert(UserDto user) async { await _db.userEntity.insertOnConflictUpdate( @@ -87,10 +97,35 @@ class DriftUserRepository extends DriftDatabaseRepository { ); return user; } + + Future> getAll() async { + final users = await _db.userEntity.select().get(); + final List result = []; + + for (final user in users) { + final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id)); + final metadata = await query.map((row) => row.toDto()).get(); + result.add(user.toDto(metadata)); + } + + return result; + } } extension on UserEntityData { - UserDto toDto() { + UserDto toDto([List? metadata]) { + AvatarColor avatarColor = AvatarColor.primary; + bool memoryEnabled = true; + + if (metadata != null) { + for (final meta in metadata) { + if (meta.key == UserMetadataKey.preferences && meta.preferences != null) { + avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary; + memoryEnabled = meta.preferences?.memoriesEnabled ?? true; + } + } + } + return UserDto( id: id, email: email, @@ -99,6 +134,8 @@ extension on UserEntityData { updatedAt: updatedAt, profileChangedAt: profileChangedAt, hasProfileImage: hasProfileImage, + avatarColor: avatarColor, + memoryEnabled: memoryEnabled, ); } } diff --git a/mobile/lib/infrastructure/repositories/user_metadata.repository.dart b/mobile/lib/infrastructure/repositories/user_metadata.repository.dart index 7205c7f73a..173ec10b97 100644 --- a/mobile/lib/infrastructure/repositories/user_metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/user_metadata.repository.dart @@ -16,7 +16,7 @@ class DriftUserMetadataRepository extends DriftDatabaseRepository { } } -extension on UserMetadataEntityData { +extension UserMetadataDataExtension on UserMetadataEntityData { UserMetadata toDto() => switch (key) { UserMetadataKey.onboarding => UserMetadata(userId: userId, key: key, onboarding: Onboarding.fromMap(value)), UserMetadataKey.preferences => UserMetadata(userId: userId, key: key, preferences: Preferences.fromMap(value)), diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 3764443566..8ef3ef9757 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -15,9 +15,6 @@ class MainTimelinePage extends ConsumerWidget { final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true)); - // TODO: the user preferences need to be updated - // from the server to get live hiding/showing of memory lane - return memoryLaneProvider.maybeWhen( data: (memories) { return memories.isEmpty || !memoriesEnabled From bedaa729e9809aaa194afceb3e791615b1cadb93 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 Aug 2025 11:06:13 -0500 Subject: [PATCH 03/31] chore: post release tasks (#21140) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 20 +++++++++++--------- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index cbd76a0bf9..b52c045a81 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -123,6 +123,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -667,7 +669,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -811,7 +813,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -841,7 +843,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -875,7 +877,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -918,7 +920,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -958,7 +960,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -997,7 +999,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1041,7 +1043,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1082,7 +1084,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 216; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 31ad10c35f..cf2bebd0b8 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.138.1 + 1.139.2 CFBundleSignature ???? CFBundleURLTypes @@ -105,7 +105,7 @@ CFBundleVersion - 215 + 216 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 801af34d9ad33a39be2d22d90e0217b2cd55c43c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 Aug 2025 11:09:00 -0500 Subject: [PATCH 04/31] fix: sync flow block oAuth login page navigation (#21187) --- mobile/lib/widgets/forms/login/login_form.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 0742bb95c3..ab78536a92 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/version_compatibility.dart'; @@ -277,7 +276,6 @@ class LoginForm extends HookConsumerWidget { } if (isBeta) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - await runNewSync(ref); context.replaceRoute(const TabShellRoute()); return; } From 03e79225896525670d87d23777dec277972ae0a9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 23 Aug 2025 12:09:36 -0400 Subject: [PATCH 05/31] fix: local offset hours (#21147) --- server/src/queries/asset.repository.sql | 2 +- server/src/repositories/asset.repository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 712fb08a50..e2bc80eabe 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -277,7 +277,7 @@ with epoch from ( - asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC' + asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC' ) )::real / 3600 as "localOffsetHours", "asset"."ownerId", diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 61ccbf6541..6752d7bf62 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -566,7 +566,7 @@ export class AssetRepository { sql`asset.type = 'IMAGE'`.as('isImage'), sql`asset."deletedAt" is not null`.as('isTrashed'), 'asset.livePhotoVideoId', - sql`extract(epoch from (asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as( + sql`extract(epoch from (asset."localDateTime" AT TIME ZONE 'UTC' - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as( 'localOffsetHours', ), 'asset.ownerId', From 2be1a58c5b8391266be20f9aab7ad9592ea6d50f Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 23 Aug 2025 21:48:57 +0530 Subject: [PATCH 06/31] fix: prefer local video if available (#21119) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- mobile/lib/domain/services/asset.service.dart | 7 ++++++- .../repositories/local_asset.repository.dart | 8 ++++++-- .../repositories/remote_asset.repository.dart | 18 ------------------ .../asset_viewer/video_viewer.widget.dart | 13 +++++++------ 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index c8cc61314e..df34a41e54 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -17,9 +17,14 @@ class AssetService { _localAssetRepository = localAssetRepository, _platform = const LocalPlatform(); + Future getAsset(BaseAsset asset) { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; + return asset is LocalAsset ? _localAssetRepository.get(id) : _remoteAssetRepository.get(id); + } + Stream watchAsset(BaseAsset asset) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; - return asset is LocalAsset ? _localAssetRepository.watchAsset(id) : _remoteAssetRepository.watchAsset(id); + return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id); } Future getRemoteAsset(String id) { diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 58adac30db..5865447064 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -9,7 +9,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { final Drift _db; const DriftLocalAssetRepository(this._db) : super(_db); - Stream watchAsset(String id) { + SingleOrNullSelectable _assetSelectable(String id) { final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ leftOuterJoin( _db.remoteAssetEntity, @@ -21,9 +21,13 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { return query.map((row) { final asset = row.readTable(_db.localAssetEntity).toDto(); return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id)); - }).watchSingleOrNull(); + }); } + Future get(String id) => _assetSelectable(id).getSingleOrNull(); + + Stream watch(String id) => _assetSelectable(id).watchSingleOrNull(); + Future updateHashes(Iterable hashes) { if (hashes.isEmpty) { return Future.value(); diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 44d7cfb6bb..3ed7dddfe8 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -55,24 +55,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return _assetSelectable(id).getSingleOrNull(); } - Stream watchAsset(String id) { - final query = - _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([ - leftOuterJoin( - _db.localAssetEntity, - _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), - useColumns: false, - ), - ]) - ..where(_db.remoteAssetEntity.id.equals(id)) - ..limit(1); - - return query.map((row) { - final asset = row.readTable(_db.remoteAssetEntity).toDto(); - return asset.copyWith(localId: row.read(_db.localAssetEntity.id)); - }).watchSingleOrNull(); - } - Future> getStackChildren(RemoteAsset asset) { if (asset.stackId == null) { return Future.value([]); diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 32510c2ca5..fa7f204596 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -87,9 +87,10 @@ class NativeVideoViewer extends HookConsumerWidget { return null; } + final videoAsset = await ref.read(assetServiceProvider).getAsset(asset) ?? asset; try { - if (asset.hasLocal && asset.livePhotoVideoId == null) { - final id = asset is LocalAsset ? (asset as LocalAsset).id : (asset as RemoteAsset).localId!; + if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) { + final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!; final file = await const StorageRepository().getFileForAsset(id); if (file == null) { throw Exception('No file found for the video'); @@ -99,14 +100,14 @@ class NativeVideoViewer extends HookConsumerWidget { return source; } - final remoteId = (asset as RemoteAsset).id; + final remoteId = (videoAsset as RemoteAsset).id; // Use a network URL for the video player controller final serverEndpoint = Store.get(StoreKey.serverEndpoint); final isOriginalVideo = ref.read(settingsProvider).get(Setting.loadOriginalVideo); final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; - final String videoUrl = asset.livePhotoVideoId != null - ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' + final String videoUrl = videoAsset.livePhotoVideoId != null + ? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl' : '$serverEndpoint/assets/$remoteId/$postfixUrl'; final source = await VideoSource.init( @@ -116,7 +117,7 @@ class NativeVideoViewer extends HookConsumerWidget { ); return source; } catch (error) { - log.severe('Error creating video source for asset ${asset.name}: $error'); + log.severe('Error creating video source for asset ${videoAsset.name}: $error'); return null; } } From 1d33ed6beda5010f5c44fe3948c96104381e4665 Mon Sep 17 00:00:00 2001 From: pojlFDlxCOvZ4Kg8y1l4 <42654642+pojlFDlxCOvZ4Kg8y1l4@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:30:41 +0200 Subject: [PATCH 07/31] docs: update oauth.md - Authentik link leads to Page Not Found error (#21186) Update oauth.md Updated Authentik link --- docs/docs/administration/oauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 7450ae1b08..4c8bd33bee 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -10,7 +10,7 @@ Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobil Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including: -- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect) +- [Authentik](https://integrations.goauthentik.io/media/immich/) - [Authelia](https://www.authelia.com/integration/openid-connect/immich/) - [Okta](https://www.okta.com/openid-connect/) - [Google](https://developers.google.com/identity/openid-connect/openid-connect) From f8b41ea8aa64e92d2ab771d34c5152c3e37b83a3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 16:37:46 +0000 Subject: [PATCH 08/31] chore: version v1.139.3 --- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package.json | 2 +- web/package.json | 2 +- 12 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cli/package.json b/cli/package.json index bfcae8bb8a..a297f3a09a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.82", + "version": "2.2.83", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 5d51f454a1..b68d266b0b 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.139.3", + "url": "https://v1.139.3.archive.immich.app" + }, { "label": "v1.139.2", "url": "https://v1.139.2.archive.immich.app" diff --git a/e2e/package.json b/e2e/package.json index db1e7237a6..f8b4ace9a3 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.139.2", + "version": "1.139.3", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 21431e33ac..f0b5608a35 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3007, - "android.injected.version.name" => "1.139.2", + "android.injected.version.code" => 3008, + "android.injected.version.name" => "1.139.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d5841b5da2..f55afe2446 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :ios do path: "./Runner.xcodeproj", ) increment_version_number( - version_number: "1.139.2" + version_number: "1.139.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 9039ad6400..5df36b56f7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.139.2 +- API version: 1.139.3 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a069751921..731c26d5b1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.139.2+3007 +version: 1.139.3+3008 environment: sdk: '>=3.8.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 36099c10f7..f00f9082e2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9592,7 +9592,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.139.2", + "version": "1.139.3", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a840986448..19a3a6bfda 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.139.2", + "version": "1.139.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7d319e2fcc..5cd59c9d3c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.139.2 + * 1.139.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package.json b/server/package.json index b377d1de28..edcb45abdc 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.139.2", + "version": "1.139.3", "description": "", "author": "", "private": true, diff --git a/web/package.json b/web/package.json index 64aa03b378..ef9e08b4db 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.139.2", + "version": "1.139.3", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From 3138048b96c6f8094bdd50357579c83ea322763d Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 Aug 2025 15:25:12 -0500 Subject: [PATCH 09/31] fix: cannot load thumbnail from unknown content length (#21192) * fix: cannot load thumbnail from unknown content length * pr feedback * pr feedback --- .../loaders/remote_image_request.dart | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index fe62469461..f228f5de17 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -64,18 +64,48 @@ class RemoteImageRequest extends ImageRequest { if (_isCancelled) { return null; } - final bytes = Uint8List(response.contentLength); + + // Handle unknown content length from reverse proxy + final contentLength = response.contentLength; + final Uint8List bytes; int offset = 0; - final subscription = response.listen((List chunk) { - // this is important to break the response stream if the request is cancelled - if (_isCancelled) { - throw StateError('Cancelled request'); + + if (contentLength >= 0) { + // Known content length - use pre-allocated buffer + bytes = Uint8List(contentLength); + final subscription = response.listen((List chunk) { + // this is important to break the response stream if the request is cancelled + if (_isCancelled) { + throw StateError('Cancelled request'); + } + bytes.setAll(offset, chunk); + offset += chunk.length; + }, cancelOnError: true); + cacheManager?.putStreamedFile(url, response); + await subscription.asFuture(); + } else { + // Unknown content length - collect chunks dynamically + final chunks = >[]; + int totalLength = 0; + final subscription = response.listen((List chunk) { + // this is important to break the response stream if the request is cancelled + if (_isCancelled) { + throw StateError('Cancelled request'); + } + chunks.add(chunk); + totalLength += chunk.length; + }, cancelOnError: true); + cacheManager?.putStreamedFile(url, response); + await subscription.asFuture(); + + // Combine all chunks into a single buffer + bytes = Uint8List(totalLength); + for (final chunk in chunks) { + bytes.setAll(offset, chunk); + offset += chunk.length; } - bytes.setAll(offset, chunk); - offset += chunk.length; - }, cancelOnError: true); - cacheManager?.putStreamedFile(url, response); - await subscription.asFuture(); + } + return await ImmutableBuffer.fromUint8List(bytes); } From 3bfa8b7575ab1d81493f7a00f5122dc73c826bfe Mon Sep 17 00:00:00 2001 From: Nicholas <30300649+NicholasFlamy@users.noreply.github.com> Date: Sat, 23 Aug 2025 16:28:00 -0400 Subject: [PATCH 10/31] fix: border around dark theme button on onboarding page (#20846) fix border around dark theme button --- web/src/lib/components/onboarding-page/onboarding-theme.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index 26e8fd9c7a..957cd8093a 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -24,7 +24,7 @@