diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt index e6cf92f57..d3282f4df 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -89,7 +89,10 @@ data class PlatformAsset ( val height: Long? = null, val durationInSeconds: Long, val orientation: Long, - val isFavorite: Boolean + val isFavorite: Boolean, + val adjustmentTime: Long? = null, + val latitude: Double? = null, + val longitude: Double? = null ) { companion object { @@ -104,7 +107,10 @@ data class PlatformAsset ( val durationInSeconds = pigeonVar_list[7] as Long val orientation = pigeonVar_list[8] as Long val isFavorite = pigeonVar_list[9] as Boolean - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite) + val adjustmentTime = pigeonVar_list[10] as Long? + val latitude = pigeonVar_list[11] as Double? + val longitude = pigeonVar_list[12] as Double? + return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude) } } fun toList(): List { @@ -119,6 +125,9 @@ data class PlatformAsset ( durationInSeconds, orientation, isFavorite, + adjustmentTime, + latitude, + longitude, ) } override fun equals(other: Any?): Boolean { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index b1e9dd7d4..b374ef50f 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.database.Cursor -import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.util.Base64 diff --git a/mobile/drift_schemas/main/drift_schema_v14.json b/mobile/drift_schemas/main/drift_schema_v14.json new file mode 100644 index 000000000..40a1af842 Binary files /dev/null and b/mobile/drift_schemas/main/drift_schema_v14.json differ diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index bbe18e737..c1cc98014 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -140,6 +140,9 @@ struct PlatformAsset: Hashable { var durationInSeconds: Int64 var orientation: Int64 var isFavorite: Bool + var adjustmentTime: Int64? = nil + var latitude: Double? = nil + var longitude: Double? = nil // swift-format-ignore: AlwaysUseLowerCamelCase @@ -154,6 +157,9 @@ struct PlatformAsset: Hashable { let durationInSeconds = pigeonVar_list[7] as! Int64 let orientation = pigeonVar_list[8] as! Int64 let isFavorite = pigeonVar_list[9] as! Bool + let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10]) + let latitude: Double? = nilOrValue(pigeonVar_list[11]) + let longitude: Double? = nilOrValue(pigeonVar_list[12]) return PlatformAsset( id: id, @@ -165,7 +171,10 @@ struct PlatformAsset: Hashable { height: height, durationInSeconds: durationInSeconds, orientation: orientation, - isFavorite: isFavorite + isFavorite: isFavorite, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude ) } func toList() -> [Any?] { @@ -180,6 +189,9 @@ struct PlatformAsset: Hashable { durationInSeconds, orientation, isFavorite, + adjustmentTime, + latitude, + longitude, ] } static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { diff --git a/mobile/ios/Runner/Sync/PHAssetExtensions.swift b/mobile/ios/Runner/Sync/PHAssetExtensions.swift index 2b1ef6ac8..f555d75bd 100644 --- a/mobile/ios/Runner/Sync/PHAssetExtensions.swift +++ b/mobile/ios/Runner/Sync/PHAssetExtensions.swift @@ -12,7 +12,10 @@ extension PHAsset { height: Int64(pixelHeight), durationInSeconds: Int64(duration), orientation: 0, - isFavorite: isFavorite + isFavorite: isFavorite, + adjustmentTime: adjustmentTimestamp, + latitude: location?.coordinate.latitude, + longitude: location?.coordinate.longitude ) } @@ -23,6 +26,13 @@ extension PHAsset { var filename: String? { return value(forKey: "filename") as? String } + + var adjustmentTimestamp: Int64? { + if let date = value(forKey: "adjustmentTimestamp") as? Date { + return Int64(date.timeIntervalSince1970) + } + return nil + } // This method is expected to be slow as it goes through the asset resources to fetch the originalFilename var originalFilename: String? { diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 6f2f4c06b..ba64cc40b 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -5,6 +5,10 @@ class LocalAsset extends BaseAsset { final String? remoteAssetId; final int orientation; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const LocalAsset({ required this.id, String? remoteId, @@ -19,6 +23,9 @@ class LocalAsset extends BaseAsset { super.isFavorite = false, super.livePhotoVideoId, this.orientation = 0, + this.adjustmentTime, + this.latitude, + this.longitude, }) : remoteAssetId = remoteId; @override @@ -33,6 +40,8 @@ class LocalAsset extends BaseAsset { @override String get heroTag => '${id}_${remoteId ?? checksum}'; + bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0; + @override String toString() { return '''LocalAsset { @@ -47,6 +56,9 @@ class LocalAsset extends BaseAsset { remoteId: ${remoteId ?? ""} isFavorite: $isFavorite, orientation: $orientation, + adjustmentTime: $adjustmentTime, + latitude: ${latitude ?? ""}, + longitude: ${longitude ?? ""}, }'''; } @@ -55,11 +67,23 @@ class LocalAsset extends BaseAsset { bool operator ==(Object other) { if (other is! LocalAsset) return false; if (identical(this, other)) return true; - return super == other && id == other.id && orientation == other.orientation; + return super == other && + id == other.id && + orientation == other.orientation && + adjustmentTime == other.adjustmentTime && + latitude == other.latitude && + longitude == other.longitude; } @override - int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode; + int get hashCode => + super.hashCode ^ + id.hashCode ^ + remoteId.hashCode ^ + orientation.hashCode ^ + adjustmentTime.hashCode ^ + latitude.hashCode ^ + longitude.hashCode; LocalAsset copyWith({ String? id, @@ -74,6 +98,9 @@ class LocalAsset extends BaseAsset { int? durationInSeconds, bool? isFavorite, int? orientation, + DateTime? adjustmentTime, + double? latitude, + double? longitude, }) { return LocalAsset( id: id ?? this.id, @@ -88,6 +115,9 @@ class LocalAsset extends BaseAsset { durationInSeconds: durationInSeconds ?? this.durationInSeconds, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, ); } } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 04eaf0469..c49ac49cc 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -286,11 +286,23 @@ class LocalSyncService { } bool _assetsEqual(LocalAsset a, LocalAsset b) { - return a.updatedAt.isAtSameMomentAs(b.updatedAt) && + if (CurrentPlatform.isAndroid) { + return a.updatedAt.isAtSameMomentAs(b.updatedAt) && + a.createdAt.isAtSameMomentAs(b.createdAt) && + a.width == b.width && + a.height == b.height && + a.durationInSeconds == b.durationInSeconds; + } + + final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0; + final secondAdjustment = b.adjustmentTime?.millisecondsSinceEpoch ?? 0; + return firstAdjustment == secondAdjustment && a.createdAt.isAtSameMomentAs(b.createdAt) && a.width == b.width && a.height == b.height && - a.durationInSeconds == b.durationInSeconds; + a.durationInSeconds == b.durationInSeconds && + a.latitude == b.latitude && + a.longitude == b.longitude; } bool _albumsEqual(LocalAlbum a, LocalAlbum b) { @@ -376,5 +388,8 @@ extension PlatformToLocalAsset on PlatformAsset { durationInSeconds: durationInSeconds, isFavorite: isFavorite, orientation: orientation, + adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true), + latitude: latitude, + longitude: longitude, ); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 8b253f83a..d2455b744 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -16,6 +16,12 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { IntColumn get orientation => integer().withDefault(const Constant(0))(); + DateTimeColumn get adjustmentTime => dateTime().nullable()(); + + RealColumn get latitude => real().nullable()(); + + RealColumn get longitude => real().nullable()(); + @override Set get primaryKey => {id}; } @@ -34,5 +40,8 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { width: width, remoteId: remoteId, orientation: orientation, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, ); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index d0fe74246..22219b1e6 100644 Binary files a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart and b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 548aa2e38..b42aa3155 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; @@ -21,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; @@ -95,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository { } @override - int get schemaVersion => 13; + int get schemaVersion => 14; @override MigrationStrategy get migration => MigrationStrategy( @@ -185,6 +185,11 @@ class Drift extends $Drift implements IDatabaseRepository { await m.createIndex(v13.idxTrashedLocalAssetChecksum); await m.createIndex(v13.idxTrashedLocalAssetAlbum); }, + from13To14: (m, v14) async { + await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime); + await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude); + await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index f2d87a7f8..21a3db527 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.steps.dart and b/mobile/lib/infrastructure/repositories/db.repository.steps.dart differ diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 59546a453..9d4c9bc49 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + 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'; @@ -244,7 +246,56 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); } - Future _upsertAssets(Iterable localAssets) { + Future Function(Iterable) get _upsertAssets => + CurrentPlatform.isIOS ? _upsertAssetsDarwin : _upsertAssetsAndroid; + + Future _upsertAssetsDarwin(Iterable localAssets) async { + if (localAssets.isEmpty) { + return Future.value(); + } + + // Reset checksum if asset changed + await _db.batch((batch) async { + for (final asset in localAssets) { + final companion = LocalAssetEntityCompanion( + checksum: const Value(null), + adjustmentTime: Value(asset.adjustmentTime), + ); + batch.update( + _db.localAssetEntity, + companion, + where: (row) => row.id.equals(asset.id) & row.adjustmentTime.isNotExp(Variable(asset.adjustmentTime)), + ); + } + }); + + return _db.batch((batch) async { + for (final asset in localAssets) { + final companion = LocalAssetEntityCompanion.insert( + name: asset.name, + type: asset.type, + createdAt: Value(asset.createdAt), + updatedAt: Value(asset.updatedAt), + width: Value(asset.width), + height: Value(asset.height), + durationInSeconds: Value(asset.durationInSeconds), + id: asset.id, + orientation: Value(asset.orientation), + isFavorite: Value(asset.isFavorite), + latitude: Value(asset.latitude), + longitude: Value(asset.longitude), + adjustmentTime: Value(asset.adjustmentTime), + ); + batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( + _db.localAssetEntity, + companion.copyWith(checksum: const Value(null)), + onConflict: DoUpdate((old) => companion), + ); + } + }); + } + + Future _upsertAssetsAndroid(Iterable localAssets) async { if (localAssets.isEmpty) { return Future.value(); } @@ -260,6 +311,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { height: Value(asset.height), durationInSeconds: Value(asset.durationInSeconds), id: asset.id, + checksum: const Value(null), orientation: Value(asset.orientation), isFavorite: Value(asset.isFavorite), ); diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index 34ed7a5e2..1c3b4b083 100644 Binary files a/mobile/lib/platform/native_sync_api.g.dart and b/mobile/lib/platform/native_sync_api.g.dart differ diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 752ab5ba3..2b7034770 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/providers/infrastructure/asset.provider.dart'; @RoutePage() @@ -129,6 +130,15 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString())); final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id); properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', '))); + if (CurrentPlatform.isIOS) { + properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString())); + } + properties.add( + _PropertyItem( + label: 'GPS Coordinates', + value: asset.hasCoordinates ? '${asset.latitude}, ${asset.longitude}' : null, + ), + ); } Future _addRemoteAssetProperties(RemoteAsset asset) async { diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 552c9e356..35cdc7add 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -81,7 +81,7 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } if (version < 19 && Store.isBetaTimelineEnabled) { - if (!await _populateUpdatedAtTime(drift)) { + if (!await _populateLocalAssetTime(drift)) { return; } } @@ -229,7 +229,7 @@ Future _migrateDeviceAsset(Isar db) async { }); } -Future _populateUpdatedAtTime(Drift db) async { +Future _populateLocalAssetTime(Drift db) async { try { final nativeApi = NativeSyncApi(); final albums = await nativeApi.getAlbums(); @@ -240,6 +240,9 @@ Future _populateUpdatedAtTime(Drift db) async { 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), @@ -250,7 +253,7 @@ Future _populateUpdatedAtTime(Drift db) async { return true; } catch (error) { - dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error"); + dPrint(() => "[MIGRATION] Error while populating asset time: $error"); return false; } } diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index 822e2eddb..ec28afb00 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -27,6 +27,10 @@ class PlatformAsset { final int orientation; final bool isFavorite; + final int? adjustmentTime; + final double? latitude; + final double? longitude; + const PlatformAsset({ required this.id, required this.name, @@ -38,6 +42,9 @@ class PlatformAsset { this.durationInSeconds = 0, this.orientation = 0, this.isFavorite = false, + this.adjustmentTime, + this.latitude, + this.longitude, }); } diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 69dff89fb..5e1961057 100644 Binary files a/mobile/test/drift/main/generated/schema.dart and b/mobile/test/drift/main/generated/schema.dart differ diff --git a/mobile/test/drift/main/generated/schema_v14.dart b/mobile/test/drift/main/generated/schema_v14.dart new file mode 100644 index 000000000..242aa581f Binary files /dev/null and b/mobile/test/drift/main/generated/schema_v14.dart differ