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:
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
# Show potential errors
# - avoid-cascade-after-if-null

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/translate_extensions.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/upload.service.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:isar/isar.dart';
import 'package:logging/logging.dart';
@@ -159,7 +162,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
try {
await _cleanup();
} 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
return;
}),
LogService.I.dispose(),
Store.dispose(),
_drift.close(),
_driftLogger.close(),
backgroundSyncManager.cancel(),
@@ -192,7 +197,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up");
} 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);
},
(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();
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();
}

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.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/store.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
@@ -66,13 +66,12 @@ class LogService {
}
void _handleLogRecord(LogRecord r) {
if (kDebugMode) {
debugPrint(
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
}
dPrint(
() =>
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
final record = LogMessage(
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/infrastructure/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPartnerService {
final DriftPartnerRepository _driftPartnerRepository;
@@ -30,7 +30,7 @@ class DriftPartnerService {
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
if (partner == null) {
debugPrint("Partner not found: $partnerId for user: $userId");
dPrint(() => "Partner not found: $partnerId for user: $userId");
return;
}

View File

@@ -10,7 +10,7 @@ class StoreService {
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
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
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
_instance ??= await create(storeRepository: storeRepository);
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
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);
await instance.populateCache();
instance._storeUpdateSubscription = instance._listenForChange();
if (listenUpdates) {
instance._storeUpdateSubscription = instance._listenForChange();
}
return instance;
}
@@ -50,8 +52,8 @@ class StoreService {
});
/// Disposes the store and cancels the subscription. To reuse the store call init() again
void dispose() async {
await _storeUpdateSubscription.cancel();
Future<void> dispose() async {
await _storeUpdateSubscription?.cancel();
_cache.clear();
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.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/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
@@ -100,7 +100,7 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
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: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);

View File

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

View File

@@ -100,8 +100,14 @@ class BackgroundSyncManager {
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true))
: runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false));
? runInIsolateGentle(
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!
.whenComplete(() {
@@ -122,7 +128,10 @@ class BackgroundSyncManager {
onHashingStart?.call();
_hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets());
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
debugLabel: 'hash-assets',
);
return _hashTask!
.whenComplete(() {
@@ -142,7 +151,10 @@ class BackgroundSyncManager {
onRemoteSyncStart?.call();
_syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync());
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
debugLabel: 'remote-sync',
);
return _syncTask!
.whenComplete(() {
onRemoteSyncComplete?.call();
@@ -169,7 +181,7 @@ class BackgroundSyncManager {
return _linkedAlbumSyncTask!.future;
}
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
return _linkedAlbumSyncTask!.whenComplete(() {
_linkedAlbumSyncTask = null;
});
@@ -178,4 +190,5 @@ class BackgroundSyncManager {
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
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:intl/message_format.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/debug_print.dart';
extension StringTranslateExtension on String {
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)
: translatedMessage;
} catch (e) {
debugPrint('Translation failed for key "$key". Error: $e');
dPrint(() => 'Translation failed for key "$key". Error: $e');
return key;
}
}

View File

@@ -69,6 +69,29 @@ class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: 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
int get schemaVersion => 10;

View File

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

View File

@@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
}
return _db.mergedAssetDrift
.mergedBucket(userIds: userIds, groupBy: groupBy.index)
.map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
})
.watch()
.throttle(const Duration(seconds: 3), trailing: true);
return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
}).watch();
}
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:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart';
import 'package:immich_mobile/utils/debug_print.dart';
void main() async {
ImmichWidgetsBinding();
@@ -69,9 +70,9 @@ Future<void> initApp() async {
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
debugPrint("Enabled high refresh mode");
dPrint(() => "Enabled high refresh mode");
} 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) {
switch (state) {
case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed");
dPrint(() => "[APP STATE] resumed");
ref.read(appStateProvider.notifier).handleAppResume();
break;
case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive");
dPrint(() => "[APP STATE] inactive");
ref.read(appStateProvider.notifier).handleAppInactivity();
break;
case AppLifecycleState.paused:
debugPrint("[APP STATE] paused");
dPrint(() => "[APP STATE] paused");
ref.read(appStateProvider.notifier).handleAppPause();
break;
case AppLifecycleState.detached:
debugPrint("[APP STATE] detached");
dPrint(() => "[APP STATE] detached");
ref.read(appStateProvider.notifier).handleAppDetached();
break;
case AppLifecycleState.hidden:
debugPrint("[APP STATE] hidden");
dPrint(() => "[APP STATE] hidden");
ref.read(appStateProvider.notifier).handleAppHidden();
break;
}
@@ -200,7 +201,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
@override
initState() {
super.initState();
initApp().then((_) => debugPrint("App Init Completed"));
initApp().then((_) => dPrint(() => "App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) {
@@ -239,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
routerConfig: router.config(
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).startListeningToBetaEvents();
await ref.read(driftProvider).reset();
await Store.put(StoreKey.shouldResetSync, true);
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
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/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_timeline_list_tile.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/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';
enum SettingSection {
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
@@ -28,14 +26,14 @@ enum SettingSection {
networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_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 subtitle;
final IconData icon;
Widget get widget => switch (this) {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup =>
@@ -45,6 +43,7 @@ enum SettingSection {
SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(),
SettingSection.beta => const SyncStatusAndActions(),
};
const SettingSection(this.title, this.icon, this.subtitle);
@@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget {
context.locale;
return Scaffold(
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(
(setting) => setting == SettingSection.beta
? [
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled)
SettingsCard(
icon: Icons.sync_outlined,
@@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget {
.toList();
return ListView(
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
padding: const EdgeInsets.only(top: 10.0, bottom: 16),
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()
class SettingsSubPage extends StatelessWidget {
const SettingsSubPage(this.section, {super.key});
@@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
return SafeArea(
bottom: true,
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/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@RoutePage()
class DriftPartnerDetailPage extends StatelessWidget {
@@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
_inTimeline = !_inTimeline;
});
} catch (error, stack) {
debugPrint("Failed to toggle in timeline: $error $stack");
dPrint(() => "Failed to toggle in timeline: $error $stack");
ImmichToast.show(
context: context,
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/providers/cast.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/readonly_mode.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/routing/router.dart';
@@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final previousRouteName = ref.watch(previousRouteNameProvider);
final tabRoute = ref.watch(tabProvider);
final showViewInTimelineButton =
previousRouteName != TabShellRoute.name &&
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name &&
previousRouteName != null;

View File

@@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(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) ...[
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: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/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_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/edit_date_time_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 EditLocationActionButton(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) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),

View File

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

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.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/domain/models/album/album.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/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_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/edit_date_time_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 EditLocationActionButton(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) ...[
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/widgets/common/immich_toast.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
context.pop<DateTime>(_selectedDate);
}
} catch (error) {
debugPrint('Error updating birthday: $error');
dPrint(() => 'Error updating birthday: $error');
if (!context.mounted) {
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/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
context.pop<String>(newName);
}
} catch (error) {
debugPrint('Error updating name: $error');
dPrint(() => 'Error updating name: $error');
if (!context.mounted) {
return;

View File

@@ -3,14 +3,15 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.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/extensions/build_context_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/segment.model.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:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
@@ -79,6 +80,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
double _thumbTopOffset = 0.0;
bool _isDragging = false;
List<_Segment> _segments = [];
int _monthCount = 0;
late AnimationController _thumbAnimationController;
Timer? _fadeOutTimer;
@@ -105,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_monthCount = getMonthCount();
_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) {
_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) {
if (_isDragging) {
// 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 _) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
}
setState(() {
_isDragging = true;
_labelAnimationController.forward();
@@ -191,13 +202,22 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
final label = nearestMonthSegment.scrollLabel;
if (_lastLabel != label) {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
_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

View File

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

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(
@@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
_log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace);
} catch (error, stackTrace) {
_log.severe("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");
}
dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
// 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:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.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:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier(
@@ -286,7 +285,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums);
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
Future<void> startBackupProcess() async {
debugPrint("Start backup process");
dPrint(() => "Start backup process");
assert(state.backupProgress == BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);

View File

@@ -4,7 +4,6 @@ import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.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/services/upload.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class EnqueueStatus {
final int enqueueCount;
@@ -329,16 +329,16 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
Future<void> cancel() async {
debugPrint("Canceling backup tasks...");
dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true);
final activeTaskCount = await _uploadService.cancelBackup();
if (activeTaskCount > 0) {
debugPrint("$activeTaskCount tasks left, continuing to cancel...");
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
await cancel();
} else {
debugPrint("All tasks canceled successfully.");
dPrint(() => "All tasks canceled successfully.");
// Clear all upload items when cancellation is complete
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:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier(
@@ -216,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
);
if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process");
dPrint(() => "[_startUpload] No Assets to upload - Abort Process");
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
return false;
}
@@ -294,10 +295,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
}
} else {
openAppSettings();
debugPrint("[_startUpload] Do not have permission to the gallery");
dPrint(() => "[_startUpload] Do not have permission to the gallery");
}
} catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}");
dPrint(() => "ERROR _startUpload: ${e.toString()}");
hasErrors = true;
} finally {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
@@ -340,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// waits until it has stopped to start the backup.
final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock();
if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting");
dPrint(() => "[uploadAssets] could not acquire lock, exiting");
ImmichToast.show(
context: context,
msg: "failed".tr(),
@@ -355,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// check if backup is already in process - then return
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
debugPrint("[uploadAssets] Manual upload is already running - abort");
dPrint(() => "[uploadAssets] Manual upload is already running - abort");
showInProgress = true;
}
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;
return false;
}
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
debugPrint("[uploadAssets] Background backup is running - abort");
dPrint(() => "[uploadAssets] Background backup is running - abort");
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/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode");
dPrint(() => "Current themeMode $themeMode");
if (themeMode == ThemeMode.light.name) {
return ThemeMode.light;
@@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
debugPrint("Current theme preset $primaryColorPreset");
dPrint(() => "Current theme preset $primaryColorPreset");
try {
return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset);
} 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);
return defaultColorPreset;
}

View File

@@ -1,10 +1,10 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/utils/debug_print.dart';
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());
if (profileImagePath != null) {
debugPrint("Successfully upload profile image");
dPrint(() => "Successfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath);
return true;
}

View File

@@ -2,8 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.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:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
@@ -105,7 +104,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
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
Socket socket = io(
endpoint.origin,
@@ -121,12 +120,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
);
socket.onConnect((_) {
debugPrint("Established Websocket Connection");
dPrint(() => "Established Websocket Connection");
state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges);
});
socket.onDisconnect((_) {
debugPrint("Disconnect to Websocket Connection");
dPrint(() => "Disconnect to Websocket Connection");
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_new_release', _handleReleaseUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
}
}
void disconnect() {
debugPrint("Attempting to disconnect from websocket");
dPrint(() => "Attempting to disconnect from websocket");
_batchedAssetUploadReady.clear();
@@ -200,7 +199,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
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);
}
@@ -321,10 +320,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
}
}),
);
} catch (error) {

View File

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

View File

@@ -1,5 +1,5 @@
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
class DuplicateGuard extends AutoRouteGuard {
@@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async {
// Duplicate navigation
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);
} else {
resolver.next(true);

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.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:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetServiceProvider = Provider(
(ref) => AssetService(
@@ -87,7 +87,7 @@ class AssetService {
getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets,
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -156,7 +156,7 @@ class AssetService {
if (a.isInDb) {
await _assetRepository.transaction(() => _assetRepository.update(a));
} 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:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.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/utils/backup_progress.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/http_ssl_options.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart';
@@ -165,7 +165,7 @@ class BackgroundService {
]);
}
} catch (error) {
debugPrint("[_updateNotification] failed to communicate with plugin");
dPrint(() => "[_updateNotification] failed to communicate with plugin");
}
return false;
}
@@ -177,7 +177,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]);
}
} catch (error) {
debugPrint("[_showErrorNotification] failed to communicate with plugin");
dPrint(() => "[_showErrorNotification] failed to communicate with plugin");
}
return false;
}
@@ -188,7 +188,7 @@ class BackgroundService {
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
}
} catch (error) {
debugPrint("[_clearErrorNotifications] failed to communicate with plugin");
dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin");
}
return false;
}
@@ -196,7 +196,7 @@ class BackgroundService {
/// await to ensure this thread (foreground or background) has exclusive access
Future<bool> acquireLock() async {
if (_hasLock) {
debugPrint("WARNING: [acquireLock] called more than once");
dPrint(() => "WARNING: [acquireLock] called more than once");
return true;
}
final int lockTime = Timeline.now;
@@ -302,19 +302,19 @@ class BackgroundService {
final bool hasAccess = await waitForLock;
if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting");
dPrint(() => "[_callHandler] could not acquire lock, exiting");
return false;
}
final translationsOk = await loadTranslations();
if (!translationsOk) {
debugPrint("[_callHandler] could not load translations");
dPrint(() => "[_callHandler] could not load translations");
}
final bool ok = await _onAssetsChanged();
return ok;
} catch (error) {
debugPrint(error.toString());
dPrint(() => error.toString());
return false;
} finally {
releaseLock();
@@ -324,14 +324,14 @@ class BackgroundService {
_cancellationToken?.cancel();
return true;
default:
debugPrint("Unknown method ${call.method}");
dPrint(() => "Unknown method ${call.method}");
return false;
}
}
Future<bool> _onAssetsChanged() async {
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(
overrides: [
@@ -344,9 +344,7 @@ class BackgroundService {
HttpSSLOptions.apply();
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
if (kDebugMode) {
debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
}
dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select);
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:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.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:permission_handler/permission_handler.dart' as pm;
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final backupServiceProvider = Provider(
(ref) => BackupService(
@@ -69,7 +69,7 @@ class BackupService {
try {
return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId);
} catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}');
return null;
}
}
@@ -356,8 +356,9 @@ class BackupService {
final error = responseBody;
final errorMessage = error['message'] ?? error['error'];
debugPrint(
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
dPrint(
() =>
"Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
);
onError(
@@ -398,11 +399,11 @@ class BackupService {
}
}
} on http.CancelledException {
debugPrint("Backup was cancelled by the user");
dPrint(() => "Backup was cancelled by the user");
anyErrors = true;
break;
} catch (error, stackTrace) {
debugPrint("Error backup asset: ${error.toString()}: $stackTrace");
dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace");
anyErrors = true;
continue;
} finally {
@@ -411,7 +412,7 @@ class BackupService {
await file?.delete();
await livePhotoFile?.delete();
} 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)) {
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;

View File

@@ -1,9 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final localNotificationService = Provider(
(ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref),
@@ -110,7 +110,7 @@ class LocalNotificationService {
switch (notificationResponse.actionId) {
case cancelUploadActionID:
{
debugPrint("User cancelled manual upload operation");
dPrint(() => "User cancelled manual upload operation");
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/localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/locales.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
Future<bool> loadTranslations() async {
@@ -17,7 +17,7 @@ Future<bool> loadTranslations() async {
assetLoader: const CodegenLoader(),
path: translationsPath,
useOnlyLangCode: false,
onLoadError: (e) => debugPrint(e.toString()),
onLoadError: (e) => dPrint(() => e.toString()),
fallbackLocale: locales.values.first,
);

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/string_extensions.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:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final searchServiceProvider = Provider(
(ref) => SearchService(
@@ -43,7 +43,7 @@ class SearchService {
model: model,
);
} catch (e) {
debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}");
dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}");
return [];
}
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.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_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/providers/api.provider.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)));
@@ -30,7 +30,7 @@ class ServerInfoService {
return ServerDiskInfo.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getDiskInfo] ${e.toString()}");
dPrint(() => "Error [getDiskInfo] ${e.toString()}");
}
return null;
}
@@ -42,7 +42,7 @@ class ServerInfoService {
return ServerVersion.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerVersion] ${e.toString()}");
dPrint(() => "Error [getServerVersion] ${e.toString()}");
}
return null;
}
@@ -54,7 +54,7 @@ class ServerInfoService {
return ServerFeatures.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerFeatures] ${e.toString()}");
dPrint(() => "Error [getServerFeatures] ${e.toString()}");
}
return null;
}
@@ -66,7 +66,7 @@ class ServerInfoService {
return ServerConfig.fromDto(dto);
}
} catch (e) {
debugPrint("Error [getServerConfig] ${e.toString()}");
dPrint(() => "Error [getServerConfig] ${e.toString()}");
}
return null;
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class StackService {
const StackService(this._api, this._assetRepository);
@@ -16,7 +16,7 @@ class StackService {
try {
return _api.stacksApi.getStack(stackId);
} catch (error) {
debugPrint("Error while fetching stack: $error");
dPrint(() => "Error while fetching stack: $error");
}
return null;
}
@@ -25,7 +25,7 @@ class StackService {
try {
return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds));
} catch (error) {
debugPrint("Error while creating stack: $error");
dPrint(() => "Error while creating stack: $error");
}
return null;
}
@@ -34,7 +34,7 @@ class StackService {
try {
return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId));
} catch (error) {
debugPrint("Error while updating stack children: $error");
dPrint(() => "Error while updating stack children: $error");
}
return null;
}
@@ -54,7 +54,7 @@ class StackService {
}
await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets));
} 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:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.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/infrastructure/asset.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/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:immich_mobile/utils/debug_print.dart';
final uploadServiceProvider = Provider((ref) {
final service = UploadService(
@@ -31,7 +30,6 @@ final uploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider),
);
ref.onDispose(service.dispose);
@@ -45,7 +43,6 @@ class UploadService {
this._storageRepository,
this._localAssetRepository,
this._appSettingsService,
this._assetMediaRepository,
) {
_uploadRepository.onUploadStatus = _onUploadCallback;
_uploadRepository.onTaskProgress = _onTaskProgressCallback;
@@ -56,7 +53,6 @@ class UploadService {
final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('UploadService');
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
@@ -257,7 +253,7 @@ class UploadService {
enqueueTasks([uploadTask]);
} 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;
}
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
String metadata = UploadTaskMetadata(
localAssetId: asset.id,
@@ -364,13 +359,12 @@ class UploadService {
final fields = {'livePhotoVideoId': livePhotoVideoId};
final requiresWiFi = _shouldRequireWiFi(asset);
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
return buildUploadTask(
file,
createdAt: asset.createdAt,
modifiedAt: asset.updatedAt,
originalFileName: originalFileName,
originalFileName: asset.name,
deviceAssetId: asset.id,
fields: fields,
group: kBackupLivePhotoGroup,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/debug_print.dart';
abstract final class DynamicTheme {
const DynamicTheme._();
@@ -13,7 +14,7 @@ abstract final class DynamicTheme {
final corePalette = await DynamicColorPlugin.getCorePalette();
if (corePalette != null) {
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,
// so we regenerate all colors using the primary color
@@ -23,7 +24,7 @@ abstract final class DynamicTheme {
);
}
} 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);
}
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 IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
await StoreService.init(storeRepository: storeRepo);
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await LogService.init(
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:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.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:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -37,7 +38,7 @@ Cancelable<T?> runInIsolateGentle<T>({
DartPluginRegistrant.ensureInitialized();
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(
overrides: [
// TODO: Remove once isar is removed
@@ -61,6 +62,7 @@ Cancelable<T?> runInIsolateGentle<T>({
try {
ref.dispose();
await Store.dispose();
await LogService.I.dispose();
await logDb.close();
await drift.close();
@@ -71,10 +73,10 @@ Cancelable<T?> runInIsolateGentle<T>({
await isar.close();
}
} catch (e) {
debugPrint("Error closing Isar: $e");
dPrint(() => "Error closing Isar: $e");
}
} catch (error, stack) {
debugPrint("Error closing resources in isolate: $error, $stack");
dPrint(() => "Error closing resources in isolate: $error, $stack");
} finally {
ref.dispose();
// Delay to ensure all resources are released
@@ -84,7 +86,7 @@ Cancelable<T?> runInIsolateGentle<T>({
return null;
},
(error, stack) {
debugPrint("Error in isolate zone: $error, $stack");
dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
},
);
return null;

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:collection/collection.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/store.model.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/user.entity.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:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 15;
const int targetVersion = 16;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
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.betaTimeline, true);
} else {
await resetDriftDatabase(drift);
await drift.reset();
await Store.put(StoreKey.needBetaMigration, true);
}
}
if (version < 16) {
await SyncStreamRepository(drift).reset();
await Store.put(StoreKey.shouldResetSync, true);
}
if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion);
return;
@@ -117,7 +123,7 @@ Future<bool> _isNewInstallation(Isar db, Drift drift) async {
return true;
} catch (error) {
debugPrint("[MIGRATION] Error checking if new installation: $error");
dPrint(() => "[MIGRATION] Error checking if new installation: $error");
return false;
}
}
@@ -143,10 +149,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (!ps.hasAccess) {
if (kDebugMode) {
debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
}
dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
return;
}
@@ -166,8 +169,8 @@ Future<void> _migrateDeviceAsset(Isar db) async {
localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList();
}
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}");
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}");
dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}");
dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}");
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
final List<DeviceAssetEntity> toAdd = [];
@@ -182,20 +185,14 @@ Future<void> _migrateDeviceAsset(Isar db) async {
return false;
},
onlyFirst: (deviceAsset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
},
onlySecond: (asset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
},
);
if (kDebugMode) {
debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
}
dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd);
@@ -215,7 +212,7 @@ Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
}
});
} 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) {
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) {
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);
});
} 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});
}
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:maplibre_gl/maplibre_gl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ExifMap extends StatelessWidget {
final ExifInfo exifInfo;
@@ -66,7 +67,7 @@ class ExifMap extends StatelessWidget {
return;
}
debugPrint('Opening Map Uri: $uri');
dPrint(() => 'Opening Map Uri: $uri');
launchUrl(uri);
},
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/utils/hooks/app_settings_update_hook.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/local_storage_settings.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(),
subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
),
const LocalStorageSettings(),
if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(),
SettingsSwitchListTile(
enabled: !isLoggedIn,
valueNotifier: allowSelfSignedSSLCert,
@@ -101,12 +102,13 @@ class AdvancedSettings extends HookConsumerWidget {
),
const CustomeProxyHeaderSettings(),
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
SettingsSwitchListTile(
valueNotifier: useAlternatePMFilter,
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
if (!Store.isBetaTimelineEnabled)
SettingsSwitchListTile(
valueNotifier: useAlternatePMFilter,
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
),
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled)
SettingsSwitchListTile(
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/translate_extensions.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/asset.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/storage.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:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
@@ -83,7 +82,7 @@ class SyncStatusAndActions extends HookConsumerWidget {
),
TextButton(
onPressed: () async {
await resetDriftDatabase(ref.read(driftProvider));
await ref.read(driftProvider).reset();
context.pop();
context.scaffoldMessenger.showSnackBar(
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:flutter/foundation.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/services/app_settings.service.dart';
class BetaTimelineListTile extends ConsumerStatefulWidget {
class BetaTimelineListTile extends ConsumerWidget {
const BetaTimelineListTile({super.key});
@override
ConsumerState<BetaTimelineListTile> createState() => _BetaTimelineListTileState();
}
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) {
Widget build(BuildContext context, WidgetRef ref) {
final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.betaTimeline);
final serverInfo = ref.watch(serverInfoProvider);
final auth = ref.watch(authProvider);
@@ -64,168 +23,50 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> wit
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
void onSwitchChanged(bool value) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"),
content: value
? const Text("Are you sure you want to enable the beta timeline?")
: const Text("Are you sure you want to disable the beta timeline?"),
actions: [
TextButton(
onPressed: () {
context.pop();
},
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,
),
],
),
void onSwitchChanged(bool value) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"),
content: value
? const Text("Are you sure you want to enable the beta timeline?")
: const Text("Are you sure you want to disable the beta timeline?"),
actions: [
TextButton(
onPressed: () {
context.pop();
},
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)),
),
],
);
},
);
}
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/immich_toast.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class SharedLinkItem extends ConsumerWidget {
final SharedLink sharedLink;
@@ -36,7 +37,7 @@ class SharedLinkItem extends ConsumerWidget {
return Text("expired", style: TextStyle(color: Colors.red[300])).tr();
}
final difference = sharedLink.expiresAt!.difference(DateTime.now());
debugPrint("Difference: $difference");
dPrint(() => "Difference: $difference");
if (difference.inDays > 0) {
var dayDifference = difference.inDays;
if (difference.inHours % 24 > 12) {

View File

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

View File

@@ -16,7 +16,7 @@ dependencies:
async: ^2.11.0
auto_route: ^9.2.0
background_downloader: ^9.2.0
background_downloader: ^9.2.5
cached_network_image: ^3.4.1
cancellation_token_http: ^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:http/http.dart' as http;
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:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
import '../../api.mocks.dart';
import '../../service.mocks.dart';
import '../../test_utils.dart';
class MockHttpClient extends Mock implements http.Client {}
@@ -33,6 +36,10 @@ void main() {
late StreamController<List<int>> responseStreamController;
late int testBatchSize = 3;
setUpAll(() async {
await StoreService.init(storeRepository: IsarStoreRepository(await TestUtils.initIsar()));
});
setUp(() {
mockApiService = MockApiService();
mockApiClient = MockApiClient();

View File

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

View File

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

View File

@@ -45,15 +45,4 @@ describe('Thumbnail component', () => {
const tabbables = getTabbable(container!);
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 type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { isCached } from '$lib/utils/cache';
import { moveFocus } from '$lib/utils/focus-util';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { TUNABLES } from '$lib/utils/tunables';
@@ -75,6 +76,12 @@
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
const thumbnailURL = getAssetThumbnailUrl({
id: asset.id,
size: AssetMediaSize.Thumbnail,
cacheKey: asset.thumbhash,
});
let usingMobileDevice = $derived(mobileDevice.pointerCoarse);
let element: HTMLElement | undefined = $state();
let mouseOver = $state(false);
@@ -313,7 +320,7 @@
<ImageThumbnail
class={imageClass}
{brokenAssetClass}
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
url={thumbnailURL}
altText={$getAltText(asset)}
widthStyle="{width}px"
heightStyle="{height}px"
@@ -344,17 +351,31 @@
</div>
{/if}
{#if (!loaded || thumbError) && asset.thumbhash}
<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 asset.thumbhash}
{#await isCached(new Request(thumbnailURL))}
<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"
></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}
</div>

View File

@@ -110,7 +110,7 @@
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<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">
<IconButton

View File

@@ -64,9 +64,10 @@
{#if showVerticalDots}
<div class="absolute top-2 end-2 z-1">
<ButtonContextMenu
buttonClass="icon-white-drop-shadow focus:opacity-100 {showVerticalDots ? 'opacity-100' : 'opacity-0'}"
color="primary"
buttonClass="icon-white-drop-shadow"
color="secondary"
size="medium"
variant="filled"
icon={mdiDotsVertical}
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="flex gap-2 items-center">
{#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 description}
<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 type { Snippet } from 'svelte';
import { flip } from 'svelte/animate';
import { fly, scale } from 'svelte/transition';
import { scale } from 'svelte/transition';
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"
style:width={dayGroup.width + 'px'}
>
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))}
{#if !singleSelect}
<div
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
class="inline-block pe-2 hover:cursor-pointer"
class="hover:cursor-pointer transition-all duration-200 ease-out overflow-hidden w-0"
class:w-8={(hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) ||
assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
onclick={() => 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.
let height: number = $state(0);
let isTransitioned = $state(false);
$effect(() => {
if (menuElement) {
let layoutDirection = direction;
@@ -64,6 +66,12 @@
style:top="{top}px"
transition:slide={{ duration: 250, easing: quintOut }}
use:clickOutside={{ onOutclick: onClose }}
onintroend={() => {
isTransitioned = true;
}}
onoutrostart={() => {
isTransitioned = false;
}}
>
<ul
{id}
@@ -73,7 +81,9 @@
bind:this={menuElement}
class="{isVisible
? '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"
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> {
const preferencesStore = get(preferences);
if (!preferencesStore.cast.gCastEnabled) {
if (!preferencesStore || !preferencesStore.cast.gCastEnabled) {
this.isAvailable = 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),
},
IMAGE_THUMBNAIL: {
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 100),
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 1000),
},
};