Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.5.1",
"version": "2.5.2",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions docs/static/archived-versions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"label": "v2.5.1",
"url": "https://docs.v2.5.1.archive.immich.app"
"label": "v2.5.2",
"url": "https://docs.v2.5.2.archive.immich.app"
},
{
"label": "v2.4.1",
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.5.1",
"version": "2.5.2",
"description": "",
"main": "index.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion i18n/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.5.1",
"version": "2.5.2",
"private": true,
"scripts": {
"format": "prettier --check .",
Expand Down
2 changes: 1 addition & 1 deletion machine-learning/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.5.1"
version = "2.5.2"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"
Expand Down
2 changes: 1 addition & 1 deletion machine-learning/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions mobile/android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3032,
"android.injected.version.name" => "2.5.1",
"android.injected.version.code" => 3033,
"android.injected.version.name" => "2.5.2",
}
)
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')
Expand Down
1 change: 1 addition & 0 deletions mobile/ios/Runner/Images/RemoteImagesImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
kCGImageSourceShouldCache: false,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceCreateThumbnailFromImageAlways: true
] as CFDictionary

func urlSession(
Expand Down
2 changes: 1 addition & 1 deletion mobile/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.1</string>
<string>2.5.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand Down
4 changes: 3 additions & 1 deletion mobile/lib/domain/models/store.model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ enum StoreKey<T> {
cleanupKeepMediaType<int>._(1009),
cleanupKeepAlbumIds<String>._(1010),
cleanupCutoffDaysAgo<int>._(1011),
cleanupDefaultsInitialized<bool>._(1012);
cleanupDefaultsInitialized<bool>._(1012),

syncMigrationStatus<String>._(1013);

const StoreKey._(this.id);
final int id;
Expand Down
83 changes: 83 additions & 0 deletions mobile/lib/domain/services/sync_stream.service.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// ignore_for_file: constant_identifier_names

import 'dart:async';
import 'dart:convert';

import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
Expand All @@ -7,12 +10,21 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';

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.
}

class SyncStreamService {
final Logger _logger = Logger('SyncStreamService');

Expand All @@ -22,6 +34,8 @@ class SyncStreamService {
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository;
final SyncMigrationRepository _syncMigrationRepository;
final ApiService _api;
final bool Function()? _cancelChecker;

SyncStreamService({
Expand All @@ -31,29 +45,98 @@ class SyncStreamService {
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
required SyncMigrationRepository syncMigrationRepository,
required ApiService api,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
_syncMigrationRepository = syncMigrationRepository,
_api = api,
_cancelChecker = cancelChecker;

bool get isCancelled => _cancelChecker?.call() ?? false;

Future<bool> sync() async {
_logger.info("Remote sync request for user");
final serverVersion = await _api.serverInfoApi.getServerVersion();
if (serverVersion == null) {
_logger.severe("Cannot perform sync: unable to determine server version");
return false;
}

final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);

final value = Store.get(StoreKey.syncMigrationStatus, "[]");
final migrations = (jsonDecode(value) as List).cast<String>();
int previousLength = migrations.length;
await _runPreSyncTasks(migrations, semVer);

if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
}

// Start the sync stream and handle events
bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
if (shouldReset) {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents);
}

previousLength = migrations.length;
await _runPostSyncTasks(migrations);

if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
}

return true;
}

Future<void> _runPreSyncTasks(List<String> migrations, SemVer semVer) async {
if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) {
_logger.info("Running pre-sync task: v20260128_ResetExifV1");
await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetExifV1,
SyncEntityType.partnerAssetExifV1,
SyncEntityType.albumAssetExifCreateV1,
SyncEntityType.albumAssetExifUpdateV1,
]);
migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name);
}

if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) &&
semVer >= const SemVer(major: 2, minor: 5, patch: 0)) {
_logger.info("Running pre-sync task: v20260128_ResetAssetV1");
await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetV1,
SyncEntityType.partnerAssetV1,
SyncEntityType.albumAssetCreateV1,
SyncEntityType.albumAssetUpdateV1,
]);

migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name);

if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
}

Future<void> _runPostSyncTasks(List<String> migrations) async {
if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
_logger.info("Running post-sync task: v20260128_CopyExifWidthHeightToAsset");
await _syncMigrationRepository.v20260128CopyExifWidthHeightToAsset();
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}

Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
List<SyncEvent> items = [];
for (final event in events) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class SyncApiRepository {
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
}

Future<void> deleteSyncAck(List<SyncEntityType> types) {
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types));
}

Future<void> streamChanges(
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
Function()? onReset,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';

class SyncMigrationRepository extends DriftDatabaseRepository {
final Drift _db;

const SyncMigrationRepository(super.db) : _db = db;

Future<void> v20260128CopyExifWidthHeightToAsset() async {
await _db.customStatement('''
UPDATE remote_asset_entity
SET width = CASE
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.height
ELSE exif.width
END,
height = CASE
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.width
ELSE exif.height
END
FROM remote_exif_entity exif
WHERE exif.asset_id = remote_asset_entity.id
AND (exif.width IS NOT NULL OR exif.height IS NOT NULL);
''');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ class AssetViewer extends ConsumerStatefulWidget {
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
ref.read(videoPlayerControlsProvider.notifier).pause();
// Hide controls by default for videos and motion photos
}
// Hide controls by default for videos
if (asset.isVideo) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
}
Expand Down Expand Up @@ -147,6 +149,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
if (asset != null) {
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
}
if (ref.read(assetViewerProvider).showingControls) {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge));
} else {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky));
}
}

@override
Expand Down
5 changes: 5 additions & 0 deletions mobile/lib/providers/infrastructure/sync.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/hash.service.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
Expand All @@ -13,6 +14,8 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';

final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));

final syncStreamServiceProvider = Provider(
(ref) => SyncStreamService(
syncApiRepository: ref.watch(syncApiRepositoryProvider),
Expand All @@ -21,6 +24,8 @@ final syncStreamServiceProvider = Provider(
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider),
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
api: ref.watch(apiServiceProvider),
cancelChecker: ref.watch(cancellationProvider),
),
);
Expand Down
18 changes: 1 addition & 17 deletions mobile/lib/utils/migration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';

// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';

Expand Down Expand Up @@ -88,7 +89,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {

if (version < 20 && Store.isBetaTimelineEnabled) {
await _syncLocalAlbumIsIosSharedAlbum(drift);
await _backfillAssetExifWidthHeight(drift);
}

if (targetVersion >= 12) {
Expand Down Expand Up @@ -282,22 +282,6 @@ Future<void> _syncLocalAlbumIsIosSharedAlbum(Drift db) async {
}
}

Future<void> _backfillAssetExifWidthHeight(Drift db) async {
try {
await db.customStatement('''
UPDATE remote_exif_entity AS remote_exif
SET width = asset.width,
height = asset.height
FROM remote_asset_entity AS asset
WHERE remote_exif.asset_id = asset.id;
''');

dPrint(() => "[MIGRATION] Successfully backfilled asset exif width and height");
} catch (error) {
dPrint(() => "[MIGRATION] Error while backfilling asset exif width and height: $error");
}
}

Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try {
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,13 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {

void _decideIfWeAcceptEvent(PointerEvent event) {
final move = _initialFocalPoint! - _currentFocalPoint!;
final bool shouldMove = validateAxis == Axis.vertical
? hitDetector!.shouldMove(move, Axis.vertical)
: hitDetector!.shouldMove(move, Axis.horizontal);

// Accept gesture if movement is possible in the direction the user is swiping
final bool isHorizontalGesture = move.dx.abs() > move.dy.abs();
final bool shouldMove = isHorizontalGesture
? hitDetector!.shouldMove(move, Axis.horizontal)
: hitDetector!.shouldMove(move, Axis.vertical);

if (shouldMove || _pointerLocations.keys.length > 1) {
final double spanDelta = (_currentSpan! - _initialSpan!).abs();
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance;
Expand Down
2 changes: 1 addition & 1 deletion mobile/openapi/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mobile/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone

publish_to: 'none'
version: 2.5.1+3032
version: 2.5.2+3033

environment:
sdk: '>=3.8.0 <4.0.0'
Expand Down
2 changes: 2 additions & 0 deletions mobile/test/api.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ import 'package:openapi/api.dart';
class MockAssetsApi extends Mock implements AssetsApi {}

class MockSyncApi extends Mock implements SyncApi {}

class MockServerApi extends Mock implements ServerApi {}
Loading
Loading