feat: full local assets / album sync

This commit is contained in:
shenlong-tanwen
2024-10-17 23:33:00 +05:30
parent a09710ec7b
commit c91a2878dc
87 changed files with 2417 additions and 366 deletions
+33 -10
View File
@@ -1,5 +1,7 @@
// ignore_for_file: avoid-unsafe-collection-methods
import 'dart:async';
class CollectionUtil {
const CollectionUtil();
@@ -13,28 +15,33 @@ class CollectionUtil {
return a.compareTo(b);
}
/// Find the difference between the two sorted lists [first] and [second]
/// Find the difference between the two lists [first] and [second]
/// Results are passed as callbacks back to the caller during the comparison
static bool diffSortedLists<T>(
static FutureOr<bool> diffLists<T>(
List<T> first,
List<T> second, {
required Comparator<T> compare,
required bool Function(T a, T b) both,
required void Function(T a) onlyFirst,
required void Function(T b) onlySecond,
}) {
required int Function(T a, T b) compare,
required FutureOr<bool> Function(T a, T b) both,
required FutureOr<void> Function(T a) onlyFirst,
required FutureOr<void> Function(T b) onlySecond,
}) async {
first.sort(compare);
first.uniqueConsecutive(compare);
second.sort(compare);
second.uniqueConsecutive(compare);
bool diff = false;
int i = 0, j = 0;
for (; i < first.length && j < second.length;) {
final int order = compare(first[i], second[j]);
if (order == 0) {
diff |= both(first[i++], second[j++]);
diff |= await both(first[i++], second[j++]);
} else if (order < 0) {
onlyFirst(first[i++]);
await onlyFirst(first[i++]);
diff = true;
} else if (order > 0) {
onlySecond(second[j++]);
await onlySecond(second[j++]);
diff = true;
}
}
@@ -50,3 +57,19 @@ class CollectionUtil {
return diff;
}
}
extension _ListExtension<T> on List<T> {
List<T> uniqueConsecutive(int Function(T a, T b) compare) {
int i = 1, j = 1;
for (; i < length; i++) {
if (compare(this[i - 1], this[i]) != 0) {
if (i != j) {
this[j] = this[i];
}
j++;
}
}
length = length == 0 ? 0 : j;
return this;
}
}
+16 -1
View File
@@ -1,14 +1,29 @@
import 'package:flutter/material.dart';
const String kImmichAppName = "Immich";
/// Log messages stored in the DB
const int kLogMessageLimit = 500;
/// Cache constants
const int kCacheStalePeriod = 30; // in days
const String kCacheFullImagesKey = 'ImFullImageCacheKey';
const int kCacheMaxNrOfFullImages = 500;
const String kCacheThumbnailsKey = 'ImThumbnailCacheKey';
const int kCacheMaxNrOfThumbnails = 500;
/// Grid constants
const int kGridThumbnailSize = 200;
const int kGridThumbnailQuality = 80;
/// RenderList constants
const int kRenderListBatchSize = 512;
const int kRenderListOppositeBatchSize = 128;
/// Chunked asset sync size
/// Sync constants
const int kFullSyncChunkSize = 10000;
const int kHashAssetsFileLimit = 128;
const int kHashAssetsSizeLimit = 1024 * 1024 * 1024; // 1GB
/// Headers
// Auth header
@@ -0,0 +1,11 @@
import 'dart:io';
extension ClearPhotoManagerCacheExtension on File {
Future<void> deleteDarwinCache() async {
if (Platform.isIOS) {
try {
await delete();
} catch (_) {}
}
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ import 'package:immich_mobile/utils/constants/globals.dart';
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
import 'package:openapi/api.dart';
class ImmichApiClient extends ApiClient with LogMixin {
ImmichApiClient({required String endpoint}) : super(basePath: endpoint);
class ImApiClient extends ApiClient with LogMixin {
ImApiClient({required String endpoint}) : super(basePath: endpoint);
Map<String, String> get headers => defaultHeaderMap;
@@ -1,28 +1,36 @@
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:openapi/api.dart';
enum AssetMediaSize {
preview._('preview'),
thumbnail._('thumbnail');
const AssetMediaSize._(this.value);
final String value;
}
class ImImageUrlHelper {
const ImImageUrlHelper();
static String get _serverUrl => di<ImmichApiClient>().basePath;
static String get _serverUrl => di<ImApiClient>().basePath;
static String getThumbnailUrl(
final Asset asset, {
AssetMediaSize type = AssetMediaSize.thumbnail,
}) {
return _getThumbnailUrlForRemoteId(asset.remoteId!, type: type);
return getThumbnailUrlForRemoteId(asset.remoteId!, type: type);
}
static String getThumbnailCacheKey(
final Asset asset, {
AssetMediaSize type = AssetMediaSize.thumbnail,
}) {
return _getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type);
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type);
}
static String _getThumbnailCacheKeyForRemoteId(
static String getThumbnailCacheKeyForRemoteId(
final String id, {
AssetMediaSize type = AssetMediaSize.thumbnail,
}) {
@@ -32,7 +40,7 @@ class ImImageUrlHelper {
return 'preview-image-$id';
}
static String _getThumbnailUrlForRemoteId(
static String getThumbnailUrlForRemoteId(
final String id, {
AssetMediaSize type = AssetMediaSize.thumbnail,
}) {
+11 -4
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -32,7 +33,7 @@ class IsolateHelper {
IsolateHelper();
void preIsolateHandling() {
final apiClient = di<ImmichApiClient>();
final apiClient = di<ImApiClient>();
_clientData = _ImApiClientData(
endpoint: apiClient.basePath,
headersMap: apiClient.defaultHeaderMap,
@@ -42,7 +43,7 @@ class IsolateHelper {
void postIsolateHandling() {
assert(_clientData != null);
// Reconstruct client from cached data
final client = ImmichApiClient(endpoint: _clientData!.endpoint);
final client = ImApiClient(endpoint: _clientData!.endpoint);
for (final entry in _clientData.headersMap.entries) {
client.addDefaultHeader(entry.key, entry.value);
}
@@ -54,7 +55,7 @@ class IsolateHelper {
);
// Init log manager to continue listening to log events
LogManager.I.init();
LogManager.I.init(shouldBuffer: false);
}
static Future<T> run<T>(FutureOr<T> Function() computation) async {
@@ -66,9 +67,15 @@ class IsolateHelper {
final helper = IsolateHelper()..preIsolateHandling();
return await Isolate.run(() async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
// Delay to ensure the isolate is ready
await Future.delayed(const Duration(milliseconds: 100));
helper.postIsolateHandling();
try {
return await computation();
final result = await computation();
// Delay to ensure the isolate is not killed prematurely
await Future.delayed(const Duration(milliseconds: 100));
return result;
} finally {
// Always close the new database connection on Isolate end
await di<DriftDatabaseRepository>().close();
+27 -12
View File
@@ -21,6 +21,11 @@ class LogManager {
List<LogMessage> _msgBuffer = [];
Timer? _timer;
/// Whether to buffer logs in memory before writing to the database.
/// This is useful when logging in quick succession, as it increases performance
/// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates.
bool _shouldBuffer = true;
late final StreamSubscription<logging.LogRecord> _subscription;
void _onLogRecord(logging.LogRecord record) {
@@ -42,11 +47,14 @@ class LogManager {
error: record.error?.toString(),
stack: record.stackTrace?.toString(),
);
_msgBuffer.add(lm);
// delayed batch writing to database: increases performance when logging
// messages in quick succession and reduces NAND wear
_timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase);
if (_shouldBuffer) {
_msgBuffer.add(lm);
_timer ??=
Timer(const Duration(seconds: 5), () => _flushBufferToDatabase());
} else {
di<ILogRepository>().create(lm);
}
}
void _flushBufferToDatabase() {
@@ -56,7 +64,8 @@ class LogManager {
di<ILogRepository>().createAll(buffer);
}
void init() {
void init({bool? shouldBuffer}) {
_shouldBuffer = shouldBuffer ?? _shouldBuffer;
_subscription = logging.Logger.root.onRecord.listen(_onLogRecord);
}
@@ -106,18 +115,24 @@ class Logger {
logging.Logger get _logger => logging.Logger(_loggerName);
// Highly detailed
/// Finest / Verbose logs. Useful for highly detailed messages
void v(String message) => _logger.finest(message);
// Troubleshooting
/// Fine / Debug logs. Useful for troubleshooting
void d(String message) => _logger.fine(message);
// General purpose
/// Info logs. Useful for general logging
void i(String message) => _logger.info(message);
// Potential issues
void w(String message) => _logger.warning(message);
// Error
/// Warning logs. Useful to identify potential issues
void w(String message, [Object? error, StackTrace? stack]) =>
_logger.warning(message, error, stack);
/// Error logs. Useful for identifying issues
void e(String message, [Object? error, StackTrace? stack]) =>
_logger.severe(message, error, stack);
// Crash / Serious failure
/// Crash / Serious failure logs. Shouldn't happen
void wtf(String message, [Object? error, StackTrace? stack]) =>
_logger.shout(message, error, stack);
}