feat: full local assets / album sync
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user