asset.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. import 'package:immich_mobile/shared/models/exif_info.dart';
  2. import 'package:immich_mobile/shared/models/store.dart';
  3. import 'package:immich_mobile/utils/hash.dart';
  4. import 'package:isar/isar.dart';
  5. import 'package:openapi/api.dart';
  6. import 'package:photo_manager/photo_manager.dart';
  7. import 'package:immich_mobile/utils/builtin_extensions.dart';
  8. import 'package:path/path.dart' as p;
  9. part 'asset.g.dart';
  10. /// Asset (online or local)
  11. @Collection(inheritance: false)
  12. class Asset {
  13. Asset.remote(AssetResponseDto remote)
  14. : remoteId = remote.id,
  15. isLocal = false,
  16. fileCreatedAt = DateTime.parse(remote.fileCreatedAt),
  17. fileModifiedAt = DateTime.parse(remote.fileModifiedAt),
  18. updatedAt = DateTime.parse(remote.updatedAt),
  19. durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0,
  20. type = remote.type.toAssetType(),
  21. fileName = p.basename(remote.originalPath),
  22. height = remote.exifInfo?.exifImageHeight?.toInt(),
  23. width = remote.exifInfo?.exifImageWidth?.toInt(),
  24. livePhotoVideoId = remote.livePhotoVideoId,
  25. localId = remote.deviceAssetId,
  26. deviceId = fastHash(remote.deviceId),
  27. ownerId = fastHash(remote.ownerId),
  28. exifInfo =
  29. remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
  30. isFavorite = remote.isFavorite,
  31. isArchived = remote.isArchived;
  32. Asset.local(AssetEntity local)
  33. : localId = local.id,
  34. isLocal = true,
  35. durationInSeconds = local.duration,
  36. type = AssetType.values[local.typeInt],
  37. height = local.height,
  38. width = local.width,
  39. fileName = local.title!,
  40. deviceId = Store.get(StoreKey.deviceIdHash),
  41. ownerId = Store.get(StoreKey.currentUser).isarId,
  42. fileModifiedAt = local.modifiedDateTime,
  43. updatedAt = local.modifiedDateTime,
  44. isFavorite = local.isFavorite,
  45. isArchived = false,
  46. fileCreatedAt = local.createDateTime {
  47. if (fileCreatedAt.year == 1970) {
  48. fileCreatedAt = fileModifiedAt;
  49. }
  50. if (local.latitude != null) {
  51. exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
  52. }
  53. }
  54. Asset({
  55. this.id = Isar.autoIncrement,
  56. this.remoteId,
  57. required this.localId,
  58. required this.deviceId,
  59. required this.ownerId,
  60. required this.fileCreatedAt,
  61. required this.fileModifiedAt,
  62. required this.updatedAt,
  63. required this.durationInSeconds,
  64. required this.type,
  65. this.width,
  66. this.height,
  67. required this.fileName,
  68. this.livePhotoVideoId,
  69. this.exifInfo,
  70. required this.isFavorite,
  71. required this.isLocal,
  72. required this.isArchived,
  73. });
  74. @ignore
  75. AssetEntity? _local;
  76. @ignore
  77. AssetEntity? get local {
  78. if (isLocal && _local == null) {
  79. _local = AssetEntity(
  80. id: localId,
  81. typeInt: isImage ? 1 : 2,
  82. width: width ?? 0,
  83. height: height ?? 0,
  84. duration: durationInSeconds,
  85. createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
  86. modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
  87. title: fileName,
  88. );
  89. }
  90. return _local;
  91. }
  92. Id id = Isar.autoIncrement;
  93. @Index(unique: false, replace: false, type: IndexType.hash)
  94. String? remoteId;
  95. @Index(
  96. unique: false,
  97. replace: false,
  98. type: IndexType.hash,
  99. composite: [CompositeIndex('deviceId')],
  100. )
  101. String localId;
  102. int deviceId;
  103. int ownerId;
  104. DateTime fileCreatedAt;
  105. DateTime fileModifiedAt;
  106. DateTime updatedAt;
  107. int durationInSeconds;
  108. @Enumerated(EnumType.ordinal)
  109. AssetType type;
  110. short? width;
  111. short? height;
  112. String fileName;
  113. String? livePhotoVideoId;
  114. bool isFavorite;
  115. /// `true` if this [Asset] is present on the device
  116. bool isLocal;
  117. bool isArchived;
  118. @ignore
  119. ExifInfo? exifInfo;
  120. @ignore
  121. bool get isInDb => id != Isar.autoIncrement;
  122. @ignore
  123. String get name => p.withoutExtension(fileName);
  124. /// `true` if this [Asset] is present on the server
  125. @ignore
  126. bool get isRemote => remoteId != null;
  127. @ignore
  128. bool get isImage => type == AssetType.image;
  129. @ignore
  130. AssetState get storage {
  131. if (isRemote && isLocal) {
  132. return AssetState.merged;
  133. } else if (isRemote) {
  134. return AssetState.remote;
  135. } else if (isLocal) {
  136. return AssetState.local;
  137. } else {
  138. throw Exception("Asset has illegal state: $this");
  139. }
  140. }
  141. @ignore
  142. Duration get duration => Duration(seconds: durationInSeconds);
  143. @override
  144. bool operator ==(other) {
  145. if (other is! Asset) return false;
  146. return id == other.id &&
  147. remoteId == other.remoteId &&
  148. localId == other.localId &&
  149. deviceId == other.deviceId &&
  150. ownerId == other.ownerId &&
  151. fileCreatedAt == other.fileCreatedAt &&
  152. fileModifiedAt == other.fileModifiedAt &&
  153. updatedAt == other.updatedAt &&
  154. durationInSeconds == other.durationInSeconds &&
  155. type == other.type &&
  156. width == other.width &&
  157. height == other.height &&
  158. fileName == other.fileName &&
  159. livePhotoVideoId == other.livePhotoVideoId &&
  160. isFavorite == other.isFavorite &&
  161. isLocal == other.isLocal &&
  162. isArchived == other.isArchived;
  163. }
  164. @override
  165. @ignore
  166. int get hashCode =>
  167. id.hashCode ^
  168. remoteId.hashCode ^
  169. localId.hashCode ^
  170. deviceId.hashCode ^
  171. ownerId.hashCode ^
  172. fileCreatedAt.hashCode ^
  173. fileModifiedAt.hashCode ^
  174. updatedAt.hashCode ^
  175. durationInSeconds.hashCode ^
  176. type.hashCode ^
  177. width.hashCode ^
  178. height.hashCode ^
  179. fileName.hashCode ^
  180. livePhotoVideoId.hashCode ^
  181. isFavorite.hashCode ^
  182. isLocal.hashCode ^
  183. isArchived.hashCode;
  184. /// Returns `true` if this [Asset] can updated with values from parameter [a]
  185. bool canUpdate(Asset a) {
  186. assert(isInDb);
  187. assert(localId == a.localId);
  188. assert(deviceId == a.deviceId);
  189. assert(a.storage != AssetState.merged);
  190. return a.updatedAt.isAfter(updatedAt) ||
  191. a.isRemote && !isRemote ||
  192. a.isLocal && !isLocal ||
  193. width == null && a.width != null ||
  194. height == null && a.height != null ||
  195. exifInfo == null && a.exifInfo != null ||
  196. livePhotoVideoId == null && a.livePhotoVideoId != null ||
  197. !isRemote && a.isRemote && isFavorite != a.isFavorite ||
  198. !isRemote && a.isRemote && isArchived != a.isArchived;
  199. }
  200. /// Returns a new [Asset] with values from this and merged & updated with [a]
  201. Asset updatedCopy(Asset a) {
  202. assert(canUpdate(a));
  203. if (a.updatedAt.isAfter(updatedAt)) {
  204. // take most values from newer asset
  205. // keep vales that can never be set by the asset not in DB
  206. if (a.isRemote) {
  207. return a._copyWith(
  208. id: id,
  209. isLocal: isLocal,
  210. width: a.width ?? width,
  211. height: a.height ?? height,
  212. exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
  213. );
  214. } else {
  215. return a._copyWith(
  216. id: id,
  217. remoteId: remoteId,
  218. livePhotoVideoId: livePhotoVideoId,
  219. isFavorite: isFavorite,
  220. isArchived: isArchived,
  221. );
  222. }
  223. } else {
  224. // fill in potentially missing values, i.e. merge assets
  225. if (a.isRemote) {
  226. // values from remote take precedence
  227. return _copyWith(
  228. remoteId: a.remoteId,
  229. width: a.width,
  230. height: a.height,
  231. livePhotoVideoId: a.livePhotoVideoId,
  232. // isFavorite + isArchived are not set by device-only assets
  233. isFavorite: a.isFavorite,
  234. isArchived: a.isArchived,
  235. exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
  236. );
  237. } else {
  238. // add only missing values (and set isLocal to true)
  239. return _copyWith(
  240. isLocal: true,
  241. width: width ?? a.width,
  242. height: height ?? a.height,
  243. exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
  244. );
  245. }
  246. }
  247. }
  248. Asset _copyWith({
  249. Id? id,
  250. String? remoteId,
  251. String? localId,
  252. int? deviceId,
  253. int? ownerId,
  254. DateTime? fileCreatedAt,
  255. DateTime? fileModifiedAt,
  256. DateTime? updatedAt,
  257. int? durationInSeconds,
  258. AssetType? type,
  259. short? width,
  260. short? height,
  261. String? fileName,
  262. String? livePhotoVideoId,
  263. bool? isFavorite,
  264. bool? isLocal,
  265. bool? isArchived,
  266. ExifInfo? exifInfo,
  267. }) =>
  268. Asset(
  269. id: id ?? this.id,
  270. remoteId: remoteId ?? this.remoteId,
  271. localId: localId ?? this.localId,
  272. deviceId: deviceId ?? this.deviceId,
  273. ownerId: ownerId ?? this.ownerId,
  274. fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
  275. fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt,
  276. updatedAt: updatedAt ?? this.updatedAt,
  277. durationInSeconds: durationInSeconds ?? this.durationInSeconds,
  278. type: type ?? this.type,
  279. width: width ?? this.width,
  280. height: height ?? this.height,
  281. fileName: fileName ?? this.fileName,
  282. livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
  283. isFavorite: isFavorite ?? this.isFavorite,
  284. isLocal: isLocal ?? this.isLocal,
  285. isArchived: isArchived ?? this.isArchived,
  286. exifInfo: exifInfo ?? this.exifInfo,
  287. );
  288. Future<void> put(Isar db) async {
  289. await db.assets.put(this);
  290. if (exifInfo != null) {
  291. exifInfo!.id = id;
  292. await db.exifInfos.put(exifInfo!);
  293. }
  294. }
  295. /// compares assets by [ownerId], [deviceId], [localId]
  296. static int compareByOwnerDeviceLocalId(Asset a, Asset b) {
  297. final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
  298. if (ownerIdOrder != 0) {
  299. return ownerIdOrder;
  300. }
  301. final int deviceIdOrder = a.deviceId.compareTo(b.deviceId);
  302. if (deviceIdOrder != 0) {
  303. return deviceIdOrder;
  304. }
  305. final int localIdOrder = a.localId.compareTo(b.localId);
  306. return localIdOrder;
  307. }
  308. /// compares assets by [ownerId], [deviceId], [localId], [fileModifiedAt]
  309. static int compareByOwnerDeviceLocalIdModified(Asset a, Asset b) {
  310. final int order = compareByOwnerDeviceLocalId(a, b);
  311. return order != 0 ? order : a.fileModifiedAt.compareTo(b.fileModifiedAt);
  312. }
  313. static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
  314. static int compareByLocalId(Asset a, Asset b) =>
  315. a.localId.compareTo(b.localId);
  316. @override
  317. String toString() {
  318. return """
  319. {
  320. "remoteId": "${remoteId ?? "N/A"}",
  321. "localId": "$localId",
  322. "deviceId": "$deviceId",
  323. "ownerId": "$ownerId",
  324. "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
  325. "fileCreatedAt": "$fileCreatedAt",
  326. "fileModifiedAt": "$fileModifiedAt",
  327. "updatedAt": "$updatedAt",
  328. "durationInSeconds": $durationInSeconds,
  329. "type": "$type",
  330. "fileName": "$fileName",
  331. "isFavorite": $isFavorite,
  332. "isLocal": $isLocal,
  333. "width": ${width ?? "N/A"},
  334. "height": ${height ?? "N/A"},
  335. "isArchived": $isArchived
  336. }""";
  337. }
  338. }
  339. enum AssetType {
  340. // do not change this order!
  341. other,
  342. image,
  343. video,
  344. audio,
  345. }
  346. extension AssetTypeEnumHelper on AssetTypeEnum {
  347. AssetType toAssetType() {
  348. switch (this) {
  349. case AssetTypeEnum.IMAGE:
  350. return AssetType.image;
  351. case AssetTypeEnum.VIDEO:
  352. return AssetType.video;
  353. case AssetTypeEnum.AUDIO:
  354. return AssetType.audio;
  355. case AssetTypeEnum.OTHER:
  356. return AssetType.other;
  357. }
  358. throw Exception();
  359. }
  360. }
  361. /// Describes where the information of this asset came from:
  362. /// only from the local device, only from the remote server or merged from both
  363. enum AssetState {
  364. local,
  365. remote,
  366. merged,
  367. }
  368. extension AssetsHelper on IsarCollection<Asset> {
  369. Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
  370. ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
  371. Future<int> deleteAllByLocalId(Iterable<String> ids) =>
  372. ids.isEmpty ? Future.value(0) : _local(ids).deleteAll();
  373. Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
  374. ids.isEmpty ? Future.value([]) : _remote(ids).findAll();
  375. Future<List<Asset>> getAllByLocalId(Iterable<String> ids) =>
  376. ids.isEmpty ? Future.value([]) : _local(ids).findAll();
  377. QueryBuilder<Asset, Asset, QAfterWhereClause> _remote(Iterable<String> ids) =>
  378. where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
  379. QueryBuilder<Asset, Asset, QAfterWhereClause> _local(Iterable<String> ids) {
  380. return where().anyOf(
  381. ids,
  382. (q, String e) =>
  383. q.localIdDeviceIdEqualTo(e, Store.get(StoreKey.deviceIdHash)),
  384. );
  385. }
  386. }