mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feature(mobile): sync assets, albums & users to local database on device (#1759)
* feature(mobile): sync assets, albums & users to local database on device * try to fix tests * move DB sync operations to new SyncService * clear db on user logout * fix reason for endless loading timeline * fix error when deleting album * fix thumbnail of device albums * add a few comments * fix Hive box not open in album service when loading local assets * adjust tests to int IDs * fix bug: show all albums when Recent is selected * update generated api * reworked Recents album isAll handling * guard against wrongly interleaved sync operations * fix: timeline asset ordering (sort asset state by created at) * fix: sort assets in albums by created at
This commit is contained in:
committed by
GitHub
parent
8f11529a75
commit
8708867c1c
@@ -1,132 +1,153 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
part 'album.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class Album {
|
||||
Album.remote(AlbumResponseDto dto)
|
||||
: remoteId = dto.id,
|
||||
name = dto.albumName,
|
||||
createdAt = DateTime.parse(dto.createdAt),
|
||||
// TODO add modifiedAt to server
|
||||
modifiedAt = DateTime.parse(dto.createdAt),
|
||||
shared = dto.shared,
|
||||
ownerId = dto.ownerId,
|
||||
albumThumbnailAssetId = dto.albumThumbnailAssetId,
|
||||
assetCount = dto.assetCount,
|
||||
sharedUsers = dto.sharedUsers.map((e) => User.fromDto(e)).toList(),
|
||||
assets = dto.assets.map(Asset.remote).toList();
|
||||
|
||||
@protected
|
||||
Album({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
required this.createdAt,
|
||||
required this.modifiedAt,
|
||||
required this.shared,
|
||||
required this.assetCount,
|
||||
this.albumThumbnailAssetId,
|
||||
this.sharedUsers = const [],
|
||||
this.assets = const [],
|
||||
});
|
||||
|
||||
Id id = Isar.autoIncrement;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? localId;
|
||||
String name;
|
||||
String ownerId;
|
||||
DateTime createdAt;
|
||||
DateTime modifiedAt;
|
||||
bool shared;
|
||||
String? albumThumbnailAssetId;
|
||||
int assetCount;
|
||||
List<User> sharedUsers = const [];
|
||||
List<Asset> assets = const [];
|
||||
final IsarLink<User> owner = IsarLink<User>();
|
||||
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
|
||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||
|
||||
List<Asset> _sortedAssets = [];
|
||||
|
||||
@ignore
|
||||
List<Asset> get sortedAssets => _sortedAssets;
|
||||
|
||||
@ignore
|
||||
bool get isRemote => remoteId != null;
|
||||
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
|
||||
String get id => isRemote ? remoteId! : localId!;
|
||||
@ignore
|
||||
int get assetCount => assets.length;
|
||||
|
||||
@ignore
|
||||
String? get ownerId => owner.value?.id;
|
||||
|
||||
Future<void> loadSortedAssets() async {
|
||||
_sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Album) return false;
|
||||
return remoteId == other.remoteId &&
|
||||
return id == other.id &&
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
name == other.name &&
|
||||
createdAt == other.createdAt &&
|
||||
modifiedAt == other.modifiedAt &&
|
||||
shared == other.shared &&
|
||||
ownerId == other.ownerId &&
|
||||
albumThumbnailAssetId == other.albumThumbnailAssetId;
|
||||
owner.value == other.owner.value &&
|
||||
thumbnail.value == other.thumbnail.value &&
|
||||
sharedUsers.length == other.sharedUsers.length &&
|
||||
assets.length == other.assets.length;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
localId.hashCode ^
|
||||
name.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
modifiedAt.hashCode ^
|
||||
shared.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
albumThumbnailAssetId.hashCode;
|
||||
owner.value.hashCode ^
|
||||
thumbnail.value.hashCode ^
|
||||
sharedUsers.length.hashCode ^
|
||||
assets.length.hashCode;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json["remoteId"] = remoteId;
|
||||
json["localId"] = localId;
|
||||
json["name"] = name;
|
||||
json["ownerId"] = ownerId;
|
||||
json["createdAt"] = createdAt.millisecondsSinceEpoch;
|
||||
json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
|
||||
json["shared"] = shared;
|
||||
json["albumThumbnailAssetId"] = albumThumbnailAssetId;
|
||||
json["assetCount"] = assetCount;
|
||||
json["sharedUsers"] = sharedUsers;
|
||||
json["assets"] = assets;
|
||||
return json;
|
||||
static Album local(AssetPathEntity ape) {
|
||||
final Album a = Album(
|
||||
name: ape.name,
|
||||
createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||
modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
|
||||
shared: false,
|
||||
);
|
||||
a.owner.value = Store.get(StoreKey.currentUser);
|
||||
a.localId = ape.id;
|
||||
return a;
|
||||
}
|
||||
|
||||
static Album? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
return Album(
|
||||
remoteId: json["remoteId"],
|
||||
localId: json["localId"],
|
||||
name: json["name"],
|
||||
ownerId: json["ownerId"],
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json["createdAt"],
|
||||
isUtc: true,
|
||||
),
|
||||
modifiedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json["modifiedAt"],
|
||||
isUtc: true,
|
||||
),
|
||||
shared: json["shared"],
|
||||
albumThumbnailAssetId: json["albumThumbnailAssetId"],
|
||||
assetCount: json["assetCount"],
|
||||
sharedUsers: _listFromJson<User>(json["sharedUsers"], User.fromJson),
|
||||
assets: _listFromJson<Asset>(json["assets"], Asset.fromJson),
|
||||
);
|
||||
static Future<Album> remote(AlbumResponseDto dto) async {
|
||||
final Isar db = Isar.getInstance()!;
|
||||
final Album a = Album(
|
||||
remoteId: dto.id,
|
||||
name: dto.albumName,
|
||||
createdAt: DateTime.parse(dto.createdAt),
|
||||
modifiedAt: DateTime.parse(dto.updatedAt),
|
||||
shared: dto.shared,
|
||||
);
|
||||
a.owner.value = await db.users.getById(dto.ownerId);
|
||||
if (dto.albumThumbnailAssetId != null) {
|
||||
a.thumbnail.value = await db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
return null;
|
||||
if (dto.sharedUsers.isNotEmpty) {
|
||||
final users = await db.users
|
||||
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
|
||||
a.sharedUsers.addAll(users.cast());
|
||||
}
|
||||
if (dto.assets.isNotEmpty) {
|
||||
final assets =
|
||||
await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id));
|
||||
a.assets.addAll(assets);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
List<T> _listFromJson<T>(
|
||||
dynamic json,
|
||||
T? Function(dynamic) fromJson,
|
||||
) {
|
||||
final result = <T>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final entry in json) {
|
||||
final value = fromJson(entry);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
extension AssetsHelper on IsarCollection<Album> {
|
||||
Future<void> store(Album a) async {
|
||||
await put(a);
|
||||
await a.owner.save();
|
||||
await a.thumbnail.save();
|
||||
await a.sharedUsers.save();
|
||||
await a.assets.save();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
Future<List<Asset>> getAssets({
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
}) async {
|
||||
final assetEntities = await getAssetListRange(start: start, end: end);
|
||||
return assetEntities.map(Asset.local).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
1391
mobile/lib/shared/models/album.g.dart
Normal file
1391
mobile/lib/shared/models/album.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,65 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.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';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
part 'asset.g.dart';
|
||||
|
||||
/// Asset (online or local)
|
||||
@Collection(inheritance: false)
|
||||
class Asset {
|
||||
Asset.remote(AssetResponseDto remote)
|
||||
: remoteId = remote.id,
|
||||
fileCreatedAt = DateTime.parse(remote.fileCreatedAt),
|
||||
fileModifiedAt = DateTime.parse(remote.fileModifiedAt),
|
||||
isLocal = false,
|
||||
fileCreatedAt = DateTime.parse(remote.fileCreatedAt).toUtc(),
|
||||
fileModifiedAt = DateTime.parse(remote.fileModifiedAt).toUtc(),
|
||||
updatedAt = DateTime.parse(remote.updatedAt).toUtc(),
|
||||
durationInSeconds = remote.duration.toDuration().inSeconds,
|
||||
fileName = p.basename(remote.originalPath),
|
||||
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||
livePhotoVideoId = remote.livePhotoVideoId,
|
||||
deviceAssetId = remote.deviceAssetId,
|
||||
deviceId = remote.deviceId,
|
||||
ownerId = remote.ownerId,
|
||||
latitude = remote.exifInfo?.latitude?.toDouble(),
|
||||
longitude = remote.exifInfo?.longitude?.toDouble(),
|
||||
localId = remote.deviceAssetId,
|
||||
deviceId = fastHash(remote.deviceId),
|
||||
ownerId = fastHash(remote.ownerId),
|
||||
exifInfo =
|
||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||
isFavorite = remote.isFavorite;
|
||||
|
||||
Asset.local(AssetEntity local, String owner)
|
||||
Asset.local(AssetEntity local)
|
||||
: localId = local.id,
|
||||
latitude = local.latitude,
|
||||
longitude = local.longitude,
|
||||
isLocal = true,
|
||||
durationInSeconds = local.duration,
|
||||
height = local.height,
|
||||
width = local.width,
|
||||
fileName = local.title!,
|
||||
deviceAssetId = local.id,
|
||||
deviceId = Hive.box(userInfoBox).get(deviceIdKey),
|
||||
ownerId = owner,
|
||||
deviceId = Store.get(StoreKey.deviceIdHash),
|
||||
ownerId = Store.get<User>(StoreKey.currentUser)!.isarId,
|
||||
fileModifiedAt = local.modifiedDateTime.toUtc(),
|
||||
updatedAt = local.modifiedDateTime.toUtc(),
|
||||
isFavorite = local.isFavorite,
|
||||
fileCreatedAt = local.createDateTime.toUtc() {
|
||||
if (fileCreatedAt.year == 1970) {
|
||||
fileCreatedAt = fileModifiedAt;
|
||||
}
|
||||
if (local.latitude != null) {
|
||||
exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
Asset({
|
||||
this.localId,
|
||||
this.remoteId,
|
||||
required this.deviceAssetId,
|
||||
required this.localId,
|
||||
required this.deviceId,
|
||||
required this.ownerId,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.updatedAt,
|
||||
required this.durationInSeconds,
|
||||
this.width,
|
||||
this.height,
|
||||
@@ -62,21 +67,22 @@ class Asset {
|
||||
this.livePhotoVideoId,
|
||||
this.exifInfo,
|
||||
required this.isFavorite,
|
||||
required this.isLocal,
|
||||
});
|
||||
|
||||
@ignore
|
||||
AssetEntity? _local;
|
||||
|
||||
@ignore
|
||||
AssetEntity? get local {
|
||||
if (isLocal && _local == null) {
|
||||
_local = AssetEntity(
|
||||
id: localId!.toString(),
|
||||
id: localId.toString(),
|
||||
typeInt: isImage ? 1 : 2,
|
||||
width: width!,
|
||||
height: height!,
|
||||
duration: durationInSeconds,
|
||||
createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
|
||||
title: fileName,
|
||||
);
|
||||
@@ -84,110 +90,136 @@ class Asset {
|
||||
return _local;
|
||||
}
|
||||
|
||||
String? localId;
|
||||
Id id = Isar.autoIncrement;
|
||||
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
|
||||
String deviceAssetId;
|
||||
@Index(
|
||||
unique: true,
|
||||
replace: false,
|
||||
type: IndexType.hash,
|
||||
composite: [CompositeIndex('deviceId')],
|
||||
)
|
||||
String localId;
|
||||
|
||||
String deviceId;
|
||||
int deviceId;
|
||||
|
||||
String ownerId;
|
||||
int ownerId;
|
||||
|
||||
DateTime fileCreatedAt;
|
||||
|
||||
DateTime fileModifiedAt;
|
||||
|
||||
double? latitude;
|
||||
|
||||
double? longitude;
|
||||
DateTime updatedAt;
|
||||
|
||||
int durationInSeconds;
|
||||
|
||||
int? width;
|
||||
short? width;
|
||||
|
||||
int? height;
|
||||
short? height;
|
||||
|
||||
String fileName;
|
||||
|
||||
String? livePhotoVideoId;
|
||||
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
bool isFavorite;
|
||||
|
||||
String get id => isLocal ? localId.toString() : remoteId!;
|
||||
bool isLocal;
|
||||
|
||||
@ignore
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
@ignore
|
||||
bool get isInDb => id != Isar.autoIncrement;
|
||||
|
||||
@ignore
|
||||
String get name => p.withoutExtension(fileName);
|
||||
|
||||
@ignore
|
||||
bool get isRemote => remoteId != null;
|
||||
|
||||
bool get isLocal => localId != null;
|
||||
|
||||
@ignore
|
||||
bool get isImage => durationInSeconds == 0;
|
||||
|
||||
@ignore
|
||||
Duration get duration => Duration(seconds: durationInSeconds);
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
return id == other.id && isLocal == other.isLocal;
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
// methods below are only required for caching as JSON
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json["localId"] = localId;
|
||||
json["remoteId"] = remoteId;
|
||||
json["deviceAssetId"] = deviceAssetId;
|
||||
json["deviceId"] = deviceId;
|
||||
json["ownerId"] = ownerId;
|
||||
json["fileCreatedAt"] = fileCreatedAt.millisecondsSinceEpoch;
|
||||
json["fileModifiedAt"] = fileModifiedAt.millisecondsSinceEpoch;
|
||||
json["latitude"] = latitude;
|
||||
json["longitude"] = longitude;
|
||||
json["durationInSeconds"] = durationInSeconds;
|
||||
json["width"] = width;
|
||||
json["height"] = height;
|
||||
json["fileName"] = fileName;
|
||||
json["livePhotoVideoId"] = livePhotoVideoId;
|
||||
json["isFavorite"] = isFavorite;
|
||||
if (exifInfo != null) {
|
||||
json["exifInfo"] = exifInfo!.toJson();
|
||||
bool updateFromAssetEntity(AssetEntity ae) {
|
||||
// TODO check more fields;
|
||||
// width and height are most important because local assets require these
|
||||
final bool hasChanges =
|
||||
isLocal == false || width != ae.width || height != ae.height;
|
||||
if (hasChanges) {
|
||||
isLocal = true;
|
||||
width = ae.width;
|
||||
height = ae.height;
|
||||
}
|
||||
return json;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
static Asset? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
return Asset(
|
||||
localId: json["localId"],
|
||||
remoteId: json["remoteId"],
|
||||
deviceAssetId: json["deviceAssetId"],
|
||||
deviceId: json["deviceId"],
|
||||
ownerId: json["ownerId"],
|
||||
fileCreatedAt:
|
||||
DateTime.fromMillisecondsSinceEpoch(json["fileCreatedAt"], isUtc: true),
|
||||
fileModifiedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json["fileModifiedAt"],
|
||||
isUtc: true,
|
||||
),
|
||||
latitude: json["latitude"],
|
||||
longitude: json["longitude"],
|
||||
durationInSeconds: json["durationInSeconds"],
|
||||
width: json["width"],
|
||||
height: json["height"],
|
||||
fileName: json["fileName"],
|
||||
livePhotoVideoId: json["livePhotoVideoId"],
|
||||
exifInfo: ExifInfo.fromJson(json["exifInfo"]),
|
||||
isFavorite: json["isFavorite"],
|
||||
);
|
||||
Asset withUpdatesFromDto(AssetResponseDto dto) =>
|
||||
Asset.remote(dto).updateFromDb(this);
|
||||
|
||||
Asset updateFromDb(Asset a) {
|
||||
assert(localId == a.localId);
|
||||
assert(deviceId == a.deviceId);
|
||||
id = a.id;
|
||||
isLocal |= a.isLocal;
|
||||
remoteId ??= a.remoteId;
|
||||
width ??= a.width;
|
||||
height ??= a.height;
|
||||
exifInfo ??= a.exifInfo;
|
||||
exifInfo?.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
Future<void> put(Isar db) async {
|
||||
await db.assets.put(this);
|
||||
if (exifInfo != null) {
|
||||
exifInfo!.id = id;
|
||||
await db.exifInfos.put(exifInfo!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int compareByDeviceIdLocalId(Asset a, Asset b) {
|
||||
final int order = a.deviceId.compareTo(b.deviceId);
|
||||
return order == 0 ? a.localId.compareTo(b.localId) : order;
|
||||
}
|
||||
|
||||
static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
|
||||
|
||||
static int compareByLocalId(Asset a, Asset b) =>
|
||||
a.localId.compareTo(b.localId);
|
||||
}
|
||||
|
||||
extension AssetsHelper on IsarCollection<Asset> {
|
||||
Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
|
||||
ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
|
||||
Future<int> deleteAllByLocalId(Iterable<String> ids) =>
|
||||
ids.isEmpty ? Future.value(0) : _local(ids).deleteAll();
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||
ids.isEmpty ? Future.value([]) : _remote(ids).findAll();
|
||||
Future<List<Asset>> getAllByLocalId(Iterable<String> ids) =>
|
||||
ids.isEmpty ? Future.value([]) : _local(ids).findAll();
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> _remote(Iterable<String> ids) =>
|
||||
where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> _local(Iterable<String> ids) {
|
||||
return where().anyOf(
|
||||
ids,
|
||||
(q, String e) =>
|
||||
q.localIdDeviceIdEqualTo(e, Store.get(StoreKey.deviceIdHash)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
2244
mobile/lib/shared/models/asset.g.dart
Normal file
2244
mobile/lib/shared/models/asset.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,86 +1,93 @@
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
|
||||
part 'exif_info.g.dart';
|
||||
|
||||
/// Exif information 1:1 relation with Asset
|
||||
@Collection(inheritance: false)
|
||||
class ExifInfo {
|
||||
Id? id;
|
||||
int? fileSize;
|
||||
String? make;
|
||||
String? model;
|
||||
String? orientation;
|
||||
String? lensModel;
|
||||
double? fNumber;
|
||||
double? focalLength;
|
||||
int? iso;
|
||||
double? exposureTime;
|
||||
String? lens;
|
||||
float? f;
|
||||
float? mm;
|
||||
short? iso;
|
||||
float? exposureSeconds;
|
||||
float? lat;
|
||||
float? long;
|
||||
String? city;
|
||||
String? state;
|
||||
String? country;
|
||||
|
||||
@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
|
||||
double? get latitude => lat;
|
||||
|
||||
@ignore
|
||||
double? get longitude => long;
|
||||
|
||||
ExifInfo.fromDto(ExifResponseDto dto)
|
||||
: fileSize = dto.fileSizeInByte,
|
||||
make = dto.make,
|
||||
model = dto.model,
|
||||
orientation = dto.orientation,
|
||||
lensModel = dto.lensModel,
|
||||
fNumber = dto.fNumber?.toDouble(),
|
||||
focalLength = dto.focalLength?.toDouble(),
|
||||
lens = dto.lensModel,
|
||||
f = dto.fNumber?.toDouble(),
|
||||
mm = dto.focalLength?.toDouble(),
|
||||
iso = dto.iso?.toInt(),
|
||||
exposureTime = dto.exposureTime?.toDouble(),
|
||||
exposureSeconds = _exposureTimeToSeconds(dto.exposureTime),
|
||||
lat = dto.latitude?.toDouble(),
|
||||
long = dto.longitude?.toDouble(),
|
||||
city = dto.city,
|
||||
state = dto.state,
|
||||
country = dto.country;
|
||||
|
||||
// stuff below is only required for caching as JSON
|
||||
|
||||
ExifInfo(
|
||||
ExifInfo({
|
||||
this.fileSize,
|
||||
this.make,
|
||||
this.model,
|
||||
this.orientation,
|
||||
this.lensModel,
|
||||
this.fNumber,
|
||||
this.focalLength,
|
||||
this.lens,
|
||||
this.f,
|
||||
this.mm,
|
||||
this.iso,
|
||||
this.exposureTime,
|
||||
this.exposureSeconds,
|
||||
this.lat,
|
||||
this.long,
|
||||
this.city,
|
||||
this.state,
|
||||
this.country,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json["fileSize"] = fileSize;
|
||||
json["make"] = make;
|
||||
json["model"] = model;
|
||||
json["orientation"] = orientation;
|
||||
json["lensModel"] = lensModel;
|
||||
json["fNumber"] = fNumber;
|
||||
json["focalLength"] = focalLength;
|
||||
json["iso"] = iso;
|
||||
json["exposureTime"] = exposureTime;
|
||||
json["city"] = city;
|
||||
json["state"] = state;
|
||||
json["country"] = country;
|
||||
return json;
|
||||
}
|
||||
|
||||
static ExifInfo? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
return ExifInfo(
|
||||
json["fileSize"],
|
||||
json["make"],
|
||||
json["model"],
|
||||
json["orientation"],
|
||||
json["lensModel"],
|
||||
json["fNumber"],
|
||||
json["focalLength"],
|
||||
json["iso"],
|
||||
json["exposureTime"],
|
||||
json["city"],
|
||||
json["state"],
|
||||
json["country"],
|
||||
);
|
||||
}
|
||||
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) {
|
||||
return parts[0].toDouble() / parts[1].toDouble();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
2304
mobile/lib/shared/models/exif_info.g.dart
Normal file
2304
mobile/lib/shared/models/exif_info.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
@@ -25,26 +26,28 @@ class Store {
|
||||
|
||||
/// Returns the stored value for the given key, or the default value if null
|
||||
static T? get<T>(StoreKey key, [T? defaultValue]) =>
|
||||
_cache[key._id] ?? defaultValue;
|
||||
_cache[key.id] ?? defaultValue;
|
||||
|
||||
/// Stores the value synchronously in the cache and asynchronously in the DB
|
||||
static Future<void> put<T>(StoreKey key, T value) {
|
||||
_cache[key._id] = value;
|
||||
return _db.writeTxn(() => _db.storeValues.put(StoreValue._of(value, key)));
|
||||
_cache[key.id] = value;
|
||||
return _db.writeTxn(
|
||||
() async => _db.storeValues.put(await StoreValue._of(value, key)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Removes the value synchronously from the cache and asynchronously from the DB
|
||||
static Future<void> delete(StoreKey key) {
|
||||
_cache[key._id] = null;
|
||||
return _db.writeTxn(() => _db.storeValues.delete(key._id));
|
||||
_cache[key.id] = null;
|
||||
return _db.writeTxn(() => _db.storeValues.delete(key.id));
|
||||
}
|
||||
|
||||
/// Fills the cache with the values from the DB
|
||||
static _populateCache() {
|
||||
for (StoreKey key in StoreKey.values) {
|
||||
final StoreValue? value = _db.storeValues.getSync(key._id);
|
||||
final StoreValue? value = _db.storeValues.getSync(key.id);
|
||||
if (value != null) {
|
||||
_cache[key._id] = value._extract(key);
|
||||
_cache[key.id] = value._extract(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,17 +70,22 @@ class StoreValue {
|
||||
int? intValue;
|
||||
String? strValue;
|
||||
|
||||
T? _extract<T>(StoreKey key) => key._isInt
|
||||
? intValue
|
||||
: (key._fromJson != null
|
||||
? key._fromJson!(json.decode(strValue!))
|
||||
T? _extract<T>(StoreKey key) => key.isInt
|
||||
? (key.fromDb == null ? intValue : key.fromDb!.call(Store._db, intValue!))
|
||||
: (key.fromJson != null
|
||||
? key.fromJson!(json.decode(strValue!))
|
||||
: strValue);
|
||||
static StoreValue _of(dynamic value, StoreKey key) => StoreValue(
|
||||
key._id,
|
||||
intValue: key._isInt ? value : null,
|
||||
strValue: key._isInt
|
||||
static Future<StoreValue> _of(dynamic value, StoreKey key) async =>
|
||||
StoreValue(
|
||||
key.id,
|
||||
intValue: key.isInt
|
||||
? (key.toDb == null
|
||||
? value
|
||||
: await key.toDb!.call(Store._db, value))
|
||||
: null,
|
||||
strValue: key.isInt
|
||||
? null
|
||||
: (key._fromJson == null ? value : json.encode(value.toJson())),
|
||||
: (key.fromJson == null ? value : json.encode(value.toJson())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,11 +94,28 @@ class StoreValue {
|
||||
enum StoreKey {
|
||||
userRemoteId(0),
|
||||
assetETag(1),
|
||||
currentUser(2, isInt: true, fromDb: _getUser, toDb: _toUser),
|
||||
deviceIdHash(3, isInt: true),
|
||||
deviceId(4),
|
||||
;
|
||||
|
||||
// ignore: unused_element
|
||||
const StoreKey(this._id, [this._isInt = false, this._fromJson]);
|
||||
final int _id;
|
||||
final bool _isInt;
|
||||
final Function(dynamic)? _fromJson;
|
||||
const StoreKey(
|
||||
this.id, {
|
||||
this.isInt = false,
|
||||
this.fromDb,
|
||||
this.toDb,
|
||||
// ignore: unused_element
|
||||
this.fromJson,
|
||||
});
|
||||
final int id;
|
||||
final bool isInt;
|
||||
final dynamic Function(Isar, int)? fromDb;
|
||||
final Future<int> Function(Isar, dynamic)? toDb;
|
||||
final Function(dynamic)? fromJson;
|
||||
}
|
||||
|
||||
User? _getUser(Isar db, int i) => db.users.getSync(i);
|
||||
Future<int> _toUser(Isar db, dynamic u) {
|
||||
User user = (u as User);
|
||||
return db.users.put(user);
|
||||
}
|
||||
|
@@ -1,94 +1,63 @@
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class User {
|
||||
User({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
required this.email,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.profileImagePath,
|
||||
required this.isAdmin,
|
||||
required this.oauthId,
|
||||
});
|
||||
|
||||
Id get isarId => fastHash(id);
|
||||
|
||||
User.fromDto(UserResponseDto dto)
|
||||
: id = dto.id,
|
||||
updatedAt = dto.updatedAt != null
|
||||
? DateTime.parse(dto.updatedAt!).toUtc()
|
||||
: DateTime.now().toUtc(),
|
||||
email = dto.email,
|
||||
firstName = dto.firstName,
|
||||
lastName = dto.lastName,
|
||||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = dto.isAdmin,
|
||||
oauthId = dto.oauthId;
|
||||
isAdmin = dto.isAdmin;
|
||||
|
||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||
String id;
|
||||
DateTime updatedAt;
|
||||
String email;
|
||||
String firstName;
|
||||
String lastName;
|
||||
String profileImagePath;
|
||||
bool isAdmin;
|
||||
String oauthId;
|
||||
@Backlink(to: 'owner')
|
||||
final IsarLinks<Album> albums = IsarLinks<Album>();
|
||||
@Backlink(to: 'sharedUsers')
|
||||
final IsarLinks<Album> sharedAlbums = IsarLinks<Album>();
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is! User) return false;
|
||||
return id == other.id &&
|
||||
updatedAt == other.updatedAt &&
|
||||
email == other.email &&
|
||||
firstName == other.firstName &&
|
||||
lastName == other.lastName &&
|
||||
profileImagePath == other.profileImagePath &&
|
||||
isAdmin == other.isAdmin &&
|
||||
oauthId == other.oauthId;
|
||||
isAdmin == other.isAdmin;
|
||||
}
|
||||
|
||||
@override
|
||||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
updatedAt.hashCode ^
|
||||
email.hashCode ^
|
||||
firstName.hashCode ^
|
||||
lastName.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
oauthId.hashCode;
|
||||
|
||||
UserResponseDto toDto() {
|
||||
return UserResponseDto(
|
||||
id: id,
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
profileImagePath: profileImagePath,
|
||||
createdAt: '',
|
||||
isAdmin: isAdmin,
|
||||
shouldChangePassword: false,
|
||||
oauthId: oauthId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json["id"] = id;
|
||||
json["email"] = email;
|
||||
json["firstName"] = firstName;
|
||||
json["lastName"] = lastName;
|
||||
json["profileImagePath"] = profileImagePath;
|
||||
json["isAdmin"] = isAdmin;
|
||||
json["oauthId"] = oauthId;
|
||||
return json;
|
||||
}
|
||||
|
||||
static User? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
return User(
|
||||
id: json["id"],
|
||||
email: json["email"],
|
||||
firstName: json["firstName"],
|
||||
lastName: json["lastName"],
|
||||
profileImagePath: json["profileImagePath"],
|
||||
isAdmin: json["isAdmin"],
|
||||
oauthId: json["oauthId"],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
isAdmin.hashCode;
|
||||
}
|
||||
|
1338
mobile/lib/shared/models/user.g.dart
Normal file
1338
mobile/lib/shared/models/user.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user