refactor: exif entity (#16621)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-03-06 23:28:24 +05:30 committed by GitHub
parent 4ebc25c754
commit fe931faf17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 502 additions and 359 deletions

View File

@ -67,7 +67,7 @@ custom_lint:
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
- lib/infrastructure/entities/*.entity.dart
- lib/infrastructure/repositories/{store,db,log}.repository.dart
- lib/infrastructure/repositories/{store,db,log,exif}.repository.dart
- lib/providers/infrastructure/db.provider.dart
# acceptable exceptions for the time being (until Isar is fully replaced)
- lib/providers/app_life_cycle.provider.dart
@ -90,6 +90,7 @@ custom_lint:
# required / wanted
- lib/repositories/*_api.repository.dart
- lib/infrastructure/repositories/*_api.repository.dart
- lib/infrastructure/utils/*.converter.dart
# acceptable exceptions for the time being
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine

View File

@ -0,0 +1,14 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<ExifInfo?> get(int assetId);
Future<ExifInfo> update(ExifInfo exifInfo);
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int assetId);
Future<void> deleteAll();
}

View File

@ -1,8 +1,9 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
abstract interface class ILogRepository {
abstract interface class ILogRepository implements IDatabaseRepository {
Future<bool> insert(LogMessage log);
Future<bool> insertAll(Iterable<LogMessage> logs);

View File

@ -0,0 +1,177 @@
class ExifInfo {
final int? assetId;
final int? fileSize;
final String? description;
final bool isFlipped;
final String? orientation;
final String? timeZone;
final DateTime? dateTimeOriginal;
// GPS
final double? latitude;
final double? longitude;
final String? city;
final String? state;
final String? country;
// Camera related
final String? make;
final String? model;
final String? lens;
final double? f;
final double? mm;
final int? iso;
final double? exposureSeconds;
bool get hasCoordinates =>
latitude != null && longitude != null && latitude != 0 && longitude != 0;
String get exposureTime {
if (exposureSeconds == null) {
return "";
}
if (exposureSeconds! < 1) {
return "1/${(1.0 / exposureSeconds!).round()} s";
}
return "${exposureSeconds!.toStringAsFixed(1)} s";
}
String get fNumber => f == null ? "" : f!.toStringAsFixed(1);
String get focalLength => mm == null ? "" : mm!.toStringAsFixed(1);
const ExifInfo({
this.assetId,
this.fileSize,
this.description,
this.orientation,
this.timeZone,
this.dateTimeOriginal,
this.isFlipped = false,
this.latitude,
this.longitude,
this.city,
this.state,
this.country,
this.make,
this.model,
this.lens,
this.f,
this.mm,
this.iso,
this.exposureSeconds,
});
@override
bool operator ==(covariant ExifInfo other) {
if (identical(this, other)) return true;
return other.fileSize == fileSize &&
other.description == description &&
other.orientation == orientation &&
other.timeZone == timeZone &&
other.dateTimeOriginal == dateTimeOriginal &&
other.latitude == latitude &&
other.longitude == longitude &&
other.city == city &&
other.state == state &&
other.country == country &&
other.make == make &&
other.model == model &&
other.lens == lens &&
other.f == f &&
other.mm == mm &&
other.iso == iso &&
other.exposureSeconds == exposureSeconds &&
other.assetId == assetId;
}
@override
int get hashCode {
return fileSize.hashCode ^
description.hashCode ^
orientation.hashCode ^
timeZone.hashCode ^
dateTimeOriginal.hashCode ^
latitude.hashCode ^
longitude.hashCode ^
city.hashCode ^
state.hashCode ^
country.hashCode ^
make.hashCode ^
model.hashCode ^
lens.hashCode ^
f.hashCode ^
mm.hashCode ^
iso.hashCode ^
exposureSeconds.hashCode ^
assetId.hashCode;
}
@override
String toString() {
return '''{
fileSize: ${fileSize ?? 'NA'},
description: ${description ?? 'NA'},
orientation: ${orientation ?? 'NA'},
timeZone: ${timeZone ?? 'NA'},
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
latitude: ${latitude ?? 'NA'},
longitude: ${longitude ?? 'NA'},
city: ${city ?? 'NA'},
state: ${state ?? 'NA'},
country: ${country ?? '<NA>'},
make: ${make ?? 'NA'},
model: ${model ?? 'NA'},
lens: ${lens ?? 'NA'},
f: ${f ?? 'NA'},
mm: ${mm ?? '<NA>'},
iso: ${iso ?? 'NA'},
exposureSeconds: ${exposureSeconds ?? 'NA'},
}''';
}
ExifInfo copyWith({
int? assetId,
int? fileSize,
String? description,
String? orientation,
String? timeZone,
DateTime? dateTimeOriginal,
double? latitude,
double? longitude,
String? city,
String? state,
String? country,
bool? isFlipped,
String? make,
String? model,
String? lens,
double? f,
double? mm,
int? iso,
double? exposureSeconds,
}) {
return ExifInfo(
assetId: assetId ?? this.assetId,
fileSize: fileSize ?? this.fileSize,
description: description ?? this.description,
orientation: orientation ?? this.orientation,
timeZone: timeZone ?? this.timeZone,
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
isFlipped: isFlipped ?? this.isFlipped,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
city: city ?? this.city,
state: state ?? this.state,
country: country ?? this.country,
make: make ?? this.make,
model: model ?? this.model,
lens: lens ?? this.lens,
f: f ?? this.f,
mm: mm ?? this.mm,
iso: iso ?? this.iso,
exposureSeconds: exposureSeconds ?? this.exposureSeconds,
);
}
}

View File

@ -1,13 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart' show AssetEntity;
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:path/path.dart' as p;
import 'package:photo_manager/photo_manager.dart' show AssetEntity;
part 'asset.entity.g.dart';
@ -27,8 +30,9 @@ class Asset {
width = remote.exifInfo?.exifImageWidth?.toInt(),
livePhotoVideoId = remote.livePhotoVideoId,
ownerId = fastHash(remote.ownerId),
exifInfo =
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
exifInfo = remote.exifInfo == null
? null
: ExifDtoConverter.fromDto(remote.exifInfo!),
isFavorite = remote.isFavorite,
isArchived = remote.isArchived,
isTrashed = remote.isTrashed,
@ -359,14 +363,14 @@ class Asset {
localId: localId,
width: a.width ?? width,
height: a.height ?? height,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo,
);
} else if (isRemote) {
return _copyWith(
localId: localId ?? a.localId,
width: width ?? a.width,
height: height ?? a.height,
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id),
);
} else {
// TODO: Revisit this and remove all bool field assignments
@ -407,7 +411,7 @@ class Asset {
isArchived: a.isArchived,
isTrashed: a.isTrashed,
isOffline: a.isOffline,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo,
thumbhash: a.thumbhash,
);
} else {
@ -416,7 +420,8 @@ class Asset {
localId: localId ?? a.localId,
width: width ?? a.width,
height: height ?? a.height,
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
exifInfo: exifInfo ??
a.exifInfo?.copyWith(assetId: id), // updated to use assetId
);
}
}
@ -476,8 +481,8 @@ class Asset {
Future<void> put(Isar db) async {
await db.assets.put(this);
if (exifInfo != null) {
exifInfo!.id = id;
await db.exifInfos.put(exifInfo!);
await db.exifInfos
.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id)));
}
}

View File

@ -1,241 +0,0 @@
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
part 'exif_info.entity.g.dart';
/// Exif information 1:1 relation with Asset
@Collection(inheritance: false)
class ExifInfo {
Id? id;
int? fileSize;
DateTime? dateTimeOriginal;
String? timeZone;
String? make;
String? model;
String? lens;
float? f;
float? mm;
short? iso;
float? exposureSeconds;
float? lat;
float? long;
String? city;
String? state;
String? country;
String? description;
String? orientation;
@ignore
bool get hasCoordinates =>
latitude != null && longitude != null && latitude != 0 && longitude != 0;
@ignore
String get exposureTime {
if (exposureSeconds == null) {
return "";
} else if (exposureSeconds! < 1) {
return "1/${(1.0 / exposureSeconds!).round()} s";
} else {
return "${exposureSeconds!.toStringAsFixed(1)} s";
}
}
@ignore
String get fNumber => f != null ? f!.toStringAsFixed(1) : "";
@ignore
String get focalLength => mm != null ? mm!.toStringAsFixed(1) : "";
@ignore
bool? _isFlipped;
@ignore
@pragma('vm:prefer-inline')
bool get isFlipped => _isFlipped ??= _isOrientationFlipped(orientation);
@ignore
double? get latitude => lat;
@ignore
double? get longitude => long;
ExifInfo.fromDto(ExifResponseDto dto)
: fileSize = dto.fileSizeInByte,
dateTimeOriginal = dto.dateTimeOriginal,
timeZone = dto.timeZone,
make = dto.make,
model = dto.model,
lens = dto.lensModel,
f = dto.fNumber?.toDouble(),
mm = dto.focalLength?.toDouble(),
iso = dto.iso?.toInt(),
exposureSeconds = _exposureTimeToSeconds(dto.exposureTime),
lat = dto.latitude?.toDouble(),
long = dto.longitude?.toDouble(),
city = dto.city,
state = dto.state,
country = dto.country,
description = dto.description,
orientation = dto.orientation;
ExifInfo({
this.id,
this.fileSize,
this.dateTimeOriginal,
this.timeZone,
this.make,
this.model,
this.lens,
this.f,
this.mm,
this.iso,
this.exposureSeconds,
this.lat,
this.long,
this.city,
this.state,
this.country,
this.description,
this.orientation,
});
ExifInfo copyWith({
Id? id,
int? fileSize,
DateTime? dateTimeOriginal,
String? timeZone,
String? make,
String? model,
String? lens,
float? f,
float? mm,
short? iso,
float? exposureSeconds,
float? lat,
float? long,
String? city,
String? state,
String? country,
String? description,
String? orientation,
}) =>
ExifInfo(
id: id ?? this.id,
fileSize: fileSize ?? this.fileSize,
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
timeZone: timeZone ?? this.timeZone,
make: make ?? this.make,
model: model ?? this.model,
lens: lens ?? this.lens,
f: f ?? this.f,
mm: mm ?? this.mm,
iso: iso ?? this.iso,
exposureSeconds: exposureSeconds ?? this.exposureSeconds,
lat: lat ?? this.lat,
long: long ?? this.long,
city: city ?? this.city,
state: state ?? this.state,
country: country ?? this.country,
description: description ?? this.description,
orientation: orientation ?? this.orientation,
);
@override
bool operator ==(other) {
if (other is! ExifInfo) return false;
return id == other.id &&
fileSize == other.fileSize &&
dateTimeOriginal == other.dateTimeOriginal &&
timeZone == other.timeZone &&
make == other.make &&
model == other.model &&
lens == other.lens &&
f == other.f &&
mm == other.mm &&
iso == other.iso &&
exposureSeconds == other.exposureSeconds &&
lat == other.lat &&
long == other.long &&
city == other.city &&
state == other.state &&
country == other.country &&
description == other.description &&
orientation == other.orientation;
}
@override
@ignore
int get hashCode =>
id.hashCode ^
fileSize.hashCode ^
dateTimeOriginal.hashCode ^
timeZone.hashCode ^
make.hashCode ^
model.hashCode ^
lens.hashCode ^
f.hashCode ^
mm.hashCode ^
iso.hashCode ^
exposureSeconds.hashCode ^
lat.hashCode ^
long.hashCode ^
city.hashCode ^
state.hashCode ^
country.hashCode ^
description.hashCode ^
orientation.hashCode;
@override
String toString() {
return """
{
id: $id,
fileSize: $fileSize,
dateTimeOriginal: $dateTimeOriginal,
timeZone: $timeZone,
make: $make,
model: $model,
lens: $lens,
f: $f,
mm: $mm,
iso: $iso,
exposureSeconds: $exposureSeconds,
lat: $lat,
long: $long,
city: $city,
state: $state,
country: $country,
description: $description,
orientation: $orientation
}""";
}
}
bool _isOrientationFlipped(String? orientation) {
final value = orientation != null ? int.tryParse(orientation) : null;
if (value == null) {
return false;
}
final isRotated90CW = value == 5 || value == 6 || value == 90;
final isRotated270CW = value == 7 || value == 8 || value == -90;
return isRotated90CW || isRotated270CW;
}
double? _exposureTimeToSeconds(String? s) {
if (s == null) {
return null;
}
double? value = double.tryParse(s);
if (value != null) {
return value;
}
final parts = s.split("/");
if (parts.length == 2) {
final numerator = double.tryParse(parts[0]);
final denominator = double.tryParse(parts[1]);
if (numerator != null && denominator != null) {
return numerator / denominator;
}
}
return null;
}

View File

@ -0,0 +1,90 @@
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
import 'package:isar/isar.dart';
part 'exif.entity.g.dart';
/// Exif information 1:1 relation with Asset
@Collection(inheritance: false)
class ExifInfo {
final Id? id;
final int? fileSize;
final DateTime? dateTimeOriginal;
final String? timeZone;
final String? make;
final String? model;
final String? lens;
final float? f;
final float? mm;
final short? iso;
final float? exposureSeconds;
final float? lat;
final float? long;
final String? city;
final String? state;
final String? country;
final String? description;
final String? orientation;
const ExifInfo({
this.id,
this.fileSize,
this.dateTimeOriginal,
this.timeZone,
this.make,
this.model,
this.lens,
this.f,
this.mm,
this.iso,
this.exposureSeconds,
this.lat,
this.long,
this.city,
this.state,
this.country,
this.description,
this.orientation,
});
static ExifInfo fromDto(domain.ExifInfo dto) => ExifInfo(
id: dto.assetId,
fileSize: dto.fileSize,
dateTimeOriginal: dto.dateTimeOriginal,
timeZone: dto.timeZone,
make: dto.make,
model: dto.model,
lens: dto.lens,
f: dto.f,
mm: dto.mm,
iso: dto.iso?.toInt(),
exposureSeconds: dto.exposureSeconds,
lat: dto.latitude,
long: dto.longitude,
city: dto.city,
state: dto.state,
country: dto.country,
description: dto.description,
orientation: dto.orientation,
);
domain.ExifInfo toDto() => domain.ExifInfo(
assetId: id,
fileSize: fileSize,
description: description,
orientation: orientation,
timeZone: timeZone,
dateTimeOriginal: dateTimeOriginal,
latitude: lat,
longitude: long,
city: city,
state: state,
country: country,
make: make,
model: model,
lens: lens,
f: f,
mm: mm,
iso: iso?.toInt(),
exposureSeconds: exposureSeconds,
);
}

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'exif_info.entity.dart';
part of 'exif.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
@ -288,9 +288,7 @@ List<IsarLinkBase<dynamic>> _exifInfoGetLinks(ExifInfo object) {
return [];
}
void _exifInfoAttach(IsarCollection<dynamic> col, Id id, ExifInfo object) {
object.id = id;
}
void _exifInfoAttach(IsarCollection<dynamic> col, Id id, ExifInfo object) {}
extension ExifInfoQueryWhereSort on QueryBuilder<ExifInfo, ExifInfo, QWhere> {
QueryBuilder<ExifInfo, ExifInfo, QAfterWhere> anyId() {

View File

@ -0,0 +1,50 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarExifRepository extends IsarDatabaseRepository
implements IExifInfoRepository {
final Isar _db;
const IsarExifRepository(this._db) : super(_db);
@override
Future<void> delete(int assetId) async {
await transaction(() async {
await _db.exifInfos.delete(assetId);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.exifInfos.clear();
});
}
@override
Future<ExifInfo?> get(int assetId) async {
return (await _db.exifInfos.get(assetId))?.toDto();
}
@override
Future<ExifInfo> update(ExifInfo exifInfo) {
return transaction(() async {
await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo));
return exifInfo;
});
}
@override
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) {
return transaction(() async {
await _db.exifInfos.putAll(
exifInfos.map(entity.ExifInfo.fromDto).toList(),
);
return exifInfos;
});
}
}

View File

@ -0,0 +1,56 @@
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:openapi/api.dart';
abstract final class ExifDtoConverter {
static ExifInfo fromDto(ExifResponseDto dto) {
return ExifInfo(
fileSize: dto.fileSizeInByte,
description: dto.description,
orientation: dto.orientation,
timeZone: dto.timeZone,
dateTimeOriginal: dto.dateTimeOriginal,
isFlipped: _isOrientationFlipped(dto.orientation),
latitude: dto.latitude?.toDouble(),
longitude: dto.longitude?.toDouble(),
city: dto.city,
state: dto.state,
country: dto.country,
make: dto.make,
model: dto.model,
lens: dto.lensModel,
f: dto.fNumber?.toDouble(),
mm: dto.focalLength?.toDouble(),
iso: dto.iso?.toInt(),
exposureSeconds: _exposureTimeToSeconds(dto.exposureTime),
);
}
static bool _isOrientationFlipped(String? orientation) {
final value = orientation == null ? null : int.tryParse(orientation);
if (value == null) {
return false;
}
final isRotated90CW = value == 5 || value == 6 || value == 90;
final isRotated270CW = value == 7 || value == 8 || value == -90;
return isRotated90CW || isRotated270CW;
}
static double? _exposureTimeToSeconds(String? s) {
if (s == null) {
return null;
}
double? value = double.tryParse(s);
if (value != null) {
return value;
}
final parts = s.split("/");
if (parts.length == 2) {
final numerator = double.tryParse(parts.firstOrNull ?? "-");
final denominator = double.tryParse(parts.lastOrNull ?? "-");
if (numerator != null && denominator != null) {
return numerator / denominator;
}
}
return null;
}
}

View File

@ -1,14 +0,0 @@
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<ExifInfo?> get(int id);
Future<ExifInfo> update(ExifInfo exifInfo);
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int id);
Future<void> clearTable();
}

View File

@ -0,0 +1,10 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'exif.provider.g.dart';
@Riverpod(keepAlive: true)
IExifInfoRepository exifRepository(ExifRepositoryRef ref) =>
IsarExifRepository(ref.watch(isarProvider));

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'exif.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$exifRepositoryHash() => r'f8f94d2a43fa79b08e58d60e81d7877825f33ec0';
/// See also [exifRepository].
@ProviderFor(exifRepository)
final exifRepositoryProvider = Provider<IExifInfoRepository>.internal(
exifRepository,
name: r'exifRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$exifRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -6,8 +6,8 @@ import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';

View File

@ -1,7 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
@ -39,7 +39,8 @@ class AssetMediaRepository implements IAssetMediaRepository {
asset.fileCreatedAt = asset.fileModifiedAt;
}
if (local.latitude != null) {
asset.exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
asset.exifInfo =
ExifInfo(latitude: local.latitude, longitude: local.longitude);
}
asset.local = local;
return asset;

View File

@ -5,9 +5,9 @@ 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/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/interfaces/auth.interface.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';

View File

@ -1,36 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
final exifInfoRepositoryProvider =
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
class ExifInfoRepository extends DatabaseRepository
implements IExifInfoRepository {
ExifInfoRepository(super.db);
@override
Future<void> delete(int id) => txn(() => db.exifInfos.delete(id));
@override
Future<ExifInfo?> get(int id) => db.exifInfos.get(id);
@override
Future<ExifInfo> update(ExifInfo exifInfo) async {
await txn(() => db.exifInfos.put(exifInfo));
return exifInfo;
}
@override
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) async {
await txn(() => db.exifInfos.putAll(exifInfos));
return exifInfos;
}
@override
Future<void> clearTable() {
return txn(() => db.exifInfos.clear());
}
}

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
@ -12,16 +13,15 @@ import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
@ -36,7 +36,7 @@ final assetServiceProvider = Provider(
(ref) => AssetService(
ref.watch(assetApiRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(exifRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(etagRepositoryProvider),
ref.watch(backupAlbumRepositoryProvider),
@ -166,17 +166,18 @@ class AssetService {
/// Loads the exif information from the database. If there is none, loads
/// the exif info from the server (remote assets only)
Future<Asset> loadExif(Asset a) async {
a.exifInfo ??= await _exifInfoRepository.get(a.id);
a.exifInfo ??= (await _exifInfoRepository.get(a.id));
// fileSize is always filled on the server but not set on client
if (a.exifInfo?.fileSize == null) {
if (a.isRemote) {
final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!);
if (dto != null && dto.exifInfo != null) {
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
final newExif = Asset.remote(dto).exifInfo!.copyWith(assetId: a.id);
a.exifInfo = newExif;
if (newExif != a.exifInfo) {
if (a.isInDb) {
_assetRepository.transaction(() => _assetRepository.update(a));
await _assetRepository
.transaction(() => _assetRepository.update(a));
} else {
debugPrint("[loadExif] parameter Asset is not from DB!");
}
@ -257,7 +258,8 @@ class AssetService {
for (var element in assets) {
element.fileCreatedAt = DateTime.parse(updatedDt);
element.exifInfo?.dateTimeOriginal = DateTime.parse(updatedDt);
element.exifInfo ??= element.exifInfo
?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt));
}
await _syncService.upsertAssetsWithExif(assets);
@ -283,8 +285,10 @@ class AssetService {
);
for (var element in assets) {
element.exifInfo?.lat = location.latitude;
element.exifInfo?.long = location.longitude;
element.exifInfo ??= element.exifInfo?.copyWith(
latitude: location.latitude,
longitude: location.longitude,
);
}
await _syncService.upsertAssetsWithExif(assets);
@ -348,7 +352,7 @@ class AssetService {
String newDescription,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
final localExifId = asset.exifInfo?.assetId;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
@ -366,14 +370,14 @@ class AssetService {
var exifInfo = await _exifInfoRepository.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
await _exifInfoRepository.update(exifInfo);
await _exifInfoRepository
.update(exifInfo.copyWith(description: description));
}
}
}
Future<String> getDescription(Asset asset) async {
final localExifId = asset.exifInfo?.id;
final localExifId = asset.exifInfo?.assetId;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {

View File

@ -11,9 +11,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@ -28,7 +30,6 @@ import 'package:immich_mobile/repositories/auth.repository.dart';
import 'package:immich_mobile/repositories/auth_api.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/network.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
@ -379,7 +380,7 @@ class BackgroundService {
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
BackupAlbumRepository backupRepository = BackupAlbumRepository(db);
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
IExifInfoRepository exifInfoRepository = IsarExifRepository(db);
ETagRepository eTagRepository = ETagRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();

View File

@ -5,15 +5,16 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
@ -149,11 +150,11 @@ class BackupVerificationService {
) async {
if (remote.checksum == local.checksum) return false;
ExifInfo? exif = remote.exifInfo;
if (exif != null && exif.lat != null) return false;
if (exif != null && exif.latitude != null) return false;
if (exif == null || exif.fileSize == null) {
final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!);
if (dto != null && dto.exifInfo != null) {
exif = ExifInfo.fromDto(dto.exifInfo!);
exif = ExifDtoConverter.fromDto(dto.exifInfo!);
}
}
final file = await local.local!.originFile;
@ -162,7 +163,7 @@ class BackupVerificationService {
if (exif.fileSize! == origSize || exif.fileSize! != origSize) {
final latLng = await local.local!.latlngAsync();
if (exif.lat == null &&
if (exif.latitude == null &&
latLng.latitude != null &&
(remote.fileCreatedAt.isAtSameMomentAs(local.fileCreatedAt) ||
remote.fileModifiedAt.isAtSameMomentAs(local.fileModifiedAt) ||
@ -215,6 +216,6 @@ final backupVerificationServiceProvider = Provider(
(ref) => BackupVerificationService(
ref.watch(fileMediaRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(exifRepositoryProvider),
),
);

View File

@ -1,16 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
final exifServiceProvider =
Provider((ref) => ExifService(ref.watch(exifInfoRepositoryProvider)));
Provider((ref) => ExifService(ref.watch(exifRepositoryProvider)));
class ExifService {
final IExifInfoRepository _exifInfoRepository;
ExifService(this._exifInfoRepository);
const ExifService(this._exifInfoRepository);
Future<void> clearTable() {
return _exifInfoRepository.clearTable();
return _exifInfoRepository.deleteAll();
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
@ -12,14 +13,13 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
@ -36,7 +36,7 @@ final syncServiceProvider = Provider(
ref.watch(albumApiRepositoryProvider),
ref.watch(albumRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(exifRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(etagRepositoryProvider),
),
@ -756,7 +756,7 @@ class SyncService {
await _assetRepository.transaction(() async {
await _assetRepository.updateAll(assets);
for (final Asset added in assets) {
added.exifInfo?.id = added.id;
added.exifInfo ??= added.exifInfo?.copyWith(assetId: added.id);
}
await _exifInfoRepository.updateAll(exifInfos);
});

View File

@ -9,9 +9,9 @@ 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/exif_info.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';

View File

@ -4,9 +4,9 @@ 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/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:isar/isar.dart';
const int targetVersion = 8;

View File

@ -2,9 +2,9 @@ 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/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/entities/asset.entity.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';

View File

@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';

View File

@ -1,12 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart';
class AssetDetails extends ConsumerWidget {
final Asset asset;

View File

@ -1,12 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
class AssetLocation extends HookConsumerWidget {
final Asset asset;

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class CameraInfo extends StatelessWidget {

View File

@ -1,8 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:url_launcher/url_launcher.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:timezone/data/latest.dart';
import 'package:timezone/timezone.dart';

View File

@ -1,3 +1,4 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
@ -7,7 +8,6 @@ import 'package:immich_mobile/interfaces/auth.interface.dart';
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:mocktail/mocktail.dart';

View File

@ -11,9 +11,9 @@ 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/exif_info.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:isar/isar.dart';