refactor: yeet old timeline (#27666)

* refactor: yank old timeline

# Conflicts:
#	mobile/lib/presentation/pages/editing/drift_edit.page.dart
#	mobile/lib/providers/websocket.provider.dart
#	mobile/lib/routing/router.dart

* more cleanup

* remove native code

* chore: bump sqlite-data version

* remove old background tasks from BGTaskSchedulerPermittedIdentifiers

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-04-15 23:00:27 +05:30
committed by GitHub
parent 6dd6053222
commit 79fccdbee0
367 changed files with 332 additions and 50870 deletions
-83
View File
@@ -1,83 +0,0 @@
import 'dart:async';
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
final NumberFormat numberFormat = NumberFormat("###0.##");
String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) {
final int percent = (uploadedAssets * 100) ~/ assetsToUpload;
return "$percent% ($uploadedAssets/$assetsToUpload)";
}
/// prints progress in useful (kilo/mega/giga)bytes
String humanReadableFileBytesProgress(int bytes, int bytesTotal) {
String unit = "KB";
if (bytesTotal >= 0x40000000) {
unit = "GB";
bytes >>= 20;
bytesTotal >>= 20;
} else if (bytesTotal >= 0x100000) {
unit = "MB";
bytes >>= 10;
bytesTotal >>= 10;
} else if (bytesTotal < 0x400) {
return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B";
}
return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit";
}
/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes
String humanReadableBytesProgress(int bytes, int bytesTotal) {
String unit = "KB"; // Kilobyte
if (bytesTotal >= 0x40000000) {
unit = "GB"; // Gigabyte
bytes >>= 20;
bytesTotal >>= 20;
} else if (bytesTotal >= 0x100000) {
unit = "MB"; // Megabyte
bytes >>= 10;
bytesTotal >>= 10;
} else if (bytesTotal < 0x400) {
return "$bytes / $bytesTotal B";
}
final int percent = (bytes * 100) ~/ bytesTotal;
final String done = numberFormat.format(bytes / 1024.0);
final String total = numberFormat.format(bytesTotal / 1024.0);
return "$percent% ($done/$total$unit)";
}
class ThrottleProgressUpdate {
ThrottleProgressUpdate(this._fun, Duration interval) : _interval = interval.inMicroseconds;
final void Function(String?, int, int) _fun;
final int _interval;
int _invokedAt = 0;
Timer? _timer;
String? title;
int progress = 0;
int total = 0;
void call({final String? title, final int progress = 0, final int total = 0}) {
final time = Timeline.now;
this.title = title ?? this.title;
this.progress = progress;
this.total = total;
if (time > _invokedAt + _interval) {
_timer?.cancel();
_onTimeElapsed();
} else {
_timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed);
}
}
void _onTimeElapsed() {
_invokedAt = Timeline.now;
_fun(title, progress, total);
_timer = null;
// clear title to not send/overwrite it next time if unchanged
title = null;
}
}
+6 -57
View File
@@ -1,30 +1,14 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
void configureFileDownloaderNotifications() {
FileDownloader().configureNotificationForGroup(
@@ -57,48 +41,10 @@ void configureFileDownloaderNotifications() {
}
abstract final class Bootstrap {
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async {
static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async {
final drift = Drift();
final logDb = DriftLogger();
Isar? isar = Isar.getInstance();
if (isar != null) {
return (isar, drift, logDb);
}
final dir = await getApplicationDocumentsDirectory();
isar = await Isar.open(
[
StoreValueSchema,
AssetSchema,
AlbumSchema,
ExifInfoSchema,
UserSchema,
BackupAlbumSchema,
DuplicatedAssetSchema,
ETagSchema,
if (Platform.isAndroid) AndroidDeviceAssetSchema,
if (Platform.isIOS) IOSDeviceAssetSchema,
DeviceAssetEntitySchema,
],
directory: dir.path,
maxSizeMiB: 2048,
inspector: kDebugMode,
);
return (isar, drift, logDb);
}
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);
final DriftStoreRepository storeRepo = DriftStoreRepository(drift);
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
@@ -109,5 +55,8 @@ abstract final class Bootstrap {
);
await NetworkRepository.init();
// Remove once all asset operations are migrated to Native APIs
await PhotoManager.setIgnorePermissionCheck(true);
return (drift, logDb);
}
}
@@ -1,99 +0,0 @@
import 'package:flutter/widgets.dart';
class InvertionFilter extends StatelessWidget {
final Widget? child;
const InvertionFilter({super.key, this.child});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, //
0, -1, 0, 0, 255, //
0, 0, -1, 0, 255, //
0, 0, 0, 1, 0, //
]),
child: child,
);
}
}
// -1 - darkest, 1 - brightest, 0 - unchanged
class BrightnessFilter extends StatelessWidget {
final Widget? child;
final double brightness;
const BrightnessFilter({super.key, this.child, this.brightness = 0});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix(brightness)),
child: child,
);
}
}
// -1 - greyscale, 1 - most saturated, 0 - unchanged
class SaturationFilter extends StatelessWidget {
final Widget? child;
final double saturation;
const SaturationFilter({super.key, this.child, this.saturation = 0});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix(saturation)),
child: child,
);
}
}
class _ColorFilterGenerator {
static List<double> brightnessAdjustMatrix(double value) {
value = value * 10;
if (value == 0) {
return [
1, 0, 0, 0, 0, //
0, 1, 0, 0, 0, //
0, 0, 1, 0, 0, //
0, 0, 0, 1, 0, //
];
}
return List<double>.from(<double>[
1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, //
]).map((i) => i.toDouble()).toList();
}
static List<double> saturationAdjustMatrix(double value) {
value = value * 100;
if (value == 0) {
return [
1, 0, 0, 0, 0, //
0, 1, 0, 0, 0, //
0, 0, 1, 0, 0, //
0, 0, 0, 1, 0, //
];
}
double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble();
double lumR = 0.3086;
double lumG = 0.6094;
double lumB = 0.082;
return List<double>.from(<double>[
(lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), //
0, 0, //
lumR * (1 - x), //
(lumG * (1 - x)) + x, //
lumB * (1 - x), //
0, 0, //
lumR * (1 - x), //
lumG * (1 - x), //
(lumB * (1 - x)) + x, //
0, 0, 0, 0, 0, 1, 0, //
]).map((i) => i.toDouble()).toList();
}
}
@@ -1,2 +0,0 @@
bool isAtSameMomentAs(DateTime? a, DateTime? b) =>
(a == null && b == null) || ((a != null && b != null) && a.isAtSameMomentAs(b));
+1 -11
View File
@@ -1,20 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:thumbhash/thumbhash.dart' as thumbhash;
ObjectRef<Uint8List?> useBlurHashRef(Asset? asset) {
if (asset?.thumbhash == null) {
return useRef(null);
}
final rbga = thumbhash.thumbHashToRGBA(base64Decode(asset!.thumbhash!));
return useRef(thumbhash.rgbaToBmp(rbga));
}
ObjectRef<Uint8List?> useDriftBlurHashRef(RemoteAsset? asset) {
if (asset?.thumbHash == null) {
return useRef(null);
-40
View File
@@ -1,47 +1,7 @@
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:openapi/api.dart';
String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
return getThumbnailUrlForRemoteId(asset.remoteId!, type: type);
}
String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type);
}
String getThumbnailCacheKeyForRemoteId(
final String id,
final String thumbhash, {
AssetMediaSize type = AssetMediaSize.thumbnail,
}) {
if (type == AssetMediaSize.thumbnail) {
return 'thumbnail-image-$id-$thumbhash';
} else {
return '${id}_${thumbhash}_previewStage';
}
}
String getAlbumThumbnailUrl(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!, type: type);
}
String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return getThumbnailCacheKeyForRemoteId(
album.thumbnail.value!.remoteId!,
album.thumbnail.value!.thumbhash!,
type: type,
);
}
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited';
}
@@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
final _loadingEntry = OverlayEntry(
builder: (context) => SizedBox.square(
dimension: double.infinity,
child: DecoratedBox(
decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
child: const Center(
child: DelayedLoadingIndicator(delay: Duration(seconds: 1), fadeInDuration: Duration(milliseconds: 400)),
),
),
),
);
ValueNotifier<bool> useProcessingOverlay() {
return use(const _LoadingOverlay());
}
class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
const _LoadingOverlay();
@override
_LoadingOverlayState createState() => _LoadingOverlayState();
}
class _LoadingOverlayState extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
late final _isLoading = ValueNotifier(false)..addListener(_listener);
OverlayEntry? _loadingOverlay;
void _listener() {
setState(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isLoading.value) {
_loadingOverlay?.remove();
_loadingOverlay = _loadingEntry;
Overlay.of(context).insert(_loadingEntry);
} else {
_loadingOverlay?.remove();
_loadingOverlay = null;
}
});
});
}
@override
ValueNotifier<bool> build(BuildContext context) {
return _isLoading;
}
@override
void dispose() {
_isLoading.dispose();
super.dispose();
}
@override
Object? get debugValue => _isLoading.value;
@override
String get debugLabel => 'useProcessingOverlay<>';
}
+1 -15
View File
@@ -5,7 +5,6 @@ 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';
@@ -38,13 +37,9 @@ Cancelable<T?> runInIsolateGentle<T>({
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer(
overrides: [
// TODO: Remove once isar is removed
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
cancellationProvider.overrideWithValue(cancelledChecker),
driftProvider.overrideWith(driftOverride(drift)),
],
@@ -66,15 +61,6 @@ Cancelable<T?> runInIsolateGentle<T>({
await LogService.I.dispose();
await logDb.close();
await drift.close();
// Close Isar safely
try {
if (isar.isOpen) {
await isar.close();
}
} catch (e) {
dPrint(() => "Error closing Isar: $e");
}
} catch (error, stack) {
dPrint(() => "Error closing resources in isolate: $error, $stack");
} finally {
+3 -463
View File
@@ -1,115 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart' as isar_backup_album;
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
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/platform/native_sync_api.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/platform/network_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 25;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null;
Future<void> migrateDatabaseIfNeeded() async {
final int version = Store.get(StoreKey.version, targetVersion);
if (version < 9) {
await Store.put(StoreKey.version, targetVersion);
final value = await db.storeValues.get(StoreKey.currentUser.id);
if (value != null) {
final id = value.intValue;
if (id != null) {
await db.writeTxn(() async {
final user = await db.users.get(id);
await db.storeValues.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id));
});
}
}
}
if (version < 10) {
await Store.put(StoreKey.version, targetVersion);
await _migrateDeviceAsset(db);
}
if (version < 13) {
await Store.put(StoreKey.photoManagerCustomFilter, true);
}
// This means that the SQLite DB is just created and has no version
if (version < 14 || !hasVersion) {
await migrateStoreToSqlite(db, drift);
await Store.populateCache();
}
final syncStreamRepository = SyncStreamRepository(drift);
await handleBetaMigration(version, await _isNewInstallation(db, drift), syncStreamRepository);
if (version < 17 && Store.isBetaTimelineEnabled) {
final delay = Store.get(StoreKey.backupTriggerDelay, AppSettingsEnum.backupTriggerDelay.defaultValue);
if (delay >= 1000) {
await Store.put(StoreKey.backupTriggerDelay, (delay / 1000).toInt());
}
}
if (version < 18 && Store.isBetaTimelineEnabled) {
await syncStreamRepository.reset();
await Store.put(StoreKey.shouldResetSync, true);
}
if (version < 19 && Store.isBetaTimelineEnabled) {
if (!await _populateLocalAssetTime(drift)) {
return;
}
}
if (version < 20 && Store.isBetaTimelineEnabled) {
await _syncLocalAlbumIsIosSharedAlbum(drift);
}
if (version < 21) {
final certData = SSLClientCertStoreVal.load();
if (certData != null) {
await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? ""));
}
}
if (version < 23 && Store.isBetaTimelineEnabled) {
await _populateLocalAssetPlaybackStyle(drift);
}
if (version < 24 && Store.isBetaTimelineEnabled) {
await _applyLocalAssetOrientation(drift);
}
if (version < 25) {
final accessToken = Store.tryGet(StoreKey.accessToken);
@@ -121,365 +20,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
}
}
if (version < 22 && !Store.isBetaTimelineEnabled) {
await Store.put(StoreKey.needBetaMigration, true);
}
if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion);
return;
}
final shouldTruncate = version < 8 || version < targetVersion;
if (shouldTruncate) {
await _migrateTo(db, targetVersion);
}
}
Future<void> handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async {
// Handle migration only for this version
// TODO: remove when old timeline is removed
final isBeta = Store.tryGet(StoreKey.betaTimeline);
final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration);
if (version <= 15 && needBetaMigration == null) {
// For new installations, no migration needed
// For existing installations, only migrate if beta timeline is not enabled (null or false)
if (isNewInstallation || isBeta == true) {
await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true);
} else {
await Store.put(StoreKey.needBetaMigration, true);
}
}
if (version > 15) {
if (isBeta == null || isBeta) {
await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true);
} else {
await Store.put(StoreKey.needBetaMigration, false);
}
}
if (version < 16) {
await syncStreamRepository.reset();
await Store.put(StoreKey.shouldResetSync, true);
}
}
Future<bool> _isNewInstallation(Isar db, Drift drift) async {
try {
final isarUserCount = await db.users.count();
if (isarUserCount > 0) {
return false;
}
final isarAssetCount = await db.assets.count();
if (isarAssetCount > 0) {
return false;
}
final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length);
if (driftStoreCount > 0) {
return false;
}
final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length);
if (driftAssetCount > 0) {
return false;
}
return true;
} catch (error) {
dPrint(() => "[MIGRATION] Error checking if new installation: $error");
return false;
}
}
Future<void> _migrateTo(Isar db, int version) async {
await Store.delete(StoreKey.assetETag);
await db.writeTxn(() async {
await db.assets.clear();
await db.exifInfos.clear();
await db.albums.clear();
await db.eTags.clear();
await db.users.clear();
});
await Store.put(StoreKey.version, version);
}
Future<void> _migrateDeviceAsset(Isar db) async {
final ids = Platform.isAndroid
? (await db.androidDeviceAssets.where().findAll())
.map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash))
.toList()
: (await db.iOSDeviceAssets.where().findAll()).map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)).toList();
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (!ps.hasAccess) {
dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
return;
}
List<_DeviceAsset> localAssets = [];
final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList(onlyAll: true);
if (paths.isEmpty) {
localAssets = (await db.assets.where().anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)).findAll())
.map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt))
.toList();
} else {
final AssetPathEntity albumWithAll = paths.first;
final int assetCount = await albumWithAll.assetCountAsync;
final List<AssetEntity> allDeviceAssets = await albumWithAll.getAssetListRange(start: 0, end: assetCount);
localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList();
}
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 = [];
await diffSortedLists(
ids,
localAssets,
compare: (a, b) => a.assetId.compareTo(b.assetId),
both: (deviceAsset, asset) {
toAdd.add(
DeviceAssetEntity(assetId: deviceAsset.assetId, hash: deviceAsset.hash!, modifiedTime: asset.dateTime!),
);
return false;
},
onlyFirst: (deviceAsset) {
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
},
onlySecond: (asset) {
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
},
);
dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd);
});
}
Future<bool> _populateLocalAssetTime(Drift db) async {
try {
final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums();
for (final album in albums) {
final assets = await nativeApi.getAssetsForAlbum(album.id);
await db.batch((batch) async {
for (final asset in assets) {
batch.update(
db.localAssetEntity,
LocalAssetEntityCompanion(
longitude: Value(asset.longitude),
latitude: Value(asset.latitude),
adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)),
updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()),
),
where: (t) => t.id.equals(asset.id),
);
}
});
}
return true;
} catch (error) {
dPrint(() => "[MIGRATION] Error while populating asset time: $error");
return false;
}
}
Future<void> _syncLocalAlbumIsIosSharedAlbum(Drift db) async {
try {
final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums();
await db.batch((batch) {
for (final album in albums) {
batch.update(
db.localAlbumEntity,
LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)),
where: (t) => t.id.equals(album.id),
);
}
});
dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums");
} catch (error) {
dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error");
}
}
Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try {
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
await drift.batch((batch) {
for (final deviceAsset in isarDeviceAssets) {
batch.update(
drift.localAssetEntity,
LocalAssetEntityCompanion(checksum: Value(base64.encode(deviceAsset.hash))),
where: (t) => t.id.equals(deviceAsset.assetId),
);
}
});
} catch (error) {
dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error");
}
}
Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
try {
final isarBackupAlbums = await db.backupAlbums.where().findAll();
// Recents is a virtual album on Android, and we don't have it with the new sync
// If recents is selected previously, select all albums during migration except the excluded ones
if (Platform.isAndroid) {
final recentAlbum = isarBackupAlbums.firstWhereOrNull((album) => album.id == 'isAll');
if (recentAlbum != null) {
await drift.localAlbumEntity.update().write(
const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.selected)),
);
final excluded = isarBackupAlbums
.where((album) => album.selection == isar_backup_album.BackupSelection.exclude)
.map((album) => album.id)
.toList();
await drift.batch((batch) async {
for (final id in excluded) {
batch.update(
drift.localAlbumEntity,
const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)),
where: (t) => t.id.equals(id),
);
}
});
return;
}
}
await drift.batch((batch) {
for (final album in isarBackupAlbums) {
batch.update(
drift.localAlbumEntity,
LocalAlbumEntityCompanion(
backupSelection: Value(switch (album.selection) {
isar_backup_album.BackupSelection.none => BackupSelection.none,
isar_backup_album.BackupSelection.select => BackupSelection.selected,
isar_backup_album.BackupSelection.exclude => BackupSelection.excluded,
}),
),
where: (t) => t.id.equals(album.id),
);
}
});
} catch (error) {
dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error");
}
}
Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
try {
final isarStoreValues = await db.storeValues.where().findAll();
await drift.batch((batch) {
for (final storeValue in isarStoreValues) {
final companion = StoreEntityCompanion(
id: Value(storeValue.id),
stringValue: Value(storeValue.strValue),
intValue: Value(storeValue.intValue),
);
batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion));
}
});
} catch (error) {
dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error");
}
}
Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
try {
final driftStoreValues = await drift.storeEntity
.select()
.map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue))
.get();
await db.writeTxn(() async {
await db.storeValues.putAll(driftStoreValues);
});
} catch (error) {
dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error");
}
}
Future<void> _populateLocalAssetPlaybackStyle(Drift db) async {
try {
final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums();
for (final album in albums) {
final assets = await nativeApi.getAssetsForAlbum(album.id);
await db.batch((batch) {
for (final asset in assets) {
batch.update(
db.localAssetEntity,
LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
where: (t) => t.id.equals(asset.id),
);
}
});
}
if (Platform.isAndroid) {
final trashedAssetMap = await nativeApi.getTrashedAssets();
for (final entry in trashedAssetMap.cast<String, List<Object?>>().entries) {
final assets = entry.value.cast<PlatformAsset>();
await db.batch((batch) {
for (final asset in assets) {
batch.update(
db.trashedLocalAssetEntity,
TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
where: (t) => t.id.equals(asset.id),
);
}
});
}
dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets");
} else {
dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local assets");
}
} catch (error) {
dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error");
}
}
Future<void> _applyLocalAssetOrientation(Drift db) {
final query = db.localAssetEntity.update()
..where((filter) => (filter.orientation.equals(90) | (filter.orientation.equals(270))));
return query.write(
LocalAssetEntityCompanion.custom(
width: db.localAssetEntity.height,
height: db.localAssetEntity.width,
orientation: const Variable(0),
),
);
}
AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) {
PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown,
PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image,
PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video,
PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated,
PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto,
PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping,
};
class _DeviceAsset {
final String assetId;
final List<int>? hash;
final DateTime? dateTime;
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
await Store.put(StoreKey.version, targetVersion);
return;
}
-4
View File
@@ -2,21 +2,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
import 'package:immich_mobile/repositories/timeline.repository.dart';
void invalidateAllApiRepositoryProviders(WidgetRef ref) {
ref.invalidate(userApiRepositoryProvider);
ref.invalidate(activityApiRepositoryProvider);
ref.invalidate(partnerApiRepositoryProvider);
ref.invalidate(albumApiRepositoryProvider);
ref.invalidate(personApiRepositoryProvider);
ref.invalidate(assetApiRepositoryProvider);
ref.invalidate(timelineRepositoryProvider);
ref.invalidate(searchApiRepositoryProvider);
// Drift
-143
View File
@@ -1,143 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/share.service.dart';
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/location_picker.dart';
import 'package:immich_mobile/widgets/common/share_dialog.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
void handleShareAssets(WidgetRef ref, BuildContext context, Iterable<Asset> selection) {
showDialog(
context: context,
builder: (BuildContext buildContext) {
ref.watch(shareServiceProvider).shareAssets(selection.toList(), context).then((bool status) {
if (!status) {
ImmichToast.show(
context: context,
msg: 'image_viewer_page_state_provider_share_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
buildContext.pop();
});
return const ShareDialog();
},
barrierDismissible: false,
useRootNavigator: false,
);
}
Future<void> handleArchiveAssets(
WidgetRef ref,
BuildContext context,
List<Asset> selection, {
bool? shouldArchive,
ToastGravity toastGravity = ToastGravity.BOTTOM,
}) async {
if (selection.isNotEmpty) {
shouldArchive ??= !selection.every((a) => a.isArchived);
await ref.read(assetProvider.notifier).toggleArchive(selection, shouldArchive);
final message = shouldArchive
? 'moved_to_archive'.t(context: context, args: {'count': selection.length})
: 'moved_to_library'.t(context: context, args: {'count': selection.length});
if (context.mounted) {
ImmichToast.show(context: context, msg: message, gravity: toastGravity);
}
}
}
Future<void> handleFavoriteAssets(
WidgetRef ref,
BuildContext context,
List<Asset> selection, {
bool? shouldFavorite,
ToastGravity toastGravity = ToastGravity.BOTTOM,
}) async {
if (selection.isNotEmpty) {
shouldFavorite ??= !selection.every((a) => a.isFavorite);
await ref.watch(assetProvider.notifier).toggleFavorite(selection, shouldFavorite);
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset';
final toastMessage = shouldFavorite
? 'Added ${selection.length} $assetOrAssets to favorites'
: 'Removed ${selection.length} $assetOrAssets from favorites';
if (context.mounted) {
ImmichToast.show(context: context, msg: toastMessage, gravity: toastGravity);
}
}
}
Future<void> handleEditDateTime(WidgetRef ref, BuildContext context, List<Asset> selection) async {
DateTime? initialDate;
String? timeZone;
Duration? offset;
if (selection.length == 1) {
final asset = selection.first;
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
final (dt, oft) = assetWithExif.getTZAdjustedTimeAndOffset();
initialDate = dt;
offset = oft;
timeZone = assetWithExif.exifInfo?.timeZone;
}
final dateTime = await showDateTimePicker(
context: context,
initialDateTime: initialDate,
initialTZ: timeZone,
initialTZOffset: offset,
);
if (dateTime == null) {
return;
}
await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime);
}
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
LatLng? initialLatLng;
if (selection.length == 1) {
final asset = selection.first;
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) {
initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!);
}
}
final location = await showLocationPicker(context: context, initialLatLng: initialLatLng);
if (location == null) {
return;
}
await ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
}
Future<void> handleSetAssetsVisibility(
WidgetRef ref,
BuildContext context,
AssetVisibilityEnum visibility,
List<Asset> selection,
) async {
if (selection.isNotEmpty) {
await ref.watch(assetProvider.notifier).setLockedView(selection, visibility);
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset';
final toastMessage = visibility == AssetVisibilityEnum.locked
? 'Added ${selection.length} $assetOrAssets to locked folder'
: 'Removed ${selection.length} $assetOrAssets from locked folder';
if (context.mounted) {
ImmichToast.show(context: context, msg: toastMessage, gravity: ToastGravity.BOTTOM);
}
}
}
-7
View File
@@ -1,7 +0,0 @@
extension StringExtension on String {
String capitalizeFirstLetter() {
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
}
String s(num count) => (count == 1 ? '' : 's');
-51
View File
@@ -1,51 +0,0 @@
import 'package:flutter_hooks/flutter_hooks.dart';
/// Throttles function calls with the [interval] provided.
/// Also make sures to call the last Action after the elapsed interval
class Throttler {
final Duration interval;
DateTime? _lastActionTime;
Throttler({required this.interval});
T? run<T>(T Function() action) {
if (_lastActionTime == null || (DateTime.now().difference(_lastActionTime!) > interval)) {
final response = action();
_lastActionTime = DateTime.now();
return response;
}
return null;
}
void dispose() {
_lastActionTime = null;
}
}
/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a
/// default interval of 300ms is used to throttle the function calls
Throttler useThrottler({Duration interval = const Duration(milliseconds: 300), List<Object?>? keys}) =>
use(_ThrottleHook(interval: interval, keys: keys));
class _ThrottleHook extends Hook<Throttler> {
const _ThrottleHook({required this.interval, super.keys});
final Duration interval;
@override
HookState<Throttler, Hook<Throttler>> createState() => _ThrottlerHookState();
}
class _ThrottlerHookState extends HookState<Throttler, _ThrottleHook> {
late final throttler = Throttler(interval: hook.interval);
@override
Throttler build(_) => throttler;
@override
void dispose() => throttler.dispose();
@override
String get debugLabel => 'useThrottler';
}
-49
View File
@@ -1,49 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
String getAltText(ExifInfo? exifInfo, DateTime fileCreatedAt, AssetType type, List<String> peopleNames) {
if (exifInfo?.description != null && exifInfo!.description!.isNotEmpty) {
return exifInfo.description!;
}
final (template, args) = getAltTextTemplate(exifInfo, fileCreatedAt, type, peopleNames);
return template.t(args: args);
}
(String, Map<String, String>) getAltTextTemplate(
ExifInfo? exifInfo,
DateTime fileCreatedAt,
AssetType type,
List<String> peopleNames,
) {
final isVideo = type == AssetType.video;
final hasLocation = exifInfo?.city != null && exifInfo?.country != null;
final date = DateFormat.yMMMMd().format(fileCreatedAt);
final args = {
"isVideo": isVideo.toString(),
"date": date,
"city": exifInfo?.city ?? "",
"country": exifInfo?.country ?? "",
"person1": peopleNames.elementAtOrNull(0) ?? "",
"person2": peopleNames.elementAtOrNull(1) ?? "",
"person3": peopleNames.elementAtOrNull(2) ?? "",
"additionalCount": (peopleNames.length - 3).toString(),
};
final template = hasLocation
? (switch (peopleNames.length) {
0 => "image_alt_text_date_place",
1 => "image_alt_text_date_place_1_person",
2 => "image_alt_text_date_place_2_people",
3 => "image_alt_text_date_place_3_people",
_ => "image_alt_text_date_place_4_or_more_people",
})
: (switch (peopleNames.length) {
0 => "image_alt_text_date",
1 => "image_alt_text_date_1_person",
2 => "image_alt_text_date_2_people",
3 => "image_alt_text_date_3_people",
_ => "image_alt_text_date_4_or_more_people",
});
return (template, args);
}