Compare commits

..

20 Commits

Author SHA1 Message Date
Thomas
412633d0aa Merge branch 'main' into feat-no-thumbhash-cache 2025-09-15 15:30:09 +01:00
shenlong
dcee34095b fix: reset sqlite on beta migration (#20735)
reset sync stream on migration

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 16:30:25 -05:00
Alex
15f182902f fix: check if preferencesStore is defined (#21958) 2025-09-14 20:30:15 +00:00
shenlong
b26b452530 fix: do not listen for store updates in isolates (#21947)
* dispose store on isolate cleanup

* do not listen for store updates in isolates

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-14 14:50:17 -05:00
shenlong
2dcb32f7d0 chore: update background downloader (#21909)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 14:44:48 -05:00
Brandon Wees
27d2f3efe2 feat: disable snapping when a timeline has less than 12 months (#21649)
* feat: disable snapping when a timeline has less than 12 months

* fix: disable placeholders when not snapping

also moved month constant to constants.dart

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-14 19:24:52 +00:00
shenlong
d38468439b fix: complete does not destroy engine on close (#21943)
* fix: complete does not destroy engine on close

* reset flutterApi on cleanup

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-14 14:17:12 -05:00
Alex
0166e99d90 chore: remove main timeline query watch throttle (#21942) 2025-09-14 02:09:07 -05:00
Alex
71e33e35dc chore: check before sync linked albums from websocket events (#21941) 2025-09-14 02:08:41 -05:00
Mert
a122d4b969 fix(mobile): double hero animation (#21927)
fix double hero animation
2025-09-13 16:47:07 -05:00
shenlong
dad81af6e3 fix: show view in timeline from search page (#21873)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-12 22:44:31 -05:00
shenlong
ac6b42e1e8 fix: do not show stack action if there is only one selection (#21868)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-12 22:43:51 -05:00
Stewart Rand
4059638151 fix: context menu jank (#21844)
* Fix issue with context menu jank by only applying overflow styling when transition is complete

* Remove comment

Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-12 22:43:22 -05:00
Stewart Rand
1823a28e59 chore: improve date text slide-in transition (#21879)
* Make date text slide-in transition smooth

* fix: lint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-09-13 03:42:42 +00:00
Stewart Rand
b6bf1852cd fix: keep adequate space around page title (#21881)
Keep space around page title
2025-09-12 22:42:25 -05:00
Stewart Rand
cdc26f2c7b fix: z-index of top bar on show/hide people view (#21847)
Fix z-index of top bar on show/hide people view
2025-09-12 22:32:50 -05:00
Alex
913b3789cc chore: simplify timeline switcher toggle (#21864)
chore: timeline switcher option simplify
2025-09-12 22:32:15 -05:00
Stewart Rand
994a770921 chore: improve context button accessibility (#21876)
Make context menu button filled on album list and faces page
2025-09-12 22:31:52 -05:00
Mert
17bbcdf584 chore(mobile): add debugPrint lint rule (#21872)
* add lint rule

* update usages

* stragglers

* use dcm

* formatting

* test ci

* Revert "test ci"

This reverts commit 8f864c4e4d.

* revert whitespace change
2025-09-12 18:56:00 -04:00
Thomas Way
2bba33f834 feat(web): don't animate cached thumbnails
Thumbnails for assets always are displayed with a thumbhash which fades out
over 100ms, even if the thumbnail is cached and ready immediately. This can be
a bit distracting and make Immich feel 'slow', or inefficient as it feels like
the thumbnails are always being reloaded. Skipping the thumbhash and animation
for cached thumbnails makes it feel much more responsive.
2025-08-10 20:59:02 +01:00
73 changed files with 457 additions and 504 deletions

View File

@@ -134,6 +134,13 @@ custom_lint:
dart_code_metrics: dart_code_metrics:
rules: rules:
- banned-usage:
entries:
- name: debugPrint
description: Use dPrint instead of debugPrint for proper tree-shaking in release builds.
exclude-paths:
- 'lib/utils/debug_print.dart'
severity: perf
# All rules from "recommended" preset # All rules from "recommended" preset
# Show potential errors # Show potential errors
# - avoid-cascade-after-if-null # - avoid-cascade-after-if-null

View File

@@ -133,7 +133,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
return return
} }
isComplete = true
flutterApi?.cancel { result in flutterApi?.cancel { result in
self.complete(success: false) self.complete(success: false)
} }
@@ -174,6 +173,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
isComplete = true isComplete = true
engine.destroyContext() engine.destroyContext()
flutterApi = nil
completionHandler(success) completionHandler(success)
} }
} }

View File

@@ -45,3 +45,5 @@ const List<(String, String)> kWidgetNames = [
const double kUploadStatusFailed = -1.0; const double kUploadStatusFailed = -1.0;
const double kUploadStatusCanceled = -2.0; const double kUploadStatusCanceled = -2.0;
const int kMinMonthsToEnableScrubberSnap = 12;

View File

@@ -77,7 +77,9 @@ enum StoreKey<T> {
enableBackup<bool>._(1003), enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004), useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005), useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006); needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;

View File

@@ -7,6 +7,8 @@ import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/generated/intl_keys.g.dart';
@@ -27,6 +29,7 @@ import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_mobile/services/server_info.service.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -159,7 +162,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
try { try {
await _cleanup(); await _cleanup();
} catch (error, stack) { } catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack'); dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
} }
} }
@@ -180,6 +183,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
// Discard any errors on the dispose call // Discard any errors on the dispose call
return; return;
}), }),
LogService.I.dispose(),
Store.dispose(),
_drift.close(), _drift.close(),
_driftLogger.close(), _driftLogger.close(),
backgroundSyncManager.cancel(), backgroundSyncManager.cancel(),
@@ -192,7 +197,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
await Future.wait(cleanupFutures); await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up"); _logger.info("Background worker resources cleaned up");
} catch (error, stack) { } catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack'); dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
} }
} }
@@ -230,7 +235,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
}, },
(error, stack) { (error, stack) {
debugPrint("Error in backup zone $error, $stack"); dPrint(() => "Error in backup zone $error, $stack");
}, },
); );
} }
@@ -268,6 +273,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
DartPluginRegistrant.ensureInitialized(); DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDB) = await Bootstrap.initDB(); final (isar, drift, logDB) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false); await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init(); await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
} }

View File

@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
/// Service responsible for handling application logging. /// Service responsible for handling application logging.
@@ -66,13 +66,12 @@ class LogService {
} }
void _handleLogRecord(LogRecord r) { void _handleLogRecord(LogRecord r) {
if (kDebugMode) { dPrint(
debugPrint( () =>
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}' '${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
); );
}
final record = LogMessage( final record = LogMessage(
message: r.message, message: r.message,

View File

@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPartnerService { class DriftPartnerService {
final DriftPartnerRepository _driftPartnerRepository; final DriftPartnerRepository _driftPartnerRepository;
@@ -30,7 +30,7 @@ class DriftPartnerService {
Future<void> toggleShowInTimeline(String partnerId, String userId) async { Future<void> toggleShowInTimeline(String partnerId, String userId) async {
final partner = await _driftPartnerRepository.getPartner(partnerId, userId); final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
if (partner == null) { if (partner == null) {
debugPrint("Partner not found: $partnerId for user: $userId"); dPrint(() => "Partner not found: $partnerId for user: $userId");
return; return;
} }

View File

@@ -10,7 +10,7 @@ class StoreService {
/// In-memory cache. Keys are [StoreKey.id] /// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {}; final Map<int, Object?> _cache = {};
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription; StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
@@ -24,15 +24,17 @@ class StoreService {
} }
// TODO: Replace the implementation with the one from create after removing the typedef // TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required IStoreRepository storeRepository}) async { static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository); _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
return _instance!; return _instance!;
} }
static Future<StoreService> create({required IStoreRepository storeRepository}) async { static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
final instance = StoreService._(isarStoreRepository: storeRepository); final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache(); await instance.populateCache();
instance._storeUpdateSubscription = instance._listenForChange(); if (listenUpdates) {
instance._storeUpdateSubscription = instance._listenForChange();
}
return instance; return instance;
} }
@@ -50,8 +52,8 @@ class StoreService {
}); });
/// Disposes the store and cancels the subscription. To reuse the store call init() again /// Disposes the store and cancels the subscription. To reuse the store call init() again
void dispose() async { Future<void> dispose() async {
await _storeUpdateSubscription.cancel(); await _storeUpdateSubscription?.cancel();
_cache.clear(); _cache.clear();
} }

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
@@ -6,6 +5,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final syncLinkedAlbumServiceProvider = Provider( final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService( (ref) => SyncLinkedAlbumService(
@@ -100,7 +100,7 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album /// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async { Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
debugPrint("Creating new remote album for local album: ${localAlbum.name}"); dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []); final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []); await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id); return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);

View File

@@ -29,6 +29,7 @@ class SyncStreamService {
bool shouldReset = false; bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true); await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
if (shouldReset) { if (shouldReset) {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents); await _syncApiRepository.streamChanges(_handleEvents);
} }
} }

View File

@@ -100,8 +100,14 @@ class BackgroundSyncManager {
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle]. // captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full _deviceAlbumSyncTask = full
? runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true)) ? runInIsolateGentle(
: runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false)); computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true),
debugLabel: 'local-sync-full-true',
)
: runInIsolateGentle(
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false),
debugLabel: 'local-sync-full-false',
);
return _deviceAlbumSyncTask! return _deviceAlbumSyncTask!
.whenComplete(() { .whenComplete(() {
@@ -122,7 +128,10 @@ class BackgroundSyncManager {
onHashingStart?.call(); onHashingStart?.call();
_hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets()); _hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
debugLabel: 'hash-assets',
);
return _hashTask! return _hashTask!
.whenComplete(() { .whenComplete(() {
@@ -142,7 +151,10 @@ class BackgroundSyncManager {
onRemoteSyncStart?.call(); onRemoteSyncStart?.call();
_syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync()); _syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
debugLabel: 'remote-sync',
);
return _syncTask! return _syncTask!
.whenComplete(() { .whenComplete(() {
onRemoteSyncComplete?.call(); onRemoteSyncComplete?.call();
@@ -169,7 +181,7 @@ class BackgroundSyncManager {
return _linkedAlbumSyncTask!.future; return _linkedAlbumSyncTask!.future;
} }
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated); _linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
return _linkedAlbumSyncTask!.whenComplete(() { return _linkedAlbumSyncTask!.whenComplete(() {
_linkedAlbumSyncTask = null; _linkedAlbumSyncTask = null;
}); });
@@ -178,4 +190,5 @@ class BackgroundSyncManager {
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle( Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData), computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
debugLabel: 'websocket-batch',
); );

View File

@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:intl/message_format.dart'; import 'package:intl/message_format.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/debug_print.dart';
extension StringTranslateExtension on String { extension StringTranslateExtension on String {
String t({BuildContext? context, Map<String, Object>? args}) { String t({BuildContext? context, Map<String, Object>? args}) {
@@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map<String, Object>?
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args)
: translatedMessage; : translatedMessage;
} catch (e) { } catch (e) {
debugPrint('Translation failed for key "$key". Error: $e'); dPrint(() => 'Translation failed for key "$key". Error: $e');
return key; return key;
} }
} }

View File

@@ -69,6 +69,29 @@ class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor]) Drift([QueryExecutor? executor])
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
Future<void> reset() async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
await exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await customStatement('PRAGMA writable_schema = 1;');
await customStatement('DELETE FROM sqlite_master;');
await customStatement('VACUUM;');
await customStatement('PRAGMA writable_schema = 0;');
await customStatement('PRAGMA integrity_check');
await customStatement('PRAGMA user_version = 0');
await beforeOpen(
// ignore: invalid_use_of_internal_member
resolvedEngine.executor,
OpeningDetails(null, schemaVersion),
);
await customStatement('PRAGMA user_version = $schemaVersion');
// Refresh all stream queries
notifyUpdates({for (final table in allTables) TableUpdate.onTable(table)});
});
}
@override @override
int get schemaVersion => 10; int get schemaVersion => 10;

View File

@@ -3,7 +3,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart'; 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/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -33,6 +35,7 @@ class SyncApiRepository {
await _api.applyToParams([], headerParams); await _api.applyToParams([], headerParams);
headers.addAll(headerParams); headers.addAll(headerParams);
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
final request = http.Request('POST', Uri.parse(endpoint)); final request = http.Request('POST', Uri.parse(endpoint));
request.headers.addAll(headers); request.headers.addAll(headers);
request.body = jsonEncode( request.body = jsonEncode(
@@ -58,6 +61,7 @@ class SyncApiRepository {
SyncRequestType.peopleV1, SyncRequestType.peopleV1,
SyncRequestType.assetFacesV1, SyncRequestType.assetFacesV1,
], ],
reset: shouldReset,
).toJson(), ).toJson(),
); );
@@ -81,6 +85,9 @@ class SyncApiRepository {
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody'); 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)) { await for (final chunk in response.stream.transform(utf8.decoder)) {
if (shouldAbort) { if (shouldAbort) {
break; break;

View File

@@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket"); throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
} }
return _db.mergedAssetDrift return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
.mergedBucket(userIds: userIds, groupBy: groupBy.index) final date = row.bucketDate.dateFmt(groupBy);
.map((row) { return TimeBucket(date: date, assetCount: row.assetCount);
final date = row.bucketDate.dateFmt(groupBy); }).watch();
return TimeBucket(date: date, assetCount: row.assetCount);
})
.watch()
.throttle(const Duration(seconds: 3), trailing: true);
} }
Future<List<BaseAsset>> _getMainBucketAssets(List<String> userIds, {required int offset, required int count}) { Future<List<BaseAsset>> _getMainBucketAssets(List<String> userIds, {required int offset, required int count}) {

View File

@@ -39,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart'; import 'package:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart'; import 'package:worker_manager/worker_manager.dart';
import 'package:immich_mobile/utils/debug_print.dart';
void main() async { void main() async {
ImmichWidgetsBinding(); ImmichWidgetsBinding();
@@ -69,9 +70,9 @@ Future<void> initApp() async {
if (kReleaseMode && Platform.isAndroid) { if (kReleaseMode && Platform.isAndroid) {
try { try {
await FlutterDisplayMode.setHighRefreshRate(); await FlutterDisplayMode.setHighRefreshRate();
debugPrint("Enabled high refresh mode"); dPrint(() => "Enabled high refresh mode");
} catch (e) { } catch (e) {
debugPrint("Error setting high refresh rate: $e"); dPrint(() => "Error setting high refresh rate: $e");
} }
} }
@@ -126,23 +127,23 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (state) {
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed"); dPrint(() => "[APP STATE] resumed");
ref.read(appStateProvider.notifier).handleAppResume(); ref.read(appStateProvider.notifier).handleAppResume();
break; break;
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive"); dPrint(() => "[APP STATE] inactive");
ref.read(appStateProvider.notifier).handleAppInactivity(); ref.read(appStateProvider.notifier).handleAppInactivity();
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
debugPrint("[APP STATE] paused"); dPrint(() => "[APP STATE] paused");
ref.read(appStateProvider.notifier).handleAppPause(); ref.read(appStateProvider.notifier).handleAppPause();
break; break;
case AppLifecycleState.detached: case AppLifecycleState.detached:
debugPrint("[APP STATE] detached"); dPrint(() => "[APP STATE] detached");
ref.read(appStateProvider.notifier).handleAppDetached(); ref.read(appStateProvider.notifier).handleAppDetached();
break; break;
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
debugPrint("[APP STATE] hidden"); dPrint(() => "[APP STATE] hidden");
ref.read(appStateProvider.notifier).handleAppHidden(); ref.read(appStateProvider.notifier).handleAppHidden();
break; break;
} }
@@ -200,7 +201,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
@override @override
initState() { initState() {
super.initState(); super.initState();
initApp().then((_) => debugPrint("App Init Completed")); initApp().then((_) => dPrint(() => "App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working // needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
@@ -239,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
routerConfig: router.config( routerConfig: router.config(
deepLinkBuilder: _deepLinkBuilder, deepLinkBuilder: _deepLinkBuilder,
navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()], navigatorObservers: () => [AppNavigationObserver(ref: ref)],
), ),
), ),
); );

View File

@@ -91,6 +91,8 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
ref.read(websocketProvider.notifier).stopListenToOldEvents(); ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents(); ref.read(websocketProvider.notifier).startListeningToBetaEvents();
await ref.read(driftProvider).reset();
await Store.put(StoreKey.shouldResetSync, true);
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) { if (permission.isGranted) {

View File

@@ -12,7 +12,6 @@ import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewe
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart';
@@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
import 'package:immich_mobile/widgets/settings/settings_card.dart'; import 'package:immich_mobile/widgets/settings/settings_card.dart';
enum SettingSection { enum SettingSection {
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
@@ -28,14 +26,14 @@ enum SettingSection {
networking('networking_settings', Icons.wifi, "networking_subtitle"), networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"), preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"); timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"),
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle");
final String title; final String title;
final String subtitle; final String subtitle;
final IconData icon; final IconData icon;
Widget get widget => switch (this) { Widget get widget => switch (this) {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(), SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(), SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup => SettingSection.backup =>
@@ -45,6 +43,7 @@ enum SettingSection {
SettingSection.notifications => const NotificationSetting(), SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(), SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(), SettingSection.timeline => const AssetListSettings(),
SettingSection.beta => const SyncStatusAndActions(),
}; };
const SettingSection(this.title, this.icon, this.subtitle); const SettingSection(this.title, this.icon, this.subtitle);
@@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget {
context.locale; context.locale;
return Scaffold( return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('settings').tr()), appBar: AppBar(centerTitle: false, title: const Text('settings').tr()),
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()),
); );
} }
} }
@@ -72,7 +71,6 @@ class _MobileLayout extends StatelessWidget {
.expand( .expand(
(setting) => setting == SettingSection.beta (setting) => setting == SettingSection.beta
? [ ? [
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled) if (Store.isBetaTimelineEnabled)
SettingsCard( SettingsCard(
icon: Icons.sync_outlined, icon: Icons.sync_outlined,
@@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget {
.toList(); .toList();
return ListView( return ListView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(top: 10.0, bottom: 56), padding: const EdgeInsets.only(top: 10.0, bottom: 16),
children: [...settings], children: [...settings],
); );
} }
@@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget {
} }
} }
class _BetaLandscapeToggle extends HookWidget {
const _BetaLandscapeToggle();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 100, child: BetaTimelineListTile()),
if (Store.isBetaTimelineEnabled) const Expanded(child: SyncStatusAndActions()),
],
);
}
}
@RoutePage() @RoutePage()
class SettingsSubPage extends StatelessWidget { class SettingsSubPage extends StatelessWidget {
const SettingsSubPage(this.section, {super.key}); const SettingsSubPage(this.section, {super.key});
@@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
context.locale; context.locale;
return Scaffold( return SafeArea(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), bottom: true,
body: section.widget, top: false,
right: true,
child: Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
),
); );
} }
} }

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@RoutePage() @RoutePage()
class DriftPartnerDetailPage extends StatelessWidget { class DriftPartnerDetailPage extends StatelessWidget {
@@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
_inTimeline = !_inTimeline; _inTimeline = !_inTimeline;
}); });
} catch (error, stack) { } catch (error, stack) {
debugPrint("Failed to toggle in timeline: $error $stack"); dPrint(() => "Failed to toggle in timeline: $error $stack");
ImmichToast.show( ImmichToast.show(
context: context, context: context,
toastType: ToastType.error, toastType: ToastType.error,

View File

@@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final previousRouteName = ref.watch(previousRouteNameProvider); final previousRouteName = ref.watch(previousRouteNameProvider);
final tabRoute = ref.watch(tabProvider);
final showViewInTimelineButton = final showViewInTimelineButton =
previousRouteName != TabShellRoute.name && (previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name && previousRouteName != AssetViewerRoute.name &&
previousRouteName != null; previousRouteName != null;

View File

@@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -43,7 +43,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -104,7 +104,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const EditDateTimeActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
isTrashEnable isTrashEnable
? const TrashActionButton(source: ActionSource.timeline) ? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline), : const DeletePermanentActionButton(source: ActionSource.timeline),

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -100,7 +100,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const EditDateTimeActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
final DriftPerson person; final DriftPerson person;
@@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
context.pop<DateTime>(_selectedDate); context.pop<DateTime>(_selectedDate);
} }
} catch (error) { } catch (error) {
debugPrint('Error updating birthday: $error'); dPrint(() => 'Error updating birthday: $error');
if (!context.mounted) { if (!context.mounted) {
return; return;

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonNameEditForm extends ConsumerStatefulWidget { class DriftPersonNameEditForm extends ConsumerStatefulWidget {
final DriftPerson person; final DriftPerson person;
@@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
context.pop<String>(newName); context.pop<String>(newName);
} }
} catch (error) { } catch (error) {
debugPrint('Error updating name: $error'); dPrint(() => 'Error updating name: $error');
if (!context.mounted) { if (!context.mounted) {
return; return;

View File

@@ -3,14 +3,15 @@ import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView. /// for quick navigation of the BoxScrollView.
@@ -79,6 +80,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
double _thumbTopOffset = 0.0; double _thumbTopOffset = 0.0;
bool _isDragging = false; bool _isDragging = false;
List<_Segment> _segments = []; List<_Segment> _segments = [];
int _monthCount = 0;
late AnimationController _thumbAnimationController; late AnimationController _thumbAnimationController;
Timer? _fadeOutTimer; Timer? _fadeOutTimer;
@@ -105,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration); _thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut); _thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration); _labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_monthCount = getMonthCount();
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn); _labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
} }
@@ -121,6 +124,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) { if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight); _segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_monthCount = getMonthCount();
} }
} }
@@ -140,6 +144,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}); });
} }
int getMonthCount() {
return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length;
}
bool _onScrollNotification(ScrollNotification notification) { bool _onScrollNotification(ScrollNotification notification) {
if (_isDragging) { if (_isDragging) {
// If the user is dragging the thumb, we don't want to update the position // If the user is dragging the thumb, we don't want to update the position
@@ -169,7 +177,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
} }
void _onDragStart(DragStartDetails _) { void _onDragStart(DragStartDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(true); if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
}
setState(() { setState(() {
_isDragging = true; _isDragging = true;
_labelAnimationController.forward(); _labelAnimationController.forward();
@@ -191,13 +202,22 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
final nearestMonthSegment = _findNearestMonthSegment(dragPosition); final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
if (nearestMonthSegment != null) { if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
final label = nearestMonthSegment.scrollLabel; final label = nearestMonthSegment.scrollLabel;
if (_lastLabel != label) { if (_lastLabel != label) {
ref.read(hapticFeedbackProvider.notifier).selectionClick(); ref.read(hapticFeedbackProvider.notifier).selectionClick();
_lastLabel = label; _lastLabel = label;
} }
} }
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
setState(() {
_thumbTopOffset = dragPosition;
_scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent);
});
} else if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
}
} }
/// Calculate the drag position relative to the scrubber area /// Calculate the drag position relative to the scrubber area

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
@@ -13,6 +12,7 @@ import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart'; import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) { final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier( return AssetNotifier(
@@ -68,7 +68,7 @@ class AssetNotifier extends StateNotifier<bool> {
} }
final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums(); final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint("changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
if (newRemote) { if (newRemote) {
_ref.invalidate(memoryFutureProvider); _ref.invalidate(memoryFutureProvider);
} }

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
@@ -18,6 +17,7 @@ import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) { final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier( return AuthNotifier(
@@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
_log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace); _log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace);
} catch (error, stackTrace) { } catch (error, stackTrace) {
_log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace); _log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace);
dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace");
if (kDebugMode) {
debugPrint("Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
} }
// If the user is null, the login was not successful // If the user is null, the login was not successful

View File

@@ -2,8 +2,6 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
@@ -33,6 +31,7 @@ import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) { final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier( return BackupNotifier(
@@ -286,7 +285,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums);
log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums");
debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
} }
/// ///
@@ -428,7 +427,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process /// Invoke backup process
Future<void> startBackupProcess() async { Future<void> startBackupProcess() async {
debugPrint("Start backup process"); dPrint(() => "Start backup process");
assert(state.backupProgress == BackUpProgressEnum.idle); assert(state.backupProgress == BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
@@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class EnqueueStatus { class EnqueueStatus {
final int enqueueCount; final int enqueueCount;
@@ -329,16 +329,16 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
} }
Future<void> cancel() async { Future<void> cancel() async {
debugPrint("Canceling backup tasks..."); dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true); state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true);
final activeTaskCount = await _uploadService.cancelBackup(); final activeTaskCount = await _uploadService.cancelBackup();
if (activeTaskCount > 0) { if (activeTaskCount > 0) {
debugPrint("$activeTaskCount tasks left, continuing to cancel..."); dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
await cancel(); await cancel();
} else { } else {
debugPrint("All tasks canceled successfully."); dPrint(() => "All tasks canceled successfully.");
// Clear all upload items when cancellation is complete // Clear all upload items when cancellation is complete
state = state.copyWith(isCanceling: false, uploadItems: {}); state = state.copyWith(isCanceling: false, uploadItems: {});
} }

View File

@@ -30,6 +30,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) { final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier( return ManualUploadNotifier(
@@ -216,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
); );
if (uploadAssets.isEmpty) { if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process"); dPrint(() => "[_startUpload] No Assets to upload - Abort Process");
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
return false; return false;
} }
@@ -294,10 +295,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
} }
} else { } else {
openAppSettings(); openAppSettings();
debugPrint("[_startUpload] Do not have permission to the gallery"); dPrint(() => "[_startUpload] Do not have permission to the gallery");
} }
} catch (e) { } catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}"); dPrint(() => "ERROR _startUpload: ${e.toString()}");
hasErrors = true; hasErrors = true;
} finally { } finally {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
@@ -340,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// waits until it has stopped to start the backup. // waits until it has stopped to start the backup.
final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock();
if (!hasLock) { if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting"); dPrint(() => "[uploadAssets] could not acquire lock, exiting");
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: "failed".tr(), msg: "failed".tr(),
@@ -355,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// check if backup is already in process - then return // check if backup is already in process - then return
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
debugPrint("[uploadAssets] Manual upload is already running - abort"); dPrint(() => "[uploadAssets] Manual upload is already running - abort");
showInProgress = true; showInProgress = true;
} }
if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[uploadAssets] Auto Backup is already in progress - abort"); dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort");
showInProgress = true; showInProgress = true;
return false; return false;
} }
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
debugPrint("[uploadAssets] Background backup is running - abort"); dPrint(() => "[uploadAssets] Background backup is running - abort");
showInProgress = true; showInProgress = true;
} }

View File

@@ -7,11 +7,12 @@ import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) { final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode); final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode"); dPrint(() => "Current themeMode $themeMode");
if (themeMode == ThemeMode.light.name) { if (themeMode == ThemeMode.light.name) {
return ThemeMode.light; return ThemeMode.light;
@@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
final appSettingsProvider = ref.watch(appSettingsServiceProvider); final appSettingsProvider = ref.watch(appSettingsServiceProvider);
final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
debugPrint("Current theme preset $primaryColorPreset"); dPrint(() => "Current theme preset $primaryColorPreset");
try { try {
return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset); return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset);
} catch (e) { } catch (e) {
debugPrint("Theme preset $primaryColorPreset not found. Applying default preset."); dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset.");
appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName); appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName);
return defaultColorPreset; return defaultColorPreset;
} }

View File

@@ -1,10 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum UploadProfileStatus { idle, loading, success, failure } enum UploadProfileStatus { idle, loading, success, failure }
@@ -67,7 +67,7 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes()); var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes());
if (profileImagePath != null) { if (profileImagePath != null) {
debugPrint("Successfully upload profile image"); dPrint(() => "Successfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath); state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath);
return true; return true;
} }

View File

@@ -2,8 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
@@ -20,6 +18,7 @@ import 'package:immich_mobile/utils/debounce.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart'; import 'package:socket_io_client/socket_io_client.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
@@ -105,7 +104,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
} }
debugPrint("Attempting to connect to websocket"); dPrint(() => "Attempting to connect to websocket");
// Configure socket transports must be specified // Configure socket transports must be specified
Socket socket = io( Socket socket = io(
endpoint.origin, endpoint.origin,
@@ -121,12 +120,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
); );
socket.onConnect((_) { socket.onConnect((_) {
debugPrint("Established Websocket Connection"); dPrint(() => "Established Websocket Connection");
state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges);
}); });
socket.onDisconnect((_) { socket.onDisconnect((_) {
debugPrint("Disconnect to Websocket Connection"); dPrint(() => "Disconnect to Websocket Connection");
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
}); });
@@ -150,13 +149,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_new_release', _handleReleaseUpdates); socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) { } catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
} }
} }
} }
void disconnect() { void disconnect() {
debugPrint("Attempting to disconnect from websocket"); dPrint(() => "Attempting to disconnect from websocket");
_batchedAssetUploadReady.clear(); _batchedAssetUploadReady.clear();
@@ -200,7 +199,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
} }
void listenUploadEvent() { void listenUploadEvent() {
debugPrint("Start listening to event on_upload_success"); dPrint(() => "Start listening to event on_upload_success");
state.socket?.on('on_upload_success', _handleOnUploadSuccess); state.socket?.on('on_upload_success', _handleOnUploadSuccess);
} }
@@ -321,10 +320,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return; return;
} }
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
return _ref.read(backgroundSyncProvider).syncLinkedAlbum(); if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
}
}), }),
); );
} catch (error) { } catch (error) {

View File

@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class UploadTaskWithFile { class UploadTaskWithFile {
final File file; final File file;
@@ -79,14 +79,17 @@ class UploadRepository {
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup), FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
]); ]);
debugPrint(""" dPrint(
() =>
"""
Upload Info: Upload Info:
Enqueued: ${enqueuedTasks.length} Enqueued: ${enqueuedTasks.length}
Running: ${runningTasks.length} Running: ${runningTasks.length}
Canceled: ${canceledTasks.length} Canceled: ${canceledTasks.length}
Waiting: ${waitingTasks.length} Waiting: ${waitingTasks.length}
Paused: ${pausedTasks.length} Paused: ${pausedTasks.length}
"""); """,
);
} }
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async { Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {

View File

@@ -1,5 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:immich_mobile/utils/debug_print.dart';
/// Guards against duplicate navigation to this route /// Guards against duplicate navigation to this route
class DuplicateGuard extends AutoRouteGuard { class DuplicateGuard extends AutoRouteGuard {
@@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async { void onNavigation(NavigationResolver resolver, StackRouter router) async {
// Duplicate navigation // Duplicate navigation
if (resolver.route.name == router.current.name) { if (resolver.route.name == router.current.name) {
debugPrint('DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}');
resolver.next(false); resolver.next(false);
} else { } else {
resolver.next(true); resolver.next(true);

View File

@@ -3,7 +3,6 @@ import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
@@ -24,6 +23,7 @@ import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final albumServiceProvider = Provider( final albumServiceProvider = Provider(
(ref) => AlbumService( (ref) => AlbumService(
@@ -124,7 +124,7 @@ class AlbumService {
} finally { } finally {
_localCompleter.complete(changes); _localCompleter.complete(changes);
} }
debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
return changes; return changes;
} }
@@ -172,7 +172,7 @@ class AlbumService {
} finally { } finally {
_remoteCompleter.complete(changes); _remoteCompleter.complete(changes);
} }
debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
return changes; return changes;
} }
@@ -220,7 +220,7 @@ class AlbumService {
return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length);
} catch (e) { } catch (e) {
debugPrint("Error addAssets ${e.toString()}"); dPrint(() => "Error addAssets ${e.toString()}");
} }
return null; return null;
} }
@@ -242,7 +242,7 @@ class AlbumService {
await _albumRepository.update(album); await _albumRepository.update(album);
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}"); dPrint(() => "Error setActivityEnabled ${e.toString()}");
} }
return false; return false;
} }
@@ -271,7 +271,7 @@ class AlbumService {
} }
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}"); dPrint(() => "Error deleteAlbum ${e.toString()}");
} }
return false; return false;
} }
@@ -281,7 +281,7 @@ class AlbumService {
await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}"); dPrint(() => "Error leaveAlbum ${e.toString()}");
return false; return false;
} }
} }
@@ -293,7 +293,7 @@ class AlbumService {
await _updateAssets(album.id, remove: toRemove.toList()); await _updateAssets(album.id, remove: toRemove.toList());
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}"); dPrint(() => "Error removeAssetFromAlbum ${e.toString()}");
} }
return false; return false;
} }
@@ -310,7 +310,7 @@ class AlbumService {
return true; return true;
} catch (error) { } catch (error) {
debugPrint("Error removeUser ${error.toString()}"); dPrint(() => "Error removeUser ${error.toString()}");
return false; return false;
} }
} }
@@ -327,7 +327,7 @@ class AlbumService {
return true; return true;
} catch (error) { } catch (error) {
debugPrint("Error addUsers ${error.toString()}"); dPrint(() => "Error addUsers ${error.toString()}");
} }
return false; return false;
} }
@@ -340,7 +340,7 @@ class AlbumService {
await _albumRepository.update(album); await _albumRepository.update(album);
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}"); dPrint(() => "Error changeTitleAlbum ${e.toString()}");
return false; return false;
} }
} }
@@ -353,7 +353,7 @@ class AlbumService {
await _albumRepository.update(album); await _albumRepository.update(album);
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error changeDescriptionAlbum ${e.toString()}"); dPrint(() => "Error changeDescriptionAlbum ${e.toString()}");
return false; return false;
} }
} }

View File

@@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -11,6 +10,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart'; import 'package:immich_mobile/utils/user_agent.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ApiService implements Authentication { class ApiService implements Authentication {
late ApiClient _apiClient; late ApiClient _apiClient;
@@ -155,7 +155,7 @@ class ApiService implements Authentication {
return endpoint; return endpoint;
} }
} catch (e) { } catch (e) {
debugPrint("Could not locate /.well-known/immich at $baseUrl"); dPrint(() => "Could not locate /.well-known/immich at $baseUrl");
} }
return ""; return "";

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
@@ -26,6 +25,7 @@ import 'package:immich_mobile/services/sync.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetServiceProvider = Provider( final assetServiceProvider = Provider(
(ref) => AssetService( (ref) => AssetService(
@@ -87,7 +87,7 @@ class AssetService {
getChangedAssets: _getRemoteAssetChanges, getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets, loadAssets: _getRemoteAssets,
); );
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes; return changes;
} }
@@ -156,7 +156,7 @@ class AssetService {
if (a.isInDb) { if (a.isInDb) {
await _assetRepository.transaction(() => _assetRepository.update(a)); await _assetRepository.transaction(() => _assetRepository.update(a));
} else { } else {
debugPrint("[loadExif] parameter Asset is not from DB!"); dPrint(() => "[loadExif] parameter Asset is not from DB!");
} }
} }
} }

View File

@@ -7,7 +7,6 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -29,6 +28,7 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart'; import 'package:path_provider_foundation/path_provider_foundation.dart';
@@ -165,7 +165,7 @@ class BackgroundService {
]); ]);
} }
} catch (error) { } catch (error) {
debugPrint("[_updateNotification] failed to communicate with plugin"); dPrint(() => "[_updateNotification] failed to communicate with plugin");
} }
return false; return false;
} }
@@ -177,7 +177,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]);
} }
} catch (error) { } catch (error) {
debugPrint("[_showErrorNotification] failed to communicate with plugin"); dPrint(() => "[_showErrorNotification] failed to communicate with plugin");
} }
return false; return false;
} }
@@ -188,7 +188,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('clearErrorNotifications'); return await _backgroundChannel.invokeMethod('clearErrorNotifications');
} }
} catch (error) { } catch (error) {
debugPrint("[_clearErrorNotifications] failed to communicate with plugin"); dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin");
} }
return false; return false;
} }
@@ -196,7 +196,7 @@ class BackgroundService {
/// await to ensure this thread (foreground or background) has exclusive access /// await to ensure this thread (foreground or background) has exclusive access
Future<bool> acquireLock() async { Future<bool> acquireLock() async {
if (_hasLock) { if (_hasLock) {
debugPrint("WARNING: [acquireLock] called more than once"); dPrint(() => "WARNING: [acquireLock] called more than once");
return true; return true;
} }
final int lockTime = Timeline.now; final int lockTime = Timeline.now;
@@ -302,19 +302,19 @@ class BackgroundService {
final bool hasAccess = await waitForLock; final bool hasAccess = await waitForLock;
if (!hasAccess) { if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting"); dPrint(() => "[_callHandler] could not acquire lock, exiting");
return false; return false;
} }
final translationsOk = await loadTranslations(); final translationsOk = await loadTranslations();
if (!translationsOk) { if (!translationsOk) {
debugPrint("[_callHandler] could not load translations"); dPrint(() => "[_callHandler] could not load translations");
} }
final bool ok = await _onAssetsChanged(); final bool ok = await _onAssetsChanged();
return ok; return ok;
} catch (error) { } catch (error) {
debugPrint(error.toString()); dPrint(() => error.toString());
return false; return false;
} finally { } finally {
releaseLock(); releaseLock();
@@ -324,14 +324,14 @@ class BackgroundService {
_cancellationToken?.cancel(); _cancellationToken?.cancel();
return true; return true;
default: default:
debugPrint("Unknown method ${call.method}"); dPrint(() => "Unknown method ${call.method}");
return false; return false;
} }
} }
Future<bool> _onAssetsChanged() async { Future<bool> _onAssetsChanged() async {
final (isar, drift, logDb) = await Bootstrap.initDB(); final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb); await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer( final ref = ProviderContainer(
overrides: [ overrides: [
@@ -344,9 +344,7 @@ class BackgroundService {
HttpSSLOptions.apply(); HttpSSLOptions.apply();
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
if (kDebugMode) { dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
}
final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select);
final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude);

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart' as http; import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
@@ -29,6 +28,7 @@ import 'package:openapi/api.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart' as pm; import 'package:permission_handler/permission_handler.dart' as pm;
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupServiceProvider = Provider( final backupServiceProvider = Provider(
(ref) => BackupService( (ref) => BackupService(
@@ -69,7 +69,7 @@ class BackupService {
try { try {
return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId);
} catch (e) { } catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}');
return null; return null;
} }
} }
@@ -356,8 +356,9 @@ class BackupService {
final error = responseBody; final error = responseBody;
final errorMessage = error['message'] ?? error['error']; final errorMessage = error['message'] ?? error['error'];
debugPrint( dPrint(
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", () =>
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
); );
onError( onError(
@@ -398,11 +399,11 @@ class BackupService {
} }
} }
} on http.CancelledException { } on http.CancelledException {
debugPrint("Backup was cancelled by the user"); dPrint(() => "Backup was cancelled by the user");
anyErrors = true; anyErrors = true;
break; break;
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace");
anyErrors = true; anyErrors = true;
continue; continue;
} finally { } finally {
@@ -411,7 +412,7 @@ class BackupService {
await file?.delete(); await file?.delete();
await livePhotoFile?.delete(); await livePhotoFile?.delete();
} catch (e) { } catch (e) {
debugPrint("ERROR deleting file: ${e.toString()}"); dPrint(() => "ERROR deleting file: ${e.toString()}");
} }
} }
} }
@@ -454,7 +455,9 @@ class BackupService {
if (![200, 201].contains(response.statusCode)) { if (![200, 201].contains(response.statusCode)) {
var error = responseBody; var error = responseBody;
debugPrint("Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}"); dPrint(
() => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}",
);
} }
return responseBody.containsKey('id') ? responseBody['id'] : null; return responseBody.containsKey('id') ? responseBody['id'] : null;

View File

@@ -1,9 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final localNotificationService = Provider( final localNotificationService = Provider(
(ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref),
@@ -110,7 +110,7 @@ class LocalNotificationService {
switch (notificationResponse.actionId) { switch (notificationResponse.actionId) {
case cancelUploadActionID: case cancelUploadActionID:
{ {
debugPrint("User cancelled manual upload operation"); dPrint(() => "User cancelled manual upload operation");
ref.read(manualUploadProvider.notifier).cancelBackup(); ref.read(manualUploadProvider.notifier).cancelBackup();
} }
} }

View File

@@ -2,9 +2,9 @@
import 'package:easy_localization/src/easy_localization_controller.dart'; import 'package:easy_localization/src/easy_localization_controller.dart';
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/utils/debug_print.dart';
/// Workaround to manually load translations in another Isolate /// Workaround to manually load translations in another Isolate
Future<bool> loadTranslations() async { Future<bool> loadTranslations() async {
@@ -17,7 +17,7 @@ Future<bool> loadTranslations() async {
assetLoader: const CodegenLoader(), assetLoader: const CodegenLoader(),
path: translationsPath, path: translationsPath,
useOnlyLangCode: false, useOnlyLangCode: false,
onLoadError: (e) => debugPrint(e.toString()), onLoadError: (e) => dPrint(() => e.toString()),
fallbackLocale: locales.values.first, fallbackLocale: locales.values.first,
); );

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
@@ -10,6 +9,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final searchServiceProvider = Provider( final searchServiceProvider = Provider(
(ref) => SearchService( (ref) => SearchService(
@@ -43,7 +43,7 @@ class SearchService {
model: model, model: model,
); );
} catch (e) { } catch (e) {
debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}"); dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}");
return []; return [];
} }
} }

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart';
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
@@ -6,6 +5,7 @@ import 'package:immich_mobile/models/server_info/server_features.model.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider))); final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider)));
@@ -30,7 +30,7 @@ class ServerInfoService {
return ServerDiskInfo.fromDto(dto); return ServerDiskInfo.fromDto(dto);
} }
} catch (e) { } catch (e) {
debugPrint("Error [getDiskInfo] ${e.toString()}"); dPrint(() => "Error [getDiskInfo] ${e.toString()}");
} }
return null; return null;
} }
@@ -42,7 +42,7 @@ class ServerInfoService {
return ServerVersion.fromDto(dto); return ServerVersion.fromDto(dto);
} }
} catch (e) { } catch (e) {
debugPrint("Error [getServerVersion] ${e.toString()}"); dPrint(() => "Error [getServerVersion] ${e.toString()}");
} }
return null; return null;
} }
@@ -54,7 +54,7 @@ class ServerInfoService {
return ServerFeatures.fromDto(dto); return ServerFeatures.fromDto(dto);
} }
} catch (e) { } catch (e) {
debugPrint("Error [getServerFeatures] ${e.toString()}"); dPrint(() => "Error [getServerFeatures] ${e.toString()}");
} }
return null; return null;
} }
@@ -66,7 +66,7 @@ class ServerInfoService {
return ServerConfig.fromDto(dto); return ServerConfig.fromDto(dto);
} }
} catch (e) { } catch (e) {
debugPrint("Error [getServerConfig] ${e.toString()}"); dPrint(() => "Error [getServerConfig] ${e.toString()}");
} }
return null; return null;
} }

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class StackService { class StackService {
const StackService(this._api, this._assetRepository); const StackService(this._api, this._assetRepository);
@@ -16,7 +16,7 @@ class StackService {
try { try {
return _api.stacksApi.getStack(stackId); return _api.stacksApi.getStack(stackId);
} catch (error) { } catch (error) {
debugPrint("Error while fetching stack: $error"); dPrint(() => "Error while fetching stack: $error");
} }
return null; return null;
} }
@@ -25,7 +25,7 @@ class StackService {
try { try {
return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds));
} catch (error) { } catch (error) {
debugPrint("Error while creating stack: $error"); dPrint(() => "Error while creating stack: $error");
} }
return null; return null;
} }
@@ -34,7 +34,7 @@ class StackService {
try { try {
return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId));
} catch (error) { } catch (error) {
debugPrint("Error while updating stack children: $error"); dPrint(() => "Error while updating stack children: $error");
} }
return null; return null;
} }
@@ -54,7 +54,7 @@ class StackService {
} }
await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets));
} catch (error) { } catch (error) {
debugPrint("Error while deleting stack: $error"); dPrint(() => "Error while deleting stack: $error");
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.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/asset/base_asset.model.dart';
@@ -17,12 +16,12 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:immich_mobile/utils/debug_print.dart';
final uploadServiceProvider = Provider((ref) { final uploadServiceProvider = Provider((ref) {
final service = UploadService( final service = UploadService(
@@ -31,7 +30,6 @@ final uploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider), ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository), ref.watch(localAssetRepository),
ref.watch(appSettingsServiceProvider), ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider),
); );
ref.onDispose(service.dispose); ref.onDispose(service.dispose);
@@ -45,7 +43,6 @@ class UploadService {
this._storageRepository, this._storageRepository,
this._localAssetRepository, this._localAssetRepository,
this._appSettingsService, this._appSettingsService,
this._assetMediaRepository,
) { ) {
_uploadRepository.onUploadStatus = _onUploadCallback; _uploadRepository.onUploadStatus = _onUploadCallback;
_uploadRepository.onTaskProgress = _onTaskProgressCallback; _uploadRepository.onTaskProgress = _onTaskProgressCallback;
@@ -56,7 +53,6 @@ class UploadService {
final StorageRepository _storageRepository; final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final AppSettingsService _appSettingsService; final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('UploadService'); final Logger _logger = Logger('UploadService');
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast(); final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
@@ -257,7 +253,7 @@ class UploadService {
enqueueTasks([uploadTask]); enqueueTasks([uploadTask]);
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrint("Error handling live photo upload task: $error $stackTrace"); dPrint(() => "Error handling live photo upload task: $error $stackTrace");
} }
} }
@@ -325,8 +321,7 @@ class UploadService {
return null; return null;
} }
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
String metadata = UploadTaskMetadata( String metadata = UploadTaskMetadata(
localAssetId: asset.id, localAssetId: asset.id,
@@ -364,13 +359,12 @@ class UploadService {
final fields = {'livePhotoVideoId': livePhotoVideoId}; final fields = {'livePhotoVideoId': livePhotoVideoId};
final requiresWiFi = _shouldRequireWiFi(asset); final requiresWiFi = _shouldRequireWiFi(asset);
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
return buildUploadTask( return buildUploadTask(
file, file,
createdAt: asset.createdAt, createdAt: asset.createdAt,
modifiedAt: asset.updatedAt, modifiedAt: asset.updatedAt,
originalFileName: originalFileName, originalFileName: asset.name,
deviceAssetId: asset.id, deviceAssetId: asset.id,
fields: fields, fields: fields,
group: kBackupLivePhotoGroup, group: kBackupLivePhotoGroup,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/debug_print.dart';
abstract final class DynamicTheme { abstract final class DynamicTheme {
const DynamicTheme._(); const DynamicTheme._();
@@ -13,7 +14,7 @@ abstract final class DynamicTheme {
final corePalette = await DynamicColorPlugin.getCorePalette(); final corePalette = await DynamicColorPlugin.getCorePalette();
if (corePalette != null) { if (corePalette != null) {
final primaryColor = corePalette.toColorScheme().primary; final primaryColor = corePalette.toColorScheme().primary;
debugPrint('dynamic_color: Core palette detected.'); dPrint(() => 'dynamic_color: Core palette detected.');
// Some palettes do not generate surface container colors accurately, // Some palettes do not generate surface container colors accurately,
// so we regenerate all colors using the primary color // so we regenerate all colors using the primary color
@@ -23,7 +24,7 @@ abstract final class DynamicTheme {
); );
} }
} catch (error) { } catch (error) {
debugPrint('dynamic_color: Failed to obtain core palette: $error'); dPrint(() => 'dynamic_color: Failed to obtain core palette: $error');
} }
} }

View File

@@ -89,11 +89,17 @@ abstract final class Bootstrap {
return (isar, drift, logDb); return (isar, drift, logDb);
} }
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async { static Future<void> initDomain(
Isar db,
Drift drift,
DriftLogger logDb, {
bool listenStoreUpdates = true,
bool shouldBufferLogs = true,
}) async {
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true;
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
await StoreService.init(storeRepository: storeRepo); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await LogService.init( await LogService.init(
logRepository: LogRepository(logDb), logRepository: LogRepository(logDb),

View File

@@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart';
@pragma('vm:prefer-inline')
void dPrint(String Function() message) {
if (kDebugMode) {
debugPrint(message());
}
}

View File

@@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart'; import 'package:worker_manager/worker_manager.dart';
@@ -37,7 +38,7 @@ Cancelable<T?> runInIsolateGentle<T>({
DartPluginRegistrant.ensureInitialized(); DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDb) = await Bootstrap.initDB(); final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false); await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer( final ref = ProviderContainer(
overrides: [ overrides: [
// TODO: Remove once isar is removed // TODO: Remove once isar is removed
@@ -61,6 +62,7 @@ Cancelable<T?> runInIsolateGentle<T>({
try { try {
ref.dispose(); ref.dispose();
await Store.dispose();
await LogService.I.dispose(); await LogService.I.dispose();
await logDb.close(); await logDb.close();
await drift.close(); await drift.close();
@@ -71,10 +73,10 @@ Cancelable<T?> runInIsolateGentle<T>({
await isar.close(); await isar.close();
} }
} catch (e) { } catch (e) {
debugPrint("Error closing Isar: $e"); dPrint(() => "Error closing Isar: $e");
} }
} catch (error, stack) { } catch (error, stack) {
debugPrint("Error closing resources in isolate: $error, $stack"); dPrint(() => "Error closing resources in isolate: $error, $stack");
} finally { } finally {
ref.dispose(); ref.dispose();
// Delay to ensure all resources are released // Delay to ensure all resources are released
@@ -84,7 +86,7 @@ Cancelable<T?> runInIsolateGentle<T>({
return null; return null;
}, },
(error, stack) { (error, stack) {
debugPrint("Error in isolate zone: $error, $stack"); dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
}, },
); );
return null; return null;

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
@@ -22,12 +21,14 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager // ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 15; const int targetVersion = 16;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async { Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null; final hasVersion = Store.tryGet(StoreKey.version) != null;
@@ -76,11 +77,16 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.put(StoreKey.needBetaMigration, false); await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true); await Store.put(StoreKey.betaTimeline, true);
} else { } else {
await resetDriftDatabase(drift); await drift.reset();
await Store.put(StoreKey.needBetaMigration, true); await Store.put(StoreKey.needBetaMigration, true);
} }
} }
if (version < 16) {
await SyncStreamRepository(drift).reset();
await Store.put(StoreKey.shouldResetSync, true);
}
if (targetVersion >= 12) { if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
@@ -117,7 +123,7 @@ Future<bool> _isNewInstallation(Isar db, Drift drift) async {
return true; return true;
} catch (error) { } catch (error) {
debugPrint("[MIGRATION] Error checking if new installation: $error"); dPrint(() => "[MIGRATION] Error checking if new installation: $error");
return false; return false;
} }
} }
@@ -143,10 +149,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
final PermissionState ps = await PhotoManager.requestPermissionExtend(); final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (!ps.hasAccess) { if (!ps.hasAccess) {
if (kDebugMode) { dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
}
return; return;
} }
@@ -166,8 +169,8 @@ Future<void> _migrateDeviceAsset(Isar db) async {
localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList();
} }
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}"); dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}");
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}"); dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}");
ids.sort((a, b) => a.assetId.compareTo(b.assetId)); ids.sort((a, b) => a.assetId.compareTo(b.assetId));
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
final List<DeviceAssetEntity> toAdd = []; final List<DeviceAssetEntity> toAdd = [];
@@ -182,20 +185,14 @@ Future<void> _migrateDeviceAsset(Isar db) async {
return false; return false;
}, },
onlyFirst: (deviceAsset) { onlyFirst: (deviceAsset) {
if (kDebugMode) { dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
}
}, },
onlySecond: (asset) { onlySecond: (asset) {
if (kDebugMode) { dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
}
}, },
); );
if (kDebugMode) { dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
}
await db.writeTxn(() async { await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd); await db.deviceAssetEntitys.putAll(toAdd);
@@ -215,7 +212,7 @@ Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
} }
}); });
} catch (error) { } catch (error) {
debugPrint("[MIGRATION] Error while migrating device assets to SQLite: $error"); dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error");
} }
} }
@@ -263,7 +260,7 @@ Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
} }
}); });
} catch (error) { } catch (error) {
debugPrint("[MIGRATION] Error while migrating backup albums to SQLite: $error"); dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error");
} }
} }
@@ -281,7 +278,7 @@ Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
} }
}); });
} catch (error) { } catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error"); dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error");
} }
} }
@@ -296,7 +293,7 @@ Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
await db.storeValues.putAll(driftStoreValues); await db.storeValues.putAll(driftStoreValues);
}); });
} catch (error) { } catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to Isar: $error"); dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error");
} }
} }
@@ -307,27 +304,3 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
} }
Future<void> resetDriftDatabase(Drift drift) async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
final database = drift.attachedDatabase;
await database.exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await database.customStatement('PRAGMA writable_schema = 1;');
await database.customStatement('DELETE FROM sqlite_master;');
await database.customStatement('VACUUM;');
await database.customStatement('PRAGMA writable_schema = 0;');
await database.customStatement('PRAGMA integrity_check');
await database.customStatement('PRAGMA user_version = 0');
await database.beforeOpen(
// ignore: invalid_use_of_internal_member
database.resolvedEngine.executor,
OpeningDetails(null, database.schemaVersion),
);
await database.customStatement('PRAGMA user_version = ${database.schemaVersion}');
// Refresh all stream queries
database.notifyUpdates({for (final table in database.allTables) TableUpdate.onTable(table)});
});
}

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ExifMap extends StatelessWidget { class ExifMap extends StatelessWidget {
final ExifInfo exifInfo; final ExifInfo exifInfo;
@@ -66,7 +67,7 @@ class ExifMap extends StatelessWidget {
return; return;
} }
debugPrint('Opening Map Uri: $uri'); dPrint(() => 'Opening Map Uri: $uri');
launchUrl(uri); launchUrl(uri);
}, },
onCreated: onMapCreated, onCreated: onMapCreated,

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
@@ -91,7 +92,7 @@ class AdvancedSettings extends HookConsumerWidget {
title: "advanced_settings_prefer_remote_title".tr(), title: "advanced_settings_prefer_remote_title".tr(),
subtitle: "advanced_settings_prefer_remote_subtitle".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
), ),
const LocalStorageSettings(), if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(),
SettingsSwitchListTile( SettingsSwitchListTile(
enabled: !isLoggedIn, enabled: !isLoggedIn,
valueNotifier: allowSelfSignedSSLCert, valueNotifier: allowSelfSignedSSLCert,
@@ -101,12 +102,13 @@ class AdvancedSettings extends HookConsumerWidget {
), ),
const CustomeProxyHeaderSettings(), const CustomeProxyHeaderSettings(),
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null), SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
SettingsSwitchListTile( if (!Store.isBetaTimelineEnabled)
valueNotifier: useAlternatePMFilter, SettingsSwitchListTile(
title: "advanced_settings_enable_alternate_media_filter_title".tr(), valueNotifier: useAlternatePMFilter,
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), title: "advanced_settings_enable_alternate_media_filter_title".tr(),
), subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
// TODO: Remove this check when beta timeline goes stable ),
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled) if (Store.isBetaTimelineEnabled)
SettingsSwitchListTile( SettingsSwitchListTile(
valueNotifier: readonlyModeEnabled, valueNotifier: readonlyModeEnabled,

View File

@@ -5,13 +5,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -83,7 +82,7 @@ class SyncStatusAndActions extends HookConsumerWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await resetDriftDatabase(ref.read(driftProvider)); await ref.read(driftProvider).reset();
context.pop(); context.pop();
context.scaffoldMessenger.showSnackBar( context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("reset_sqlite_success".t(context: context))), SnackBar(content: Text("reset_sqlite_success".t(context: context))),

View File

@@ -1,5 +1,3 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -12,50 +10,11 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
class BetaTimelineListTile extends ConsumerStatefulWidget { class BetaTimelineListTile extends ConsumerWidget {
const BetaTimelineListTile({super.key}); const BetaTimelineListTile({super.key});
@override @override
ConsumerState<BetaTimelineListTile> createState() => _BetaTimelineListTileState(); Widget build(BuildContext context, WidgetRef ref) {
}
class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
late Animation<double> _pulseAnimation;
late Animation<double> _gradientAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(duration: const Duration(seconds: 3), vsync: this);
_rotationAnimation = Tween<double>(
begin: 0,
end: 2 * math.pi,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
_pulseAnimation = Tween<double>(
begin: 1,
end: 1.1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_gradientAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.betaTimeline); final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.betaTimeline);
final serverInfo = ref.watch(serverInfoProvider); final serverInfo = ref.watch(serverInfoProvider);
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
@@ -64,168 +23,50 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> wit
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return AnimatedBuilder( void onSwitchChanged(bool value) {
animation: _animationController, showDialog(
builder: (context, child) { context: context,
void onSwitchChanged(bool value) { builder: (context) {
showDialog( return AlertDialog(
context: context, title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"),
builder: (context) { content: value
return AlertDialog( ? const Text("Are you sure you want to enable the beta timeline?")
title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"), : const Text("Are you sure you want to disable the beta timeline?"),
content: value actions: [
? const Text("Are you sure you want to enable the beta timeline?") TextButton(
: const Text("Are you sure you want to disable the beta timeline?"), onPressed: () {
actions: [ context.pop();
TextButton( },
onPressed: () { child: Text(
context.pop(); "cancel".t(context: context),
}, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline),
child: Text(
"cancel".t(context: context),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline),
),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
},
child: Text("ok".t(context: context)),
),
],
);
},
);
}
final gradientColors = [
Color.lerp(
context.primaryColor.withValues(alpha: 0.5),
context.primaryColor.withValues(alpha: 0.3),
_gradientAnimation.value,
)!,
Color.lerp(
context.logoPink.withValues(alpha: 0.2),
context.logoPink.withValues(alpha: 0.4),
_gradientAnimation.value,
)!,
Color.lerp(
context.logoRed.withValues(alpha: 0.3),
context.logoRed.withValues(alpha: 0.5),
_gradientAnimation.value,
)!,
];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient(
colors: gradientColors,
stops: const [0.0, 0.5, 1.0],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
transform: GradientRotation(_rotationAnimation.value * 0.5),
),
boxShadow: [
BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
color: context.scaffoldBackgroundColor,
),
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
onTap: () => onSwitchChanged(!betaTimelineValue),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
Transform.scale(
scale: _pulseAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value * 0.02,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.primaryColor.withValues(alpha: 0.2),
context.primaryColor.withValues(alpha: 0.1),
],
),
),
child: Icon(Icons.auto_awesome, color: context.primaryColor, size: 20),
),
),
),
const SizedBox(width: 28),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"advanced_settings_beta_timeline_title".t(context: context),
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
gradient: LinearGradient(
colors: [
context.primaryColor.withValues(alpha: 0.8),
context.primaryColor.withValues(alpha: 0.6),
],
),
),
child: Text(
'NEW',
style: context.textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
height: 1.2,
),
),
),
],
),
const SizedBox(height: 4),
Text(
"advanced_settings_beta_timeline_subtitle".t(context: context),
style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withValues(alpha: 0.9),
),
maxLines: 2,
),
],
),
),
Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,
activeColor: context.primaryColor,
),
],
),
), ),
), ),
), ElevatedButton(
), onPressed: () async {
); Navigator.of(context).pop();
}, context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
},
child: Text("ok".t(context: context)),
),
],
);
},
);
}
return Padding(
padding: const EdgeInsets.only(left: 4.0),
child: ListTile(
title: Text("advanced_settings_beta_timeline_title".t(context: context)),
subtitle: Text("advanced_settings_beta_timeline_subtitle".t(context: context)),
trailing: Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,
activeColor: context.primaryColor,
),
onTap: () => onSwitchChanged(!betaTimelineValue),
),
); );
} }
} }

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class SharedLinkItem extends ConsumerWidget { class SharedLinkItem extends ConsumerWidget {
final SharedLink sharedLink; final SharedLink sharedLink;
@@ -36,7 +37,7 @@ class SharedLinkItem extends ConsumerWidget {
return Text("expired", style: TextStyle(color: Colors.red[300])).tr(); return Text("expired", style: TextStyle(color: Colors.red[300])).tr();
} }
final difference = sharedLink.expiresAt!.difference(DateTime.now()); final difference = sharedLink.expiresAt!.difference(DateTime.now());
debugPrint("Difference: $difference"); dPrint(() => "Difference: $difference");
if (difference.inDays > 0) { if (difference.inDays > 0) {
var dayDifference = difference.inDays; var dayDifference = difference.inDays;
if (difference.inHours % 24 > 12) { if (difference.inHours % 24 > 12) {

View File

@@ -77,10 +77,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: background_downloader name: background_downloader
sha256: "2d4c2b7438e7643585880f9cc00ace16a52d778088751f1bfbf714627b315462" sha256: "9ed74c55750932178f6989ba8a659687c2a102e05b70f561a1b3f047a5dda790"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.2.0" version: "9.2.5"
bonsoir: bonsoir:
dependency: transitive dependency: transitive
description: description:

View File

@@ -16,7 +16,7 @@ dependencies:
async: ^2.11.0 async: ^2.11.0
auto_route: ^9.2.0 auto_route: ^9.2.0
background_downloader: ^9.2.0 background_downloader: ^9.2.5
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
cancellation_token_http: ^2.1.0 cancellation_token_http: ^2.1.0
cast: ^2.1.0 cast: ^2.1.0

View File

@@ -4,12 +4,15 @@ import 'dart:convert';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import '../../api.mocks.dart'; import '../../api.mocks.dart';
import '../../service.mocks.dart'; import '../../service.mocks.dart';
import '../../test_utils.dart';
class MockHttpClient extends Mock implements http.Client {} class MockHttpClient extends Mock implements http.Client {}
@@ -33,6 +36,10 @@ void main() {
late StreamController<List<int>> responseStreamController; late StreamController<List<int>> responseStreamController;
late int testBatchSize = 3; late int testBatchSize = 3;
setUpAll(() async {
await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar()));
});
setUp(() { setUp(() {
mockApiService = MockApiService(); mockApiService = MockApiService();
mockApiClient = MockApiClient(); mockApiClient = MockApiClient();

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/utils/throttle.dart'; import 'package:immich_mobile/utils/throttle.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class _Counter { class _Counter {
int _count = 0; int _count = 0;
@@ -8,7 +8,7 @@ class _Counter {
int get count => _count; int get count => _count;
void increment() { void increment() {
debugPrint("Counter inside increment: $count"); dPrint(() => "Counter inside increment: $count");
_count = _count + 1; _count = _count + 1;
} }
} }

View File

@@ -48,7 +48,7 @@
aria-label={$t('show_album_options')} aria-label={$t('show_album_options')}
icon={mdiDotsVertical} icon={mdiDotsVertical}
shape="round" shape="round"
variant="ghost" variant="filled"
size="medium" size="medium"
class="icon-white-drop-shadow" class="icon-white-drop-shadow"
onclick={showAlbumContextMenu} onclick={showAlbumContextMenu}

View File

@@ -45,15 +45,4 @@ describe('Thumbnail component', () => {
const tabbables = getTabbable(container!); const tabbables = getTabbable(container!);
expect(tabbables.length).toBe(0); expect(tabbables.length).toBe(0);
}); });
it('shows thumbhash while image is loading', () => {
const asset = assetFactory.build({ originalPath: 'image.jpg', originalMimeType: 'image/jpeg' });
const sut = render(Thumbnail, {
asset,
selected: true,
});
const thumbhash = sut.getByTestId('thumbhash');
expect(thumbhash).not.toBeFalsy();
});
}); });

View File

@@ -20,6 +20,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { isCached } from '$lib/utils/cache';
import { moveFocus } from '$lib/utils/focus-util'; import { moveFocus } from '$lib/utils/focus-util';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation'; import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
@@ -75,6 +76,12 @@
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION }, IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES; } = TUNABLES;
const thumbnailURL = getAssetThumbnailUrl({
id: asset.id,
size: AssetMediaSize.Thumbnail,
cacheKey: asset.thumbhash,
});
let usingMobileDevice = $derived(mobileDevice.pointerCoarse); let usingMobileDevice = $derived(mobileDevice.pointerCoarse);
let element: HTMLElement | undefined = $state(); let element: HTMLElement | undefined = $state();
let mouseOver = $state(false); let mouseOver = $state(false);
@@ -313,7 +320,7 @@
<ImageThumbnail <ImageThumbnail
class={imageClass} class={imageClass}
{brokenAssetClass} {brokenAssetClass}
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })} url={thumbnailURL}
altText={$getAltText(asset)} altText={$getAltText(asset)}
widthStyle="{width}px" widthStyle="{width}px"
heightStyle="{height}px" heightStyle="{height}px"
@@ -344,17 +351,31 @@
</div> </div>
{/if} {/if}
{#if (!loaded || thumbError) && asset.thumbhash} {#if asset.thumbhash}
<canvas {#await isCached(new Request(thumbnailURL))}
use:thumbhash={{ base64ThumbHash: asset.thumbhash }} <canvas
data-testid="thumbhash" use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
class="absolute top-0 object-cover" data-testid="thumbhash"
style:width="{width}px" class="absolute top-0 object-cover"
style:height="{height}px" style:width="{width}px"
class:rounded-xl={selected} style:height="{height}px"
draggable="false" class:rounded-xl={selected}
out:fade={{ duration: THUMBHASH_FADE_DURATION }} draggable="false"
></canvas> ></canvas>
{:then cached}
{#if !cached && !loaded && !thumbError}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
data-testid="thumbhash"
class="absolute top-0 object-cover"
style:width="{width}px"
style:height="{height}px"
class:rounded-xl={selected}
draggable="false"
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
></canvas>
{/if}
{/await}
{/if} {/if}
</div> </div>

View File

@@ -110,7 +110,7 @@
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} /> <svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<div <div
class="fixed top-0 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8" class="fixed top-0 z-1 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconButton <IconButton

View File

@@ -64,9 +64,10 @@
{#if showVerticalDots} {#if showVerticalDots}
<div class="absolute top-2 end-2 z-1"> <div class="absolute top-2 end-2 z-1">
<ButtonContextMenu <ButtonContextMenu
buttonClass="icon-white-drop-shadow focus:opacity-100 {showVerticalDots ? 'opacity-100' : 'opacity-0'}" buttonClass="icon-white-drop-shadow"
color="primary" color="secondary"
size="medium" size="medium"
variant="filled"
icon={mdiDotsVertical} icon={mdiDotsVertical}
title={$t('show_person_options')} title={$t('show_person_options')}
> >

View File

@@ -68,7 +68,7 @@
<div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark"> <div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
{#if title} {#if title}
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div> <div class="font-medium outline-none pe-8" tabindex="-1" id={headerId}>{title}</div>
{/if} {/if}
{#if description} {#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p> <p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>

View File

@@ -16,7 +16,7 @@
import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util'; import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { fly, scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
let { isUploading } = uploadAssetsStore; let { isUploading } = uploadAssetsStore;
@@ -169,10 +169,11 @@
class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm" class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
style:width={dayGroup.width + 'px'} style:width={dayGroup.width + 'px'}
> >
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))} {#if !singleSelect}
<div <div
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }} class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0"
class="inline-block pe-2 hover:cursor-pointer" class:w-8={(hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) ||
assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
> >

View File

@@ -40,6 +40,8 @@
// of zero when starting the 'slide' animation. // of zero when starting the 'slide' animation.
let height: number = $state(0); let height: number = $state(0);
let isTransitioned = $state(false);
$effect(() => { $effect(() => {
if (menuElement) { if (menuElement) {
let layoutDirection = direction; let layoutDirection = direction;
@@ -64,6 +66,12 @@
style:top="{top}px" style:top="{top}px"
transition:slide={{ duration: 250, easing: quintOut }} transition:slide={{ duration: 250, easing: quintOut }}
use:clickOutside={{ onOutclick: onClose }} use:clickOutside={{ onOutclick: onClose }}
onintroend={() => {
isTransitioned = true;
}}
onoutrostart={() => {
isTransitioned = false;
}}
> >
<ul <ul
{id} {id}
@@ -73,7 +81,9 @@
bind:this={menuElement} bind:this={menuElement}
class="{isVisible class="{isVisible
? 'max-h-dvh' ? 'max-h-dvh'
: 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none overflow-auto" : 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none {isTransitioned
? 'overflow-auto'
: ''}"
role="menu" role="menu"
tabindex="-1" tabindex="-1"
> >

View File

@@ -0,0 +1,18 @@
let cache: Cache | undefined;
const getCache = async () => {
cache ||= await openCache();
return cache;
};
const openCache = async () => {
const [key] = await caches.keys();
if (key) {
return caches.open(key);
}
};
export const isCached = async (req: Request) => {
const cache = await getCache();
return !!(await cache?.match(req));
};

View File

@@ -27,7 +27,7 @@ export class GCastDestination implements ICastDestination {
async initialize(): Promise<boolean> { async initialize(): Promise<boolean> {
const preferencesStore = get(preferences); const preferencesStore = get(preferences);
if (!preferencesStore.cast.gCastEnabled) { if (!preferencesStore || !preferencesStore.cast.gCastEnabled) {
this.isAvailable = false; this.isAvailable = false;
return false; return false;
} }

View File

@@ -29,6 +29,6 @@ export const TUNABLES = {
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false), NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
}, },
IMAGE_THUMBNAIL: { IMAGE_THUMBNAIL: {
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 100), THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 1000),
}, },
}; };