asset.dart 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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.remoteId,
  56. required this.localId,
  57. required this.deviceId,
  58. required this.ownerId,
  59. required this.fileCreatedAt,
  60. required this.fileModifiedAt,
  61. required this.updatedAt,
  62. required this.durationInSeconds,
  63. required this.type,
  64. this.width,
  65. this.height,
  66. required this.fileName,
  67. this.livePhotoVideoId,
  68. this.exifInfo,
  69. required this.isFavorite,
  70. required this.isLocal,
  71. required this.isArchived,
  72. });
  73. @ignore
  74. AssetEntity? _local;
  75. @ignore
  76. AssetEntity? get local {
  77. if (isLocal && _local == null) {
  78. _local = AssetEntity(
  79. id: localId,
  80. typeInt: isImage ? 1 : 2,
  81. width: width ?? 0,
  82. height: height ?? 0,
  83. duration: durationInSeconds,
  84. createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
  85. modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
  86. title: fileName,
  87. );
  88. }
  89. return _local;
  90. }
  91. Id id = Isar.autoIncrement;
  92. @Index(unique: false, replace: false, type: IndexType.hash)
  93. String? remoteId;
  94. @Index(
  95. unique: false,
  96. replace: false,
  97. type: IndexType.hash,
  98. composite: [CompositeIndex('deviceId')],
  99. )
  100. String localId;
  101. int deviceId;
  102. int ownerId;
  103. DateTime fileCreatedAt;
  104. DateTime fileModifiedAt;
  105. DateTime updatedAt;
  106. int durationInSeconds;
  107. @Enumerated(EnumType.ordinal)
  108. AssetType type;
  109. short? width;
  110. short? height;
  111. String fileName;
  112. String? livePhotoVideoId;
  113. bool isFavorite;
  114. bool isLocal;
  115. bool isArchived;
  116. @ignore
  117. ExifInfo? exifInfo;
  118. @ignore
  119. bool get isInDb => id != Isar.autoIncrement;
  120. @ignore
  121. String get name => p.withoutExtension(fileName);
  122. @ignore
  123. bool get isRemote => remoteId != null;
  124. @ignore
  125. bool get isImage => type == AssetType.image;
  126. @ignore
  127. Duration get duration => Duration(seconds: durationInSeconds);
  128. @override
  129. bool operator ==(other) {
  130. if (other is! Asset) return false;
  131. return id == other.id &&
  132. remoteId == other.remoteId &&
  133. localId == other.localId &&
  134. deviceId == other.deviceId &&
  135. ownerId == other.ownerId &&
  136. fileCreatedAt == other.fileCreatedAt &&
  137. fileModifiedAt == other.fileModifiedAt &&
  138. updatedAt == other.updatedAt &&
  139. durationInSeconds == other.durationInSeconds &&
  140. type == other.type &&
  141. width == other.width &&
  142. height == other.height &&
  143. fileName == other.fileName &&
  144. livePhotoVideoId == other.livePhotoVideoId &&
  145. isFavorite == other.isFavorite &&
  146. isLocal == other.isLocal &&
  147. isArchived == other.isArchived;
  148. }
  149. @override
  150. @ignore
  151. int get hashCode =>
  152. id.hashCode ^
  153. remoteId.hashCode ^
  154. localId.hashCode ^
  155. deviceId.hashCode ^
  156. ownerId.hashCode ^
  157. fileCreatedAt.hashCode ^
  158. fileModifiedAt.hashCode ^
  159. updatedAt.hashCode ^
  160. durationInSeconds.hashCode ^
  161. type.hashCode ^
  162. width.hashCode ^
  163. height.hashCode ^
  164. fileName.hashCode ^
  165. livePhotoVideoId.hashCode ^
  166. isFavorite.hashCode ^
  167. isLocal.hashCode ^
  168. isArchived.hashCode;
  169. bool updateFromAssetEntity(AssetEntity ae) {
  170. // TODO check more fields;
  171. // width and height are most important because local assets require these
  172. final bool hasChanges =
  173. isLocal == false || width != ae.width || height != ae.height;
  174. if (hasChanges) {
  175. isLocal = true;
  176. width = ae.width;
  177. height = ae.height;
  178. }
  179. return hasChanges;
  180. }
  181. Asset withUpdatesFromDto(AssetResponseDto dto) =>
  182. Asset.remote(dto).updateFromDb(this);
  183. Asset updateFromDb(Asset a) {
  184. assert(localId == a.localId);
  185. assert(deviceId == a.deviceId);
  186. id = a.id;
  187. isLocal |= a.isLocal;
  188. remoteId ??= a.remoteId;
  189. width ??= a.width;
  190. height ??= a.height;
  191. exifInfo ??= a.exifInfo;
  192. exifInfo?.id = id;
  193. if (!isRemote) {
  194. isArchived = a.isArchived;
  195. }
  196. return this;
  197. }
  198. Future<void> put(Isar db) async {
  199. await db.assets.put(this);
  200. if (exifInfo != null) {
  201. exifInfo!.id = id;
  202. await db.exifInfos.put(exifInfo!);
  203. }
  204. }
  205. /// compares assets by [ownerId], [deviceId], [localId]
  206. static int compareByOwnerDeviceLocalId(Asset a, Asset b) {
  207. final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
  208. if (ownerIdOrder != 0) {
  209. return ownerIdOrder;
  210. }
  211. final int deviceIdOrder = a.deviceId.compareTo(b.deviceId);
  212. if (deviceIdOrder != 0) {
  213. return deviceIdOrder;
  214. }
  215. final int localIdOrder = a.localId.compareTo(b.localId);
  216. return localIdOrder;
  217. }
  218. /// compares assets by [ownerId], [deviceId], [localId], [fileModifiedAt]
  219. static int compareByOwnerDeviceLocalIdModified(Asset a, Asset b) {
  220. final int order = compareByOwnerDeviceLocalId(a, b);
  221. return order != 0 ? order : a.fileModifiedAt.compareTo(b.fileModifiedAt);
  222. }
  223. static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
  224. static int compareByLocalId(Asset a, Asset b) =>
  225. a.localId.compareTo(b.localId);
  226. @override
  227. String toString() {
  228. return """
  229. {
  230. "remoteId": "${remoteId ?? "N/A"}",
  231. "localId": "$localId",
  232. "deviceId": "$deviceId",
  233. "ownerId": "$ownerId",
  234. "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
  235. "fileCreatedAt": "$fileCreatedAt",
  236. "fileModifiedAt": "$fileModifiedAt",
  237. "updatedAt": "$updatedAt",
  238. "durationInSeconds": $durationInSeconds,
  239. "type": "$type",
  240. "fileName": "$fileName",
  241. "isFavorite": $isFavorite,
  242. "isLocal": $isLocal,
  243. "width": ${width ?? "N/A"},
  244. "height": ${height ?? "N/A"},
  245. "isArchived": $isArchived
  246. }""";
  247. }
  248. }
  249. enum AssetType {
  250. // do not change this order!
  251. other,
  252. image,
  253. video,
  254. audio,
  255. }
  256. extension AssetTypeEnumHelper on AssetTypeEnum {
  257. AssetType toAssetType() {
  258. switch (this) {
  259. case AssetTypeEnum.IMAGE:
  260. return AssetType.image;
  261. case AssetTypeEnum.VIDEO:
  262. return AssetType.video;
  263. case AssetTypeEnum.AUDIO:
  264. return AssetType.audio;
  265. case AssetTypeEnum.OTHER:
  266. return AssetType.other;
  267. }
  268. throw Exception();
  269. }
  270. }
  271. extension AssetsHelper on IsarCollection<Asset> {
  272. Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
  273. ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
  274. Future<int> deleteAllByLocalId(Iterable<String> ids) =>
  275. ids.isEmpty ? Future.value(0) : _local(ids).deleteAll();
  276. Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
  277. ids.isEmpty ? Future.value([]) : _remote(ids).findAll();
  278. Future<List<Asset>> getAllByLocalId(Iterable<String> ids) =>
  279. ids.isEmpty ? Future.value([]) : _local(ids).findAll();
  280. QueryBuilder<Asset, Asset, QAfterWhereClause> _remote(Iterable<String> ids) =>
  281. where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
  282. QueryBuilder<Asset, Asset, QAfterWhereClause> _local(Iterable<String> ids) {
  283. return where().anyOf(
  284. ids,
  285. (q, String e) =>
  286. q.localIdDeviceIdEqualTo(e, Store.get(StoreKey.deviceIdHash)),
  287. );
  288. }
  289. }