mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'upstream/main' into feature/load-previews
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
abstract interface class IDatabaseRepository {
|
||||
Future<T> transaction<T>(Future<T> Function() callback);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
|
||||
part 'local_asset.model.dart';
|
||||
part 'remote_asset.model.dart';
|
||||
|
||||
@@ -23,7 +25,7 @@ sealed class BaseAsset {
|
||||
final DateTime updatedAt;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final int? durationInSeconds;
|
||||
final int? durationMs;
|
||||
final bool isFavorite;
|
||||
final String? livePhotoVideoId;
|
||||
final bool isEdited;
|
||||
@@ -36,7 +38,7 @@ sealed class BaseAsset {
|
||||
required this.updatedAt,
|
||||
this.width,
|
||||
this.height,
|
||||
this.durationInSeconds,
|
||||
this.durationMs,
|
||||
this.isFavorite = false,
|
||||
this.livePhotoVideoId,
|
||||
required this.isEdited,
|
||||
@@ -51,15 +53,17 @@ sealed class BaseAsset {
|
||||
AssetPlaybackStyle get playbackStyle {
|
||||
if (isVideo) return AssetPlaybackStyle.video;
|
||||
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
|
||||
if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated;
|
||||
if (isImage && durationMs != null && durationMs! > 0) {
|
||||
return AssetPlaybackStyle.imageAnimated;
|
||||
}
|
||||
if (isImage) return AssetPlaybackStyle.image;
|
||||
return AssetPlaybackStyle.unknown;
|
||||
}
|
||||
|
||||
Duration get duration {
|
||||
final durationInSeconds = this.durationInSeconds;
|
||||
if (durationInSeconds != null) {
|
||||
return Duration(seconds: durationInSeconds);
|
||||
final durationMs = this.durationMs;
|
||||
if (durationMs != null) {
|
||||
return Duration(milliseconds: durationMs);
|
||||
}
|
||||
return const Duration();
|
||||
}
|
||||
@@ -69,6 +73,8 @@ sealed class BaseAsset {
|
||||
bool get isLocalOnly => storage == AssetState.local;
|
||||
bool get isRemoteOnly => storage == AssetState.remote;
|
||||
|
||||
bool get isEditable => false;
|
||||
|
||||
// Overridden in subclasses
|
||||
AssetState get storage;
|
||||
String? get localId;
|
||||
@@ -84,7 +90,7 @@ sealed class BaseAsset {
|
||||
updatedAt: $updatedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
durationMs: ${durationMs ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
isEdited: $isEdited,
|
||||
}''';
|
||||
@@ -100,7 +106,7 @@ sealed class BaseAsset {
|
||||
updatedAt == other.updatedAt &&
|
||||
width == other.width &&
|
||||
height == other.height &&
|
||||
durationInSeconds == other.durationInSeconds &&
|
||||
durationMs == other.durationMs &&
|
||||
isFavorite == other.isFavorite &&
|
||||
isEdited == other.isEdited;
|
||||
}
|
||||
@@ -115,7 +121,7 @@ sealed class BaseAsset {
|
||||
updatedAt.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
durationInSeconds.hashCode ^
|
||||
durationMs.hashCode ^
|
||||
isFavorite.hashCode ^
|
||||
isEdited.hashCode;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class LocalAsset extends BaseAsset {
|
||||
required super.updatedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
super.durationMs,
|
||||
super.isFavorite = false,
|
||||
super.livePhotoVideoId,
|
||||
this.orientation = 0,
|
||||
@@ -58,7 +58,7 @@ class LocalAsset extends BaseAsset {
|
||||
updatedAt: $updatedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
durationMs: ${durationMs ?? "<NA>"},
|
||||
playbackStyle: $playbackStyle,
|
||||
remoteId: ${remoteId ?? "<NA>"},
|
||||
cloudId: ${cloudId ?? "<NA>"},
|
||||
@@ -108,7 +108,7 @@ class LocalAsset extends BaseAsset {
|
||||
DateTime? updatedAt,
|
||||
int? width,
|
||||
int? height,
|
||||
int? durationInSeconds,
|
||||
int? durationMs,
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
AssetPlaybackStyle? playbackStyle,
|
||||
@@ -128,7 +128,7 @@ class LocalAsset extends BaseAsset {
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
|
||||
@@ -22,7 +22,7 @@ class RemoteAsset extends BaseAsset {
|
||||
required super.updatedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationInSeconds,
|
||||
super.durationMs,
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
@@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset {
|
||||
@override
|
||||
String get heroTag => '${localId ?? checksum}_$id';
|
||||
|
||||
@override
|
||||
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Asset {
|
||||
@@ -54,7 +57,7 @@ class RemoteAsset extends BaseAsset {
|
||||
updatedAt: $updatedAt,
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
durationMs: ${durationMs ?? "<NA>"},
|
||||
localId: ${localId ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
@@ -99,7 +102,7 @@ class RemoteAsset extends BaseAsset {
|
||||
DateTime? updatedAt,
|
||||
int? width,
|
||||
int? height,
|
||||
int? durationInSeconds,
|
||||
int? durationMs,
|
||||
bool? isFavorite,
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
@@ -118,7 +121,7 @@ class RemoteAsset extends BaseAsset {
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
@@ -128,3 +131,81 @@ class RemoteAsset extends BaseAsset {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteAssetExif extends RemoteAsset {
|
||||
final ExifInfo exifInfo;
|
||||
|
||||
const RemoteAssetExif({
|
||||
required super.id,
|
||||
super.localId,
|
||||
required super.name,
|
||||
required super.ownerId,
|
||||
required super.checksum,
|
||||
required super.type,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationMs,
|
||||
super.isFavorite = false,
|
||||
super.thumbHash,
|
||||
super.visibility = AssetVisibility.timeline,
|
||||
super.livePhotoVideoId,
|
||||
super.stackId,
|
||||
super.isEdited = false,
|
||||
this.exifInfo = const ExifInfo(),
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! RemoteAssetExif) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return super == other && exifInfo == other.exifInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => super.hashCode ^ exifInfo.hashCode;
|
||||
|
||||
@override
|
||||
RemoteAssetExif copyWith({
|
||||
String? id,
|
||||
String? localId,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
String? checksum,
|
||||
AssetType? type,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
int? width,
|
||||
int? height,
|
||||
int? durationMs,
|
||||
bool? isFavorite,
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
bool? isEdited,
|
||||
ExifInfo? exifInfo,
|
||||
}) {
|
||||
return RemoteAssetExif(
|
||||
id: id ?? this.id,
|
||||
localId: localId ?? this.localId,
|
||||
name: name ?? this.name,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
checksum: checksum ?? this.checksum,
|
||||
type: type ?? this.type,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import "package:openapi/api.dart" as api show AssetEditAction;
|
||||
import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters;
|
||||
|
||||
enum AssetEditAction { rotate, crop, mirror, other }
|
||||
|
||||
extension AssetEditActionExtension on AssetEditAction {
|
||||
api.AssetEditAction? toDto() {
|
||||
return switch (this) {
|
||||
AssetEditAction.rotate => api.AssetEditAction.rotate,
|
||||
AssetEditAction.crop => api.AssetEditAction.crop,
|
||||
AssetEditAction.mirror => api.AssetEditAction.mirror,
|
||||
AssetEditAction.other => null,
|
||||
};
|
||||
}
|
||||
sealed class AssetEdit {
|
||||
const AssetEdit();
|
||||
}
|
||||
|
||||
class AssetEdit {
|
||||
final AssetEditAction action;
|
||||
final Map<String, dynamic> parameters;
|
||||
class CropEdit extends AssetEdit {
|
||||
final CropParameters parameters;
|
||||
|
||||
const AssetEdit({required this.action, required this.parameters});
|
||||
const CropEdit(this.parameters);
|
||||
}
|
||||
|
||||
class RotateEdit extends AssetEdit {
|
||||
final RotateParameters parameters;
|
||||
|
||||
const RotateEdit(this.parameters);
|
||||
}
|
||||
|
||||
class MirrorEdit extends AssetEdit {
|
||||
final MirrorParameters parameters;
|
||||
|
||||
const MirrorEdit(this.parameters);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class DeviceAsset {
|
||||
final String assetId;
|
||||
final Uint8List hash;
|
||||
final DateTime modifiedTime;
|
||||
|
||||
const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DeviceAsset other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)';
|
||||
}
|
||||
|
||||
DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) {
|
||||
return DeviceAsset(
|
||||
assetId: assetId ?? this.assetId,
|
||||
hash: hash ?? this.hash,
|
||||
modifiedTime: modifiedTime ?? this.modifiedTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ class ExifInfo {
|
||||
final String? timeZone;
|
||||
final DateTime? dateTimeOriginal;
|
||||
final int? rating;
|
||||
final int? width;
|
||||
final int? height;
|
||||
|
||||
// GPS
|
||||
final double? latitude;
|
||||
@@ -27,7 +29,7 @@ class ExifInfo {
|
||||
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
|
||||
|
||||
String get exposureTime {
|
||||
if (exposureSeconds == null) {
|
||||
if (exposureSeconds == null || exposureSeconds! <= 0 || exposureSeconds!.isNaN) {
|
||||
return "";
|
||||
}
|
||||
if (exposureSeconds! < 1) {
|
||||
@@ -48,6 +50,8 @@ class ExifInfo {
|
||||
this.timeZone,
|
||||
this.dateTimeOriginal,
|
||||
this.rating,
|
||||
this.width,
|
||||
this.height,
|
||||
this.isFlipped = false,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
@@ -74,6 +78,8 @@ class ExifInfo {
|
||||
other.timeZone == timeZone &&
|
||||
other.dateTimeOriginal == dateTimeOriginal &&
|
||||
other.rating == rating &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.city == city &&
|
||||
@@ -98,6 +104,8 @@ class ExifInfo {
|
||||
timeZone.hashCode ^
|
||||
dateTimeOriginal.hashCode ^
|
||||
rating.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode ^
|
||||
city.hashCode ^
|
||||
@@ -123,6 +131,8 @@ isFlipped: $isFlipped,
|
||||
timeZone: ${timeZone ?? 'NA'},
|
||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||
rating: ${rating ?? 'NA'},
|
||||
width: ${width ?? 'NA'},
|
||||
height: ${height ?? 'NA'},
|
||||
latitude: ${latitude ?? 'NA'},
|
||||
longitude: ${longitude ?? 'NA'},
|
||||
city: ${city ?? 'NA'},
|
||||
@@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
String? timeZone,
|
||||
DateTime? dateTimeOriginal,
|
||||
int? rating,
|
||||
int? width,
|
||||
int? height,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
@@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
timeZone: timeZone ?? this.timeZone,
|
||||
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
|
||||
rating: rating ?? this.rating,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
isFlipped: isFlipped ?? this.isFlipped,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
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/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
|
||||
typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped});
|
||||
|
||||
class AssetService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
@@ -58,49 +55,6 @@ class AssetService {
|
||||
return _remoteAssetRepository.getExif(id);
|
||||
}
|
||||
|
||||
Future<double> getAspectRatio(BaseAsset asset) async {
|
||||
final dimension = asset is LocalAsset
|
||||
? await _getLocalAssetDimensions(asset)
|
||||
: await _getRemoteAssetDimensions(asset as RemoteAsset);
|
||||
|
||||
if (dimension.width == null || dimension.height == null || dimension.height == 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!;
|
||||
}
|
||||
|
||||
Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async {
|
||||
double? width = asset.width?.toDouble();
|
||||
double? height = asset.height?.toDouble();
|
||||
int orientation = asset.orientation;
|
||||
|
||||
if (width == null || height == null) {
|
||||
final fetched = await _localAssetRepository.get(asset.id);
|
||||
width = fetched?.width?.toDouble();
|
||||
height = fetched?.height?.toDouble();
|
||||
orientation = fetched?.orientation ?? 0;
|
||||
}
|
||||
|
||||
// On Android, local assets need orientation correction for 90°/270° rotations
|
||||
// On iOS, the Photos framework pre-corrects dimensions
|
||||
final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270);
|
||||
return (width: width, height: height, isFlipped: isFlipped);
|
||||
}
|
||||
|
||||
Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async {
|
||||
double? width = asset.width?.toDouble();
|
||||
double? height = asset.height?.toDouble();
|
||||
|
||||
if (width == null || height == null) {
|
||||
final fetched = await _remoteAssetRepository.get(asset.id);
|
||||
width = fetched?.width?.toDouble();
|
||||
height = fetched?.height?.toDouble();
|
||||
}
|
||||
|
||||
return (width: width, height: height, isFlipped: false);
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces(String userId) {
|
||||
return _remoteAssetRepository.getPlaces(userId);
|
||||
}
|
||||
|
||||
@@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class BackgroundWorkerFgService {
|
||||
@@ -58,7 +55,6 @@ class BackgroundWorkerFgService {
|
||||
|
||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
ProviderContainer? _ref;
|
||||
final Isar _isar;
|
||||
final Drift _drift;
|
||||
final DriftLogger _driftLogger;
|
||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||
@@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger})
|
||||
: _isar = isar,
|
||||
_drift = drift,
|
||||
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
|
||||
: _drift = drift,
|
||||
_driftLogger = driftLogger,
|
||||
_backgroundHostApi = BackgroundWorkerBgHostApi() {
|
||||
_ref = ProviderContainer(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
@@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
),
|
||||
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
||||
FileDownloader().trackTasks(),
|
||||
_ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
||||
].nonNulls,
|
||||
);
|
||||
|
||||
@@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
backgroundSyncManager?.cancel(),
|
||||
];
|
||||
|
||||
if (_isar.isOpen) {
|
||||
cleanupFutures.add(_isar.close());
|
||||
}
|
||||
await Future.wait(cleanupFutures.nonNulls);
|
||||
_logger.info("Background worker resources cleaned up");
|
||||
} catch (error, stack) {
|
||||
@@ -301,7 +286,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final (isar, drift, logDB) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
|
||||
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
|
||||
final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false);
|
||||
await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init();
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ class LocalSyncService {
|
||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||
a.width == b.width &&
|
||||
a.height == b.height &&
|
||||
a.durationInSeconds == b.durationInSeconds;
|
||||
a.durationMs == b.durationMs;
|
||||
}
|
||||
|
||||
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||
@@ -346,7 +346,7 @@ class LocalSyncService {
|
||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||
a.width == b.width &&
|
||||
a.height == b.height &&
|
||||
a.durationInSeconds == b.durationInSeconds &&
|
||||
a.durationMs == b.durationMs &&
|
||||
a.latitude == b.latitude &&
|
||||
a.longitude == b.longitude;
|
||||
}
|
||||
@@ -432,7 +432,7 @@ extension PlatformToLocalAsset on PlatformAsset {
|
||||
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
durationMs: durationMs,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
playbackStyle: _toPlaybackStyle(playbackStyle),
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:logging/logging.dart';
|
||||
/// via [IStoreRepository]
|
||||
class LogService {
|
||||
final LogRepository _logRepository;
|
||||
final IStoreRepository _storeRepository;
|
||||
final DriftStoreRepository _storeRepository;
|
||||
|
||||
final List<LogMessage> _msgBuffer = [];
|
||||
|
||||
@@ -38,7 +38,7 @@ class LogService {
|
||||
|
||||
static Future<LogService> init({
|
||||
required LogRepository logRepository,
|
||||
required IStoreRepository storeRepository,
|
||||
required DriftStoreRepository storeRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
_instance ??= await create(
|
||||
@@ -51,7 +51,7 @@ class LogService {
|
||||
|
||||
static Future<LogService> create({
|
||||
required LogRepository logRepository,
|
||||
required IStoreRepository storeRepository,
|
||||
required DriftStoreRepository storeRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||
|
||||
class SearchService {
|
||||
@@ -52,43 +51,3 @@ class SearchService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetResponseDto {
|
||||
RemoteAsset toDto() {
|
||||
return RemoteAsset(
|
||||
id: id,
|
||||
name: originalFileName,
|
||||
checksum: checksum,
|
||||
createdAt: fileCreatedAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
visibility: switch (visibility) {
|
||||
api.AssetVisibility.timeline => AssetVisibility.timeline,
|
||||
api.AssetVisibility.hidden => AssetVisibility.hidden,
|
||||
api.AssetVisibility.archive => AssetVisibility.archive,
|
||||
api.AssetVisibility.locked => AssetVisibility.locked,
|
||||
_ => AssetVisibility.timeline,
|
||||
},
|
||||
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
|
||||
height: height?.toInt(),
|
||||
width: width?.toInt(),
|
||||
isFavorite: isFavorite,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
thumbHash: thumbhash,
|
||||
localId: null,
|
||||
type: type.toAssetType(),
|
||||
stackId: stack?.id,
|
||||
isEdited: isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on AssetTypeEnum {
|
||||
AssetType toAssetType() => switch (this) {
|
||||
AssetTypeEnum.IMAGE => AssetType.image,
|
||||
AssetTypeEnum.VIDEO => AssetType.video,
|
||||
AssetTypeEnum.AUDIO => AssetType.audio,
|
||||
AssetTypeEnum.OTHER => AssetType.other,
|
||||
_ => throw Exception('Unknown AssetType value: $this'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
|
||||
/// Provides access to a persistent key-value store with an in-memory cache.
|
||||
/// Listens for repository changes to keep the cache updated.
|
||||
class StoreService {
|
||||
final IStoreRepository _storeRepository;
|
||||
final DriftStoreRepository _storeRepository;
|
||||
|
||||
/// In-memory cache. Keys are [StoreKey.id]
|
||||
final Map<int, Object?> _cache = {};
|
||||
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
|
||||
|
||||
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
|
||||
StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
|
||||
|
||||
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
|
||||
static StoreService? _instance;
|
||||
@@ -24,12 +24,12 @@ class StoreService {
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
static Future<StoreService> init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
static Future<StoreService> create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
|
||||
final instance = StoreService._(isarStoreRepository: storeRepository);
|
||||
await instance.populateCache();
|
||||
if (listenUpdates) {
|
||||
@@ -91,8 +91,6 @@ class StoreService {
|
||||
await _storeRepository.deleteAll();
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true;
|
||||
}
|
||||
|
||||
class StoreKeyNotFoundException implements Exception {
|
||||
|
||||
@@ -34,6 +34,7 @@ enum TimelineOrigin {
|
||||
search,
|
||||
deepLink,
|
||||
albumActivities,
|
||||
folder,
|
||||
}
|
||||
|
||||
class TimelineFactory {
|
||||
|
||||
@@ -4,23 +4,17 @@ import 'dart:typed_data';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
final Logger _log = Logger("UserService");
|
||||
final IsarUserRepository _isarUserRepository;
|
||||
final UserApiRepository _userApiRepository;
|
||||
final StoreService _storeService;
|
||||
|
||||
UserService({
|
||||
required IsarUserRepository isarUserRepository,
|
||||
required UserApiRepository userApiRepository,
|
||||
required StoreService storeService,
|
||||
}) : _isarUserRepository = isarUserRepository,
|
||||
_userApiRepository = userApiRepository,
|
||||
_storeService = storeService;
|
||||
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
|
||||
: _userApiRepository = userApiRepository,
|
||||
_storeService = storeService;
|
||||
|
||||
UserDto getMyUser() {
|
||||
return _storeService.get(StoreKey.currentUser);
|
||||
@@ -38,7 +32,6 @@ class UserService {
|
||||
final user = await _userApiRepository.getMyUser();
|
||||
if (user == null) return null;
|
||||
await _storeService.put(StoreKey.currentUser, user);
|
||||
await _isarUserRepository.update(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -47,19 +40,10 @@ class UserService {
|
||||
final path = await _userApiRepository.createProfileImage(name: name, data: image);
|
||||
final updatedUser = getMyUser();
|
||||
await _storeService.put(StoreKey.currentUser, updatedUser);
|
||||
await _isarUserRepository.update(updatedUser);
|
||||
return path;
|
||||
} catch (e) {
|
||||
_log.warning("Failed to upload profile image", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getAll() async {
|
||||
return await _isarUserRepository.getAll();
|
||||
}
|
||||
|
||||
Future<void> deleteAll() {
|
||||
return _isarUserRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,12 +80,14 @@ Future<void> _processCloudIdMappingsInBatches(
|
||||
AssetMetadataBulkUpsertItemDto(
|
||||
assetId: mapping.remoteAssetId,
|
||||
key: kMobileMetadataKey,
|
||||
value: RemoteAssetMobileAppMetadata(
|
||||
cloudId: mapping.localAsset.cloudId,
|
||||
createdAt: mapping.localAsset.createdAt.toIso8601String(),
|
||||
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
|
||||
latitude: mapping.localAsset.latitude?.toString(),
|
||||
longitude: mapping.localAsset.longitude?.toString(),
|
||||
value: Map<String, Object>.from(
|
||||
RemoteAssetMobileAppMetadata(
|
||||
cloudId: mapping.localAsset.cloudId,
|
||||
createdAt: mapping.localAsset.createdAt.toIso8601String(),
|
||||
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
|
||||
latitude: mapping.localAsset.latitude?.toString(),
|
||||
longitude: mapping.localAsset.longitude?.toString(),
|
||||
).toJson(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user