feature(mobile): hash assets & sync via checksum (#2592)
* compare different sha1 implementations * remove openssl sha1 * sync via checksum * hash assets in batches * hash in background, show spinner in tab * undo tmp changes * migrate by clearing assets * ignore duplicate assets * error handling * trigger sync/merge after download and update view * review feedback improvements * hash in background isolate on iOS * rework linking assets with existing from DB * fine-grained errors on unique index violation * hash lenth validation * revert compute in background on iOS * ignore duplicate assets on device * fix bug with batching based on accumulated size --------- Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
parent
053a0482b4
commit
73075c64d1
28 changed files with 2315 additions and 507 deletions
|
@ -84,6 +84,7 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||
implementation "com.google.guava:guava:$guava_version"
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package app.alextran.immich
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.security.MessageDigest
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService`
|
||||
|
@ -16,6 +21,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var context: Context? = null
|
||||
private val sha1: MessageDigest = MessageDigest.getInstance("SHA-1")
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
|
@ -70,9 +76,40 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||
"isIgnoringBatteryOptimizations" -> {
|
||||
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
|
||||
}
|
||||
"digestFiles" -> {
|
||||
val args = call.arguments<ArrayList<String>>()!!
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val buf = ByteArray(BUFSIZE)
|
||||
val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
|
||||
val hashes = arrayOfNulls<ByteArray>(args.size)
|
||||
for (i in args.indices) {
|
||||
val path = args[i]
|
||||
var len = 0
|
||||
try {
|
||||
val file = FileInputStream(path)
|
||||
try {
|
||||
while (true) {
|
||||
len = file.read(buf)
|
||||
if (len != BUFSIZE) break
|
||||
digest.update(buf)
|
||||
}
|
||||
} finally {
|
||||
file.close()
|
||||
}
|
||||
digest.update(buf, 0, len)
|
||||
hashes[i] = digest.digest()
|
||||
} catch (e: Exception) {
|
||||
// skip this file
|
||||
Log.w(TAG, "Failed to hash file ${args[i]}: $e")
|
||||
}
|
||||
}
|
||||
result.success(hashes.asList())
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val BUFSIZE = 2*1024*1024;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.8.20'
|
||||
ext.kotlin_coroutines_version = '1.7.1'
|
||||
ext.work_version = '2.7.1'
|
||||
ext.concurrent_version = '1.1.0'
|
||||
ext.guava_version = '31.0.1-android'
|
||||
|
|
|
@ -19,9 +19,11 @@ import 'package:immich_mobile/modules/settings/providers/notification_permission
|
|||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
|
@ -91,6 +93,7 @@ Future<Isar> loadDb() async {
|
|||
DuplicatedAssetSchema,
|
||||
LoggerMessageSchema,
|
||||
ETagSchema,
|
||||
Platform.isAndroid ? AndroidDeviceAssetSchema : IOSDeviceAssetSchema,
|
||||
],
|
||||
directory: dir.path,
|
||||
maxSizeMiB: 256,
|
||||
|
|
|
@ -2,6 +2,7 @@ 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/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
@ -12,9 +13,13 @@ import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
|||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||
final ImageViewerService _imageViewerService;
|
||||
final ShareService _shareService;
|
||||
final AlbumService _albumService;
|
||||
|
||||
ImageViewerStateNotifier(this._imageViewerService, this._shareService)
|
||||
: super(
|
||||
ImageViewerStateNotifier(
|
||||
this._imageViewerService,
|
||||
this._shareService,
|
||||
this._albumService,
|
||||
) : super(
|
||||
ImageViewerPageState(
|
||||
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||
),
|
||||
|
@ -34,6 +39,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||
toastType: ToastType.success,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
_albumService.refreshDeviceAlbums();
|
||||
} else {
|
||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||
ImmichToast.show(
|
||||
|
@ -66,5 +72,6 @@ final imageViewerStateProvider =
|
|||
((ref) => ImageViewerStateNotifier(
|
||||
ref.watch(imageViewerServiceProvider),
|
||||
ref.watch(shareServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
)),
|
||||
);
|
||||
|
|
|
@ -72,15 +72,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (!asset.isLocal)
|
||||
IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
Icons.cloud_download_outlined,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
),
|
||||
if (asset.storage == AssetState.merged)
|
||||
if (asset.storage == AssetState.remote)
|
||||
IconButton(
|
||||
onPressed: onDownloadPressed,
|
||||
icon: Icon(
|
||||
|
|
|
@ -287,7 +287,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
isFavorite: asset().isFavorite,
|
||||
onMoreInfoPressed: showInfo,
|
||||
onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
|
||||
onDownloadPressed: asset().storage == AssetState.local
|
||||
onDownloadPressed: asset().isLocal
|
||||
? null
|
||||
: () =>
|
||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||
|
|
|
@ -132,6 +132,17 @@ class BackgroundService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> digestFile(String path) {
|
||||
return _foregroundChannel.invokeMethod<Uint8List>("digestFile", [path]);
|
||||
}
|
||||
|
||||
Future<List<Uint8List?>?> digestFiles(List<String> paths) {
|
||||
return _foregroundChannel.invokeListMethod<Uint8List?>(
|
||||
"digestFiles",
|
||||
paths,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates the notification shown by the background service
|
||||
Future<bool?> _updateNotification({
|
||||
String? title,
|
||||
|
|
|
@ -47,11 +47,11 @@ class HomePage extends HookConsumerWidget {
|
|||
|
||||
useEffect(
|
||||
() {
|
||||
ref.watch(websocketProvider.notifier).connect();
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
|
||||
selectionEnabledHook.addListener(() {
|
||||
multiselectEnabled.state = selectionEnabledHook.value;
|
||||
|
@ -144,7 +144,7 @@ class HomePage extends HookConsumerWidget {
|
|||
);
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
await ref
|
||||
.watch(assetProvider.notifier)
|
||||
.read(assetProvider.notifier)
|
||||
.toggleArchive(remoteAssets, true);
|
||||
|
||||
final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
|
||||
|
@ -163,7 +163,7 @@ class HomePage extends HookConsumerWidget {
|
|||
void onDelete() async {
|
||||
processing.value = true;
|
||||
try {
|
||||
await ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
||||
await ref.read(assetProvider.notifier).deleteAssets(selection.value);
|
||||
selectionEnabledHook.value = false;
|
||||
} finally {
|
||||
processing.value = false;
|
||||
|
|
|
@ -166,23 +166,10 @@ extension AssetsHelper on IsarCollection<Album> {
|
|||
}
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
Future<List<Asset>> getAssets({
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final assetEntities = await getAssetListRange(start: start, end: end);
|
||||
if (excludedAssets != null) {
|
||||
return assetEntities
|
||||
.where((e) => !excludedAssets.contains(e.id))
|
||||
.map(Asset.local)
|
||||
.toList();
|
||||
}
|
||||
return assetEntities.map(Asset.local).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
extension AssetPathEntityHelper on AssetPathEntity {
|
||||
String get eTagKeyAssetCount => "device-album-$id-asset-count";
|
||||
}
|
||||
|
|
10
mobile/lib/shared/models/android_device_asset.dart
Normal file
10
mobile/lib/shared/models/android_device_asset.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'android_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class AndroidDeviceAsset extends DeviceAsset {
|
||||
AndroidDeviceAsset({required this.id, required super.hash});
|
||||
Id id;
|
||||
}
|
493
mobile/lib/shared/models/android_device_asset.g.dart
Normal file
493
mobile/lib/shared/models/android_device_asset.g.dart
Normal file
|
@ -0,0 +1,493 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'android_device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetAndroidDeviceAssetCollection on Isar {
|
||||
IsarCollection<AndroidDeviceAsset> get androidDeviceAssets =>
|
||||
this.collection();
|
||||
}
|
||||
|
||||
const AndroidDeviceAssetSchema = CollectionSchema(
|
||||
name: r'AndroidDeviceAsset',
|
||||
id: -6758387181232899335,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
)
|
||||
},
|
||||
estimateSize: _androidDeviceAssetEstimateSize,
|
||||
serialize: _androidDeviceAssetSerialize,
|
||||
deserialize: _androidDeviceAssetDeserialize,
|
||||
deserializeProp: _androidDeviceAssetDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _androidDeviceAssetGetId,
|
||||
getLinks: _androidDeviceAssetGetLinks,
|
||||
attach: _androidDeviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _androidDeviceAssetEstimateSize(
|
||||
AndroidDeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _androidDeviceAssetSerialize(
|
||||
AndroidDeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
}
|
||||
|
||||
AndroidDeviceAsset _androidDeviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = AndroidDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: id,
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _androidDeviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _androidDeviceAssetGetId(AndroidDeviceAsset object) {
|
||||
return object.id;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _androidDeviceAssetGetLinks(
|
||||
AndroidDeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _androidDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, AndroidDeviceAsset object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhere> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhere
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idGreaterThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idLessThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryFilter
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
|
||||
idBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryObject
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQueryLinks
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortBy> {}
|
||||
|
||||
extension AndroidDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
|
||||
thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct> {
|
||||
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct>
|
||||
distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AndroidDeviceAssetQueryProperty
|
||||
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<AndroidDeviceAsset, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AndroidDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
|
@ -14,7 +16,7 @@ part 'asset.g.dart';
|
|||
class Asset {
|
||||
Asset.remote(AssetResponseDto remote)
|
||||
: remoteId = remote.id,
|
||||
isLocal = false,
|
||||
checksum = remote.checksum,
|
||||
fileCreatedAt = remote.fileCreatedAt,
|
||||
fileModifiedAt = remote.fileModifiedAt,
|
||||
updatedAt = remote.updatedAt,
|
||||
|
@ -24,23 +26,20 @@ class Asset {
|
|||
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||
livePhotoVideoId = remote.livePhotoVideoId,
|
||||
localId = remote.deviceAssetId,
|
||||
deviceId = fastHash(remote.deviceId),
|
||||
ownerId = fastHash(remote.ownerId),
|
||||
exifInfo =
|
||||
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
|
||||
isFavorite = remote.isFavorite,
|
||||
isArchived = remote.isArchived;
|
||||
|
||||
Asset.local(AssetEntity local)
|
||||
Asset.local(AssetEntity local, List<int> hash)
|
||||
: localId = local.id,
|
||||
isLocal = true,
|
||||
checksum = base64.encode(hash),
|
||||
durationInSeconds = local.duration,
|
||||
type = AssetType.values[local.typeInt],
|
||||
height = local.height,
|
||||
width = local.width,
|
||||
fileName = local.title!,
|
||||
deviceId = Store.get(StoreKey.deviceIdHash),
|
||||
ownerId = Store.get(StoreKey.currentUser).isarId,
|
||||
fileModifiedAt = local.modifiedDateTime,
|
||||
updatedAt = local.modifiedDateTime,
|
||||
|
@ -53,13 +52,15 @@ class Asset {
|
|||
if (local.latitude != null) {
|
||||
exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
|
||||
}
|
||||
_local = local;
|
||||
assert(hash.length == 20, "invalid SHA1 hash");
|
||||
}
|
||||
|
||||
Asset({
|
||||
this.id = Isar.autoIncrement,
|
||||
required this.checksum,
|
||||
this.remoteId,
|
||||
required this.localId,
|
||||
required this.deviceId,
|
||||
required this.ownerId,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
|
@ -72,7 +73,6 @@ class Asset {
|
|||
this.livePhotoVideoId,
|
||||
this.exifInfo,
|
||||
required this.isFavorite,
|
||||
required this.isLocal,
|
||||
required this.isArchived,
|
||||
});
|
||||
|
||||
|
@ -83,7 +83,7 @@ class Asset {
|
|||
AssetEntity? get local {
|
||||
if (isLocal && _local == null) {
|
||||
_local = AssetEntity(
|
||||
id: localId,
|
||||
id: localId!,
|
||||
typeInt: isImage ? 1 : 2,
|
||||
width: width ?? 0,
|
||||
height: height ?? 0,
|
||||
|
@ -98,18 +98,21 @@ class Asset {
|
|||
|
||||
Id id = Isar.autoIncrement;
|
||||
|
||||
/// stores the raw SHA1 bytes as a base64 String
|
||||
/// because Isar cannot sort lists of byte arrays
|
||||
@Index(
|
||||
unique: true,
|
||||
replace: false,
|
||||
type: IndexType.hash,
|
||||
composite: [CompositeIndex("ownerId")],
|
||||
)
|
||||
String checksum;
|
||||
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
|
||||
@Index(
|
||||
unique: false,
|
||||
replace: false,
|
||||
type: IndexType.hash,
|
||||
composite: [CompositeIndex('deviceId')],
|
||||
)
|
||||
String localId;
|
||||
|
||||
int deviceId;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? localId;
|
||||
|
||||
int ownerId;
|
||||
|
||||
|
@ -134,14 +137,15 @@ class Asset {
|
|||
|
||||
bool isFavorite;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
bool isLocal;
|
||||
|
||||
bool isArchived;
|
||||
|
||||
@ignore
|
||||
ExifInfo? exifInfo;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
|
||||
@ignore
|
||||
bool get isInDb => id != Isar.autoIncrement;
|
||||
|
||||
|
@ -175,9 +179,9 @@ class Asset {
|
|||
bool operator ==(other) {
|
||||
if (other is! Asset) return false;
|
||||
return id == other.id &&
|
||||
checksum == other.checksum &&
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
deviceId == other.deviceId &&
|
||||
ownerId == other.ownerId &&
|
||||
fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) &&
|
||||
fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) &&
|
||||
|
@ -197,9 +201,9 @@ class Asset {
|
|||
@ignore
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
checksum.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
localId.hashCode ^
|
||||
deviceId.hashCode ^
|
||||
ownerId.hashCode ^
|
||||
fileCreatedAt.hashCode ^
|
||||
fileModifiedAt.hashCode ^
|
||||
|
@ -217,8 +221,7 @@ class Asset {
|
|||
/// Returns `true` if this [Asset] can updated with values from parameter [a]
|
||||
bool canUpdate(Asset a) {
|
||||
assert(isInDb);
|
||||
assert(localId == a.localId);
|
||||
assert(deviceId == a.deviceId);
|
||||
assert(checksum == a.checksum);
|
||||
assert(a.storage != AssetState.merged);
|
||||
return a.updatedAt.isAfter(updatedAt) ||
|
||||
a.isRemote && !isRemote ||
|
||||
|
@ -239,11 +242,18 @@ class Asset {
|
|||
if (a.isRemote) {
|
||||
return a._copyWith(
|
||||
id: id,
|
||||
isLocal: isLocal,
|
||||
localId: localId,
|
||||
width: a.width ?? width,
|
||||
height: a.height ?? height,
|
||||
exifInfo: a.exifInfo?.copyWith(id: 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),
|
||||
);
|
||||
} else {
|
||||
return a._copyWith(
|
||||
id: id,
|
||||
|
@ -270,7 +280,7 @@ class Asset {
|
|||
} else {
|
||||
// add only missing values (and set isLocal to true)
|
||||
return _copyWith(
|
||||
isLocal: true,
|
||||
localId: localId ?? a.localId,
|
||||
width: width ?? a.width,
|
||||
height: height ?? a.height,
|
||||
exifInfo: exifInfo ?? a.exifInfo?.copyWith(id: id),
|
||||
|
@ -281,9 +291,9 @@ class Asset {
|
|||
|
||||
Asset _copyWith({
|
||||
Id? id,
|
||||
String? checksum,
|
||||
String? remoteId,
|
||||
String? localId,
|
||||
int? deviceId,
|
||||
int? ownerId,
|
||||
DateTime? fileCreatedAt,
|
||||
DateTime? fileModifiedAt,
|
||||
|
@ -295,15 +305,14 @@ class Asset {
|
|||
String? fileName,
|
||||
String? livePhotoVideoId,
|
||||
bool? isFavorite,
|
||||
bool? isLocal,
|
||||
bool? isArchived,
|
||||
ExifInfo? exifInfo,
|
||||
}) =>
|
||||
Asset(
|
||||
id: id ?? this.id,
|
||||
checksum: checksum ?? this.checksum,
|
||||
remoteId: remoteId ?? this.remoteId,
|
||||
localId: localId ?? this.localId,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
|
||||
fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt,
|
||||
|
@ -315,7 +324,6 @@ class Asset {
|
|||
fileName: fileName ?? this.fileName,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isLocal: isLocal ?? this.isLocal,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
exifInfo: exifInfo ?? this.exifInfo,
|
||||
);
|
||||
|
@ -328,39 +336,36 @@ class Asset {
|
|||
}
|
||||
}
|
||||
|
||||
/// compares assets by [ownerId], [deviceId], [localId]
|
||||
static int compareByOwnerDeviceLocalId(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) {
|
||||
return ownerIdOrder;
|
||||
}
|
||||
final int deviceIdOrder = a.deviceId.compareTo(b.deviceId);
|
||||
if (deviceIdOrder != 0) {
|
||||
return deviceIdOrder;
|
||||
}
|
||||
final int localIdOrder = a.localId.compareTo(b.localId);
|
||||
return localIdOrder;
|
||||
}
|
||||
|
||||
/// compares assets by [ownerId], [deviceId], [localId], [fileModifiedAt]
|
||||
static int compareByOwnerDeviceLocalIdModified(Asset a, Asset b) {
|
||||
final int order = compareByOwnerDeviceLocalId(a, b);
|
||||
return order != 0 ? order : a.fileModifiedAt.compareTo(b.fileModifiedAt);
|
||||
}
|
||||
|
||||
static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
|
||||
|
||||
static int compareByLocalId(Asset a, Asset b) =>
|
||||
a.localId.compareTo(b.localId);
|
||||
static int compareByChecksum(Asset a, Asset b) =>
|
||||
a.checksum.compareTo(b.checksum);
|
||||
|
||||
static int compareByOwnerChecksum(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) return ownerIdOrder;
|
||||
return compareByChecksum(a, b);
|
||||
}
|
||||
|
||||
static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) {
|
||||
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
|
||||
if (ownerIdOrder != 0) return ownerIdOrder;
|
||||
final int checksumOrder = compareByChecksum(a, b);
|
||||
if (checksumOrder != 0) return checksumOrder;
|
||||
final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt);
|
||||
if (createdOrder != 0) return createdOrder;
|
||||
return a.fileModifiedAt.compareTo(b.fileModifiedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return """
|
||||
{
|
||||
"id": ${id == Isar.autoIncrement ? '"N/A"' : id},
|
||||
"remoteId": "${remoteId ?? "N/A"}",
|
||||
"localId": "$localId",
|
||||
"deviceId": "$deviceId",
|
||||
"ownerId": "$ownerId",
|
||||
"localId": "${localId ?? "N/A"}",
|
||||
"checksum": "$checksum",
|
||||
"ownerId": $ownerId,
|
||||
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
|
||||
"fileCreatedAt": "$fileCreatedAt",
|
||||
"fileModifiedAt": "$fileModifiedAt",
|
||||
|
@ -369,9 +374,8 @@ class Asset {
|
|||
"type": "$type",
|
||||
"fileName": "$fileName",
|
||||
"isFavorite": $isFavorite,
|
||||
"isLocal": $isLocal,
|
||||
"isRemote: $isRemote,
|
||||
"storage": $storage,
|
||||
"storage": "$storage",
|
||||
"width": ${width ?? "N/A"},
|
||||
"height": ${height ?? "N/A"},
|
||||
"isArchived": $isArchived
|
||||
|
@ -424,10 +428,6 @@ extension AssetsHelper on IsarCollection<Asset> {
|
|||
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)),
|
||||
);
|
||||
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ const AssetSchema = CollectionSchema(
|
|||
name: r'Asset',
|
||||
id: -2933289051367723566,
|
||||
properties: {
|
||||
r'deviceId': PropertySchema(
|
||||
r'checksum': PropertySchema(
|
||||
id: 0,
|
||||
name: r'deviceId',
|
||||
type: IsarType.long,
|
||||
name: r'checksum',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'durationInSeconds': PropertySchema(
|
||||
id: 1,
|
||||
|
@ -57,44 +57,39 @@ const AssetSchema = CollectionSchema(
|
|||
name: r'isFavorite',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isLocal': PropertySchema(
|
||||
id: 8,
|
||||
name: r'isLocal',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'livePhotoVideoId': PropertySchema(
|
||||
id: 9,
|
||||
id: 8,
|
||||
name: r'livePhotoVideoId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 10,
|
||||
id: 9,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'ownerId': PropertySchema(
|
||||
id: 11,
|
||||
id: 10,
|
||||
name: r'ownerId',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 12,
|
||||
id: 11,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'type': PropertySchema(
|
||||
id: 13,
|
||||
id: 12,
|
||||
name: r'type',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AssettypeEnumValueMap,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 14,
|
||||
id: 13,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'width': PropertySchema(
|
||||
id: 15,
|
||||
id: 14,
|
||||
name: r'width',
|
||||
type: IsarType.int,
|
||||
)
|
||||
|
@ -105,6 +100,24 @@ const AssetSchema = CollectionSchema(
|
|||
deserializeProp: _assetDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'checksum_ownerId': IndexSchema(
|
||||
id: 5611361749756160119,
|
||||
name: r'checksum_ownerId',
|
||||
unique: true,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'checksum',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
),
|
||||
IndexPropertySchema(
|
||||
name: r'ownerId',
|
||||
type: IndexType.value,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
),
|
||||
r'remoteId': IndexSchema(
|
||||
id: 6301175856541681032,
|
||||
name: r'remoteId',
|
||||
|
@ -118,9 +131,9 @@ const AssetSchema = CollectionSchema(
|
|||
)
|
||||
],
|
||||
),
|
||||
r'localId_deviceId': IndexSchema(
|
||||
id: 7649417350086526165,
|
||||
name: r'localId_deviceId',
|
||||
r'localId': IndexSchema(
|
||||
id: 1199848425898359622,
|
||||
name: r'localId',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
|
@ -128,11 +141,6 @@ const AssetSchema = CollectionSchema(
|
|||
name: r'localId',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
),
|
||||
IndexPropertySchema(
|
||||
name: r'deviceId',
|
||||
type: IndexType.value,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -151,6 +159,7 @@ int _assetEstimateSize(
|
|||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.checksum.length * 3;
|
||||
bytesCount += 3 + object.fileName.length * 3;
|
||||
{
|
||||
final value = object.livePhotoVideoId;
|
||||
|
@ -158,7 +167,12 @@ int _assetEstimateSize(
|
|||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
bytesCount += 3 + object.localId.length * 3;
|
||||
{
|
||||
final value = object.localId;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.remoteId;
|
||||
if (value != null) {
|
||||
|
@ -174,7 +188,7 @@ void _assetSerialize(
|
|||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeLong(offsets[0], object.deviceId);
|
||||
writer.writeString(offsets[0], object.checksum);
|
||||
writer.writeLong(offsets[1], object.durationInSeconds);
|
||||
writer.writeDateTime(offsets[2], object.fileCreatedAt);
|
||||
writer.writeDateTime(offsets[3], object.fileModifiedAt);
|
||||
|
@ -182,14 +196,13 @@ void _assetSerialize(
|
|||
writer.writeInt(offsets[5], object.height);
|
||||
writer.writeBool(offsets[6], object.isArchived);
|
||||
writer.writeBool(offsets[7], object.isFavorite);
|
||||
writer.writeBool(offsets[8], object.isLocal);
|
||||
writer.writeString(offsets[9], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[10], object.localId);
|
||||
writer.writeLong(offsets[11], object.ownerId);
|
||||
writer.writeString(offsets[12], object.remoteId);
|
||||
writer.writeByte(offsets[13], object.type.index);
|
||||
writer.writeDateTime(offsets[14], object.updatedAt);
|
||||
writer.writeInt(offsets[15], object.width);
|
||||
writer.writeString(offsets[8], object.livePhotoVideoId);
|
||||
writer.writeString(offsets[9], object.localId);
|
||||
writer.writeLong(offsets[10], object.ownerId);
|
||||
writer.writeString(offsets[11], object.remoteId);
|
||||
writer.writeByte(offsets[12], object.type.index);
|
||||
writer.writeDateTime(offsets[13], object.updatedAt);
|
||||
writer.writeInt(offsets[14], object.width);
|
||||
}
|
||||
|
||||
Asset _assetDeserialize(
|
||||
|
@ -199,7 +212,7 @@ Asset _assetDeserialize(
|
|||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = Asset(
|
||||
deviceId: reader.readLong(offsets[0]),
|
||||
checksum: reader.readString(offsets[0]),
|
||||
durationInSeconds: reader.readLong(offsets[1]),
|
||||
fileCreatedAt: reader.readDateTime(offsets[2]),
|
||||
fileModifiedAt: reader.readDateTime(offsets[3]),
|
||||
|
@ -208,15 +221,14 @@ Asset _assetDeserialize(
|
|||
id: id,
|
||||
isArchived: reader.readBool(offsets[6]),
|
||||
isFavorite: reader.readBool(offsets[7]),
|
||||
isLocal: reader.readBool(offsets[8]),
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
|
||||
localId: reader.readString(offsets[10]),
|
||||
ownerId: reader.readLong(offsets[11]),
|
||||
remoteId: reader.readStringOrNull(offsets[12]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
|
||||
livePhotoVideoId: reader.readStringOrNull(offsets[8]),
|
||||
localId: reader.readStringOrNull(offsets[9]),
|
||||
ownerId: reader.readLong(offsets[10]),
|
||||
remoteId: reader.readStringOrNull(offsets[11]),
|
||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[12])] ??
|
||||
AssetType.other,
|
||||
updatedAt: reader.readDateTime(offsets[14]),
|
||||
width: reader.readIntOrNull(offsets[15]),
|
||||
updatedAt: reader.readDateTime(offsets[13]),
|
||||
width: reader.readIntOrNull(offsets[14]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
@ -229,7 +241,7 @@ P _assetDeserializeProp<P>(
|
|||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readLong(offset)) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 2:
|
||||
|
@ -245,21 +257,19 @@ P _assetDeserializeProp<P>(
|
|||
case 7:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readBool(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 12:
|
||||
case 11:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
case 12:
|
||||
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AssetType.other) as P;
|
||||
case 14:
|
||||
case 13:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 15:
|
||||
case 14:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
|
@ -291,6 +301,94 @@ void _assetAttach(IsarCollection<dynamic> col, Id id, Asset object) {
|
|||
object.id = id;
|
||||
}
|
||||
|
||||
extension AssetByIndex on IsarCollection<Asset> {
|
||||
Future<Asset?> getByChecksumOwnerId(String checksum, int ownerId) {
|
||||
return getByIndex(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Asset? getByChecksumOwnerIdSync(String checksum, int ownerId) {
|
||||
return getByIndexSync(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Future<bool> deleteByChecksumOwnerId(String checksum, int ownerId) {
|
||||
return deleteByIndex(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
bool deleteByChecksumOwnerIdSync(String checksum, int ownerId) {
|
||||
return deleteByIndexSync(r'checksum_ownerId', [checksum, ownerId]);
|
||||
}
|
||||
|
||||
Future<List<Asset?>> getAllByChecksumOwnerId(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return getAllByIndex(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
List<Asset?> getAllByChecksumOwnerIdSync(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return getAllByIndexSync(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
Future<int> deleteAllByChecksumOwnerId(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return deleteAllByIndex(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
int deleteAllByChecksumOwnerIdSync(
|
||||
List<String> checksumValues, List<int> ownerIdValues) {
|
||||
final len = checksumValues.length;
|
||||
assert(ownerIdValues.length == len,
|
||||
'All index values must have the same length');
|
||||
final values = <List<dynamic>>[];
|
||||
for (var i = 0; i < len; i++) {
|
||||
values.add([checksumValues[i], ownerIdValues[i]]);
|
||||
}
|
||||
|
||||
return deleteAllByIndexSync(r'checksum_ownerId', values);
|
||||
}
|
||||
|
||||
Future<Id> putByChecksumOwnerId(Asset object) {
|
||||
return putByIndex(r'checksum_ownerId', object);
|
||||
}
|
||||
|
||||
Id putByChecksumOwnerIdSync(Asset object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'checksum_ownerId', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllByChecksumOwnerId(List<Asset> objects) {
|
||||
return putAllByIndex(r'checksum_ownerId', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByChecksumOwnerIdSync(List<Asset> objects,
|
||||
{bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'checksum_ownerId', objects,
|
||||
saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetQueryWhereSort on QueryBuilder<Asset, Asset, QWhere> {
|
||||
QueryBuilder<Asset, Asset, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
@ -365,6 +463,145 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToAnyOwnerId(
|
||||
String checksum) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'checksum_ownerId',
|
||||
value: [checksum],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumNotEqualToAnyOwnerId(
|
||||
String checksum) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [],
|
||||
upper: [checksum],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [],
|
||||
upper: [checksum],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumOwnerIdEqualTo(
|
||||
String checksum, int ownerId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'checksum_ownerId',
|
||||
value: [checksum, ownerId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
checksumEqualToOwnerIdNotEqualTo(String checksum, int ownerId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: false,
|
||||
upper: [checksum],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: false,
|
||||
upper: [checksum],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
checksumEqualToOwnerIdGreaterThan(
|
||||
String checksum,
|
||||
int ownerId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, ownerId],
|
||||
includeLower: include,
|
||||
upper: [checksum],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToOwnerIdLessThan(
|
||||
String checksum,
|
||||
int ownerId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum],
|
||||
upper: [checksum, ownerId],
|
||||
includeUpper: include,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> checksumEqualToOwnerIdBetween(
|
||||
String checksum,
|
||||
int lowerOwnerId,
|
||||
int upperOwnerId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'checksum_ownerId',
|
||||
lower: [checksum, lowerOwnerId],
|
||||
includeLower: includeLower,
|
||||
upper: [checksum, upperOwnerId],
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> remoteIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
|
@ -430,29 +667,49 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToAnyDeviceId(
|
||||
String localId) {
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
value: [null],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId',
|
||||
lower: [null],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualTo(
|
||||
String? localId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId',
|
||||
value: [localId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdNotEqualToAnyDeviceId(
|
||||
String localId) {
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdNotEqualTo(
|
||||
String? localId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [],
|
||||
upper: [localId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [localId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
|
@ -460,13 +717,13 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
|||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [localId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
indexName: r'localId',
|
||||
lower: [],
|
||||
upper: [localId],
|
||||
includeUpper: false,
|
||||
|
@ -474,151 +731,135 @@ extension AssetQueryWhere on QueryBuilder<Asset, Asset, QWhereClause> {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdDeviceIdEqualTo(
|
||||
String localId, int deviceId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'localId_deviceId',
|
||||
value: [localId, deviceId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
localIdEqualToDeviceIdNotEqualTo(String localId, int deviceId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: false,
|
||||
upper: [localId],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: false,
|
||||
upper: [localId],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause>
|
||||
localIdEqualToDeviceIdGreaterThan(
|
||||
String localId,
|
||||
int deviceId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, deviceId],
|
||||
includeLower: include,
|
||||
upper: [localId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToDeviceIdLessThan(
|
||||
String localId,
|
||||
int deviceId, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId],
|
||||
upper: [localId, deviceId],
|
||||
includeUpper: include,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterWhereClause> localIdEqualToDeviceIdBetween(
|
||||
String localId,
|
||||
int lowerDeviceId,
|
||||
int upperDeviceId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'localId_deviceId',
|
||||
lower: [localId, lowerDeviceId],
|
||||
includeLower: includeLower,
|
||||
upper: [localId, upperDeviceId],
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdEqualTo(int value) {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdGreaterThan(
|
||||
int value, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdLessThan(
|
||||
int value, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> deviceIdBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'deviceId',
|
||||
property: r'checksum',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumContains(
|
||||
String value,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'checksum',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'checksum',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'checksum',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> checksumIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'checksum',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
@ -1053,15 +1294,6 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> isLocalEqualTo(bool value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isLocal',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> livePhotoVideoIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
|
@ -1210,8 +1442,24 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'localId',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'localId',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdEqualTo(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
@ -1224,7 +1472,7 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdGreaterThan(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
|
@ -1239,7 +1487,7 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdLessThan(
|
||||
String value, {
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
|
@ -1254,8 +1502,8 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> localIdBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
|
@ -1718,15 +1966,15 @@ extension AssetQueryObject on QueryBuilder<Asset, Asset, QFilterCondition> {}
|
|||
extension AssetQueryLinks on QueryBuilder<Asset, Asset, QFilterCondition> {}
|
||||
|
||||
extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByChecksum() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.asc);
|
||||
return query.addSortBy(r'checksum', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByDeviceIdDesc() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByChecksumDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.desc);
|
||||
return query.addSortBy(r'checksum', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1814,18 +2062,6 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsLocalDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
|
@ -1912,15 +2148,15 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
|||
}
|
||||
|
||||
extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByChecksum() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.asc);
|
||||
return query.addSortBy(r'checksum', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByDeviceIdDesc() {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByChecksumDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'deviceId', Sort.desc);
|
||||
return query.addSortBy(r'checksum', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2020,18 +2256,6 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsLocalDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isLocal', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByLivePhotoVideoId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'livePhotoVideoId', Sort.asc);
|
||||
|
@ -2118,9 +2342,10 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
|||
}
|
||||
|
||||
extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByDeviceId() {
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByChecksum(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'deviceId');
|
||||
return query.addDistinctBy(r'checksum', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2167,12 +2392,6 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByIsLocal() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'isLocal');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, Asset, QDistinct> distinctByLivePhotoVideoId(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
@ -2227,9 +2446,9 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, int, QQueryOperations> deviceIdProperty() {
|
||||
QueryBuilder<Asset, String, QQueryOperations> checksumProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'deviceId');
|
||||
return query.addPropertyName(r'checksum');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2275,19 +2494,13 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, bool, QQueryOperations> isLocalProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isLocal');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String?, QQueryOperations> livePhotoVideoIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'livePhotoVideoId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<Asset, String, QQueryOperations> localIdProperty() {
|
||||
QueryBuilder<Asset, String?, QQueryOperations> localIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'localId');
|
||||
});
|
||||
|
|
8
mobile/lib/shared/models/device_asset.dart
Normal file
8
mobile/lib/shared/models/device_asset.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
import 'package:isar/isar.dart';
|
||||
|
||||
class DeviceAsset {
|
||||
DeviceAsset({required this.hash});
|
||||
|
||||
@Index(unique: false, type: IndexType.hash)
|
||||
List<byte> hash;
|
||||
}
|
14
mobile/lib/shared/models/ios_device_asset.dart
Normal file
14
mobile/lib/shared/models/ios_device_asset.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'ios_device_asset.g.dart';
|
||||
|
||||
@Collection()
|
||||
class IOSDeviceAsset extends DeviceAsset {
|
||||
IOSDeviceAsset({required this.id, required super.hash});
|
||||
|
||||
@Index(replace: true, unique: true, type: IndexType.hash)
|
||||
String id;
|
||||
Id get isarId => fastHash(id);
|
||||
}
|
780
mobile/lib/shared/models/ios_device_asset.g.dart
Normal file
780
mobile/lib/shared/models/ios_device_asset.g.dart
Normal file
|
@ -0,0 +1,780 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ios_device_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetIOSDeviceAssetCollection on Isar {
|
||||
IsarCollection<IOSDeviceAsset> get iOSDeviceAssets => this.collection();
|
||||
}
|
||||
|
||||
const IOSDeviceAssetSchema = CollectionSchema(
|
||||
name: r'IOSDeviceAsset',
|
||||
id: -1671546753821948030,
|
||||
properties: {
|
||||
r'hash': PropertySchema(
|
||||
id: 0,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 1,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _iOSDeviceAssetEstimateSize,
|
||||
serialize: _iOSDeviceAssetSerialize,
|
||||
deserialize: _iOSDeviceAssetDeserialize,
|
||||
deserializeProp: _iOSDeviceAssetDeserializeProp,
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
id: -3268401673993471357,
|
||||
name: r'id',
|
||||
unique: true,
|
||||
replace: true,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'id',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
)
|
||||
],
|
||||
),
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _iOSDeviceAssetGetId,
|
||||
getLinks: _iOSDeviceAssetGetLinks,
|
||||
attach: _iOSDeviceAssetAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _iOSDeviceAssetEstimateSize(
|
||||
IOSDeviceAsset object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
bytesCount += 3 + object.id.length * 3;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetSerialize(
|
||||
IOSDeviceAsset object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeByteList(offsets[0], object.hash);
|
||||
writer.writeString(offsets[1], object.id);
|
||||
}
|
||||
|
||||
IOSDeviceAsset _iOSDeviceAssetDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = IOSDeviceAsset(
|
||||
hash: reader.readByteList(offsets[0]) ?? [],
|
||||
id: reader.readString(offsets[1]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _iOSDeviceAssetDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
case 1:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _iOSDeviceAssetGetId(IOSDeviceAsset object) {
|
||||
return object.isarId;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _iOSDeviceAssetAttach(
|
||||
IsarCollection<dynamic> col, Id id, IOSDeviceAsset object) {}
|
||||
|
||||
extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
|
||||
Future<IOSDeviceAsset?> getById(String id) {
|
||||
return getByIndex(r'id', [id]);
|
||||
}
|
||||
|
||||
IOSDeviceAsset? getByIdSync(String id) {
|
||||
return getByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
Future<bool> deleteById(String id) {
|
||||
return deleteByIndex(r'id', [id]);
|
||||
}
|
||||
|
||||
bool deleteByIdSync(String id) {
|
||||
return deleteByIndexSync(r'id', [id]);
|
||||
}
|
||||
|
||||
Future<List<IOSDeviceAsset?>> getAllById(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndex(r'id', values);
|
||||
}
|
||||
|
||||
List<IOSDeviceAsset?> getAllByIdSync(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return getAllByIndexSync(r'id', values);
|
||||
}
|
||||
|
||||
Future<int> deleteAllById(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndex(r'id', values);
|
||||
}
|
||||
|
||||
int deleteAllByIdSync(List<String> idValues) {
|
||||
final values = idValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndexSync(r'id', values);
|
||||
}
|
||||
|
||||
Future<Id> putById(IOSDeviceAsset object) {
|
||||
return putByIndex(r'id', object);
|
||||
}
|
||||
|
||||
Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'id', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllById(List<IOSDeviceAsset> objects) {
|
||||
return putAllByIndex(r'id', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByIdSync(List<IOSDeviceAsset> objects,
|
||||
{bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereSort
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhere> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhere> anyIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhere
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhereClause> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdEqualTo(
|
||||
Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: isarId,
|
||||
upper: isarId,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdNotEqualTo(Id isarId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdGreaterThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
isarIdLessThan(Id isarId, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdBetween(
|
||||
Id lowerIsarId,
|
||||
Id upperIsarId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerIsarId,
|
||||
includeLower: includeLower,
|
||||
upper: upperIsarId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'id',
|
||||
value: [id],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idNotEqualTo(
|
||||
String id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [],
|
||||
upper: [id],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [id],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [id],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'id',
|
||||
lower: [],
|
||||
upper: [id],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> hashEqualTo(
|
||||
List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryFilter
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'id',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idMatches(
|
||||
String pattern,
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'id',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
idIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'id',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'isarId',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
|
||||
isarIdBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'isarId',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryObject
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQueryLinks
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
|
||||
|
||||
extension IOSDeviceAssetQuerySortBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQuerySortThenBy
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortThenBy> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIsarId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy>
|
||||
thenByIsarIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'isarId', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryWhereDistinct
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> {
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctById(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSDeviceAssetQueryProperty
|
||||
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QQueryProperty> {
|
||||
QueryBuilder<IOSDeviceAsset, int, QQueryOperations> isarIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'isarId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<IOSDeviceAsset, String, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,11 +18,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
/// State does not contain archived assets.
|
||||
/// Use database provider if you want to access the isArchived assets
|
||||
class AssetsState {}
|
||||
|
||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
class AssetNotifier extends StateNotifier<bool> {
|
||||
final AssetService _assetService;
|
||||
final AlbumService _albumService;
|
||||
final UserService _userService;
|
||||
|
@ -38,7 +34,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
this._userService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
) : super(AssetsState());
|
||||
) : super(false);
|
||||
|
||||
Future<void> getAllAsset({bool clear = false}) async {
|
||||
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||
|
@ -48,14 +44,15 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
_getAllAssetInProgress = true;
|
||||
state = true;
|
||||
if (clear) {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
await _userService.refreshUsers();
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
await _userService.refreshUsers();
|
||||
final List<User> partners =
|
||||
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
|
||||
for (User u in partners) {
|
||||
|
@ -64,6 +61,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,6 +77,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
|
||||
Future<void> deleteAssets(Set<Asset> deleteAssets) async {
|
||||
_deleteInProgress = true;
|
||||
state = true;
|
||||
try {
|
||||
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||
|
@ -91,24 +90,14 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
}
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
|
||||
final int deviceId = Store.get(StoreKey.deviceIdHash);
|
||||
final List<String> local = [];
|
||||
final List<String> local =
|
||||
assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList();
|
||||
// Delete asset from device
|
||||
for (final Asset asset in assetsToDelete) {
|
||||
if (asset.isLocal) {
|
||||
local.add(asset.localId);
|
||||
} else if (asset.deviceId == deviceId) {
|
||||
// Delete asset on device if it is still present
|
||||
var localAsset = await AssetEntity.fromId(asset.localId);
|
||||
if (localAsset != null) {
|
||||
local.add(localAsset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (local.isNotEmpty) {
|
||||
try {
|
||||
return await PhotoManager.editor.deleteWithIds(local);
|
||||
|
@ -153,7 +142,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||
}
|
||||
}
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
|
|
175
mobile/lib/shared/services/hash.service.dart
Normal file
175
mobile/lib/shared/services/hash.service.dart
Normal file
|
@ -0,0 +1,175 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/shared/models/android_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/device_asset.dart';
|
||||
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class HashService {
|
||||
HashService(this._db, this._backgroundService);
|
||||
final Isar _db;
|
||||
final BackgroundService _backgroundService;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
/// Returns all assets that were successfully hashed
|
||||
Future<List<Asset>> getHashedAssets(
|
||||
AssetPathEntity album, {
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final entities = await album.getAssetListRange(start: start, end: end);
|
||||
final filtered = excludedAssets == null
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.id)).toList();
|
||||
return _hashAssets(filtered);
|
||||
}
|
||||
|
||||
/// Converts a list of [AssetEntity]s to [Asset]s including only those
|
||||
/// that were successfully hashed. Hashes are looked up in a DB table
|
||||
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
||||
/// entries are newly hashed and added to the DB table.
|
||||
Future<List<Asset>> _hashAssets(List<AssetEntity> assetEntities) async {
|
||||
const int batchFileCount = 128;
|
||||
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
final ids = assetEntities
|
||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
final List<String> toHash = [];
|
||||
|
||||
int bytes = 0;
|
||||
|
||||
for (int i = 0; i < assetEntities.length; i++) {
|
||||
if (hashes[i] != null) {
|
||||
continue;
|
||||
}
|
||||
final file = await assetEntities[i].originFile;
|
||||
if (file == null) {
|
||||
_log.warning(
|
||||
"Failed to get file for asset ${assetEntities[i].id}, skipping",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
bytes += await file.length();
|
||||
toHash.add(file.path);
|
||||
final deviceAsset = Platform.isAndroid
|
||||
? AndroidDeviceAsset(id: ids[i] as int, hash: const [])
|
||||
: IOSDeviceAsset(id: ids[i] as String, hash: const []);
|
||||
toAdd.add(deviceAsset);
|
||||
hashes[i] = deviceAsset;
|
||||
if (toHash.length == batchFileCount || bytes >= batchDataSize) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
toAdd.clear();
|
||||
toHash.clear();
|
||||
bytes = 0;
|
||||
}
|
||||
}
|
||||
if (toHash.isNotEmpty) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
}
|
||||
return _mapAllHashedAssets(assetEntities, hashes);
|
||||
}
|
||||
|
||||
/// Lookup hashes of assets by their local ID
|
||||
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
/// Processes a batch of files and saves any successfully hashed
|
||||
/// values to the DB table.
|
||||
Future<void> _processBatch(
|
||||
final List<String> toHash,
|
||||
final List<DeviceAsset> toAdd,
|
||||
) async {
|
||||
final hashes = await _hashFiles(toHash);
|
||||
bool anyNull = false;
|
||||
for (int j = 0; j < hashes.length; j++) {
|
||||
if (hashes[j]?.length == 20) {
|
||||
toAdd[j].hash = hashes[j]!;
|
||||
} else {
|
||||
_log.warning("Failed to hash file ${toHash[j]}, skipping");
|
||||
anyNull = true;
|
||||
}
|
||||
}
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
await _db.writeTxn(
|
||||
() => Platform.isAndroid
|
||||
? _db.androidDeviceAssets.putAll(validHashes.cast())
|
||||
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
|
||||
);
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
/// Hashes the given files and returns a list of the same length
|
||||
/// files that could not be hashed have a `null` value
|
||||
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
||||
if (Platform.isAndroid) {
|
||||
final List<Uint8List?>? hashes =
|
||||
await _backgroundService.digestFiles(paths);
|
||||
if (hashes == null) {
|
||||
throw Exception("Hashing ${paths.length} files failed");
|
||||
}
|
||||
return hashes;
|
||||
} else if (Platform.isIOS) {
|
||||
final List<Uint8List?> result = List.filled(paths.length, null);
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
result[i] = await _hashAssetDart(File(paths[i]));
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
throw Exception("_hashFiles implementation missing");
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes a single file using Dart's crypto package
|
||||
Future<Uint8List?> _hashAssetDart(File f) async {
|
||||
late Digest output;
|
||||
final sink = sha1.startChunkedConversion(
|
||||
ChunkedConversionSink<Digest>.withCallback((accumulated) {
|
||||
output = accumulated.first;
|
||||
}),
|
||||
);
|
||||
await for (final chunk in f.openRead()) {
|
||||
sink.add(chunk);
|
||||
}
|
||||
sink.close();
|
||||
return Uint8List.fromList(output.bytes);
|
||||
}
|
||||
|
||||
/// Converts [AssetEntity]s that were successfully hashed to [Asset]s
|
||||
List<Asset> _mapAllHashedAssets(
|
||||
List<AssetEntity> assets,
|
||||
List<DeviceAsset?> hashes,
|
||||
) {
|
||||
final List<Asset> result = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
|
||||
result.add(Asset.local(assets[i], hashes[i]!.hash));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
final hashServiceProvider = Provider(
|
||||
(ref) => HashService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
),
|
||||
);
|
|
@ -4,10 +4,12 @@ import 'package:collection/collection.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.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/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
|
@ -16,15 +18,17 @@ import 'package:logging/logging.dart';
|
|||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final syncServiceProvider =
|
||||
Provider((ref) => SyncService(ref.watch(dbProvider)));
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(this._db);
|
||||
SyncService(this._db, this._hashService);
|
||||
|
||||
// public methods:
|
||||
|
||||
|
@ -33,6 +37,7 @@ class SyncService {
|
|||
Future<bool> syncUsersFromServer(List<User> users) async {
|
||||
users.sortBy((u) => u.id);
|
||||
final dbUsers = await _db.users.where().sortById().findAll();
|
||||
assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
|
||||
final List<int> toDelete = [];
|
||||
final List<User> toUpsert = [];
|
||||
final changes = diffSortedListsSync(
|
||||
|
@ -108,40 +113,16 @@ class SyncService {
|
|||
// private methods:
|
||||
|
||||
/// Syncs a new asset to the db. Returns `true` if successful
|
||||
Future<bool> _syncNewAssetToDb(Asset newAsset) async {
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.localIdDeviceIdEqualTo(newAsset.localId, newAsset.deviceId)
|
||||
.findAll();
|
||||
Asset? match;
|
||||
if (inDb.length == 1) {
|
||||
// exactly one match: trivial case
|
||||
match = inDb.first;
|
||||
} else if (inDb.length > 1) {
|
||||
// TODO instead of this heuristics: match by checksum once available
|
||||
for (Asset a in inDb) {
|
||||
if (a.ownerId == newAsset.ownerId &&
|
||||
a.fileModifiedAt.isAtSameMomentAs(newAsset.fileModifiedAt)) {
|
||||
assert(match == null);
|
||||
match = a;
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
for (Asset a in inDb) {
|
||||
if (a.ownerId == newAsset.ownerId) {
|
||||
assert(match == null);
|
||||
match = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
Future<bool> _syncNewAssetToDb(Asset a) async {
|
||||
final Asset? inDb =
|
||||
await _db.assets.getByChecksumOwnerId(a.checksum, a.ownerId);
|
||||
if (inDb != null) {
|
||||
// unify local/remote assets by replacing the
|
||||
// local-only asset in the DB with a local&remote asset
|
||||
newAsset = match.updatedCopy(newAsset);
|
||||
a = inDb.updatedCopy(a);
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() => newAsset.put(_db));
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to put new asset into db: $e");
|
||||
return false;
|
||||
|
@ -162,11 +143,11 @@ class SyncService {
|
|||
final List<Asset> inDb = await _db.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(user.isarId)
|
||||
.sortByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
remote.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
|
||||
remote.sort(Asset.compareByChecksum);
|
||||
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
|
||||
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
|
||||
return false;
|
||||
|
@ -199,6 +180,7 @@ class SyncService {
|
|||
query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId));
|
||||
}
|
||||
final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
|
||||
assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
final List<Asset> existing = [];
|
||||
|
@ -245,16 +227,16 @@ class SyncService {
|
|||
if (dto.assetCount != dto.assets.length) {
|
||||
return false;
|
||||
}
|
||||
final assetsInDb = await album.assets
|
||||
.filter()
|
||||
.sortByOwnerId()
|
||||
.thenByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.findAll();
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb);
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
assetsOnRemote,
|
||||
assetsInDb,
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
);
|
||||
|
||||
// update shared users
|
||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||
|
@ -297,6 +279,7 @@ class SyncService {
|
|||
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
|
||||
await _db.albums.put(album);
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote album to database $e");
|
||||
}
|
||||
|
@ -382,10 +365,11 @@ class SyncService {
|
|||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final List<Album> inDb =
|
||||
final inDb =
|
||||
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
|
||||
final bool anyChanges = await diffSortedLists(
|
||||
onDevice,
|
||||
inDb,
|
||||
|
@ -447,14 +431,15 @@ class SyncService {
|
|||
final inDb = await album.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.deviceIdEqualTo(Store.get(StoreKey.deviceIdHash))
|
||||
.sortByLocalId()
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
||||
final List<Asset> onDevice =
|
||||
await ape.getAssets(excludedAssets: excludedAssets);
|
||||
onDevice.sort(Asset.compareByLocalId);
|
||||
final (toAdd, toUpdate, toDelete) =
|
||||
_diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(onDevice);
|
||||
// _removeDuplicates sorts `onDevice` by checksum
|
||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||
if (toAdd.isEmpty &&
|
||||
toUpdate.isEmpty &&
|
||||
toDelete.isEmpty &&
|
||||
|
@ -491,6 +476,9 @@ class SyncService {
|
|||
await _db.albums.put(album);
|
||||
album.thumbnail.value ??= await album.assets.filter().findFirst();
|
||||
await album.thumbnail.save();
|
||||
await _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, value: assetCountOnDevice.toString()),
|
||||
);
|
||||
});
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
|
@ -503,8 +491,13 @@ class SyncService {
|
|||
/// fast path for common case: only new assets were added to device album
|
||||
/// returns `true` if successfull, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
|
||||
if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice = await ape.assetCountAsync;
|
||||
final AssetPathEntity? modified = totalOnDevice > album.assetCount
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.value?.toInt() ?? 0;
|
||||
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
|
||||
? await ape.fetchPathProperties(
|
||||
filterOptionGroup: FilterOptionGroup(
|
||||
updateTimeCond: DateTimeCond(
|
||||
|
@ -517,17 +510,22 @@ class SyncService {
|
|||
if (modified == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await modified.getAssets();
|
||||
if (totalOnDevice != album.assets.length + newAssets.length) {
|
||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
||||
|
||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||
return false;
|
||||
}
|
||||
album.modifiedAt = ape.lastModified ?? DateTime.now();
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await album.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(album);
|
||||
await _db.eTags.put(
|
||||
ETag(id: ape.eTagKeyAssetCount, value: totalOnDevice.toString()),
|
||||
);
|
||||
});
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
|
@ -547,7 +545,9 @@ class SyncService {
|
|||
]) async {
|
||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||
final Album a = Album.local(ape);
|
||||
final assets = await ape.getAssets(excludedAssets: excludedAssets);
|
||||
final assets =
|
||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
||||
_removeDuplicates(assets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||
_log.info(
|
||||
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
|
||||
|
@ -570,44 +570,29 @@ class SyncService {
|
|||
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
|
||||
List<Asset> assets,
|
||||
) async {
|
||||
if (assets.isEmpty) {
|
||||
return ([].cast<Asset>(), [].cast<Asset>());
|
||||
}
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.anyOf(
|
||||
assets,
|
||||
(q, Asset e) => q.localIdDeviceIdEqualTo(e.localId, e.deviceId),
|
||||
)
|
||||
.sortByOwnerId()
|
||||
.thenByDeviceId()
|
||||
.thenByLocalId()
|
||||
.thenByFileModifiedAt()
|
||||
.findAll();
|
||||
assets.sort(Asset.compareByOwnerDeviceLocalIdModified);
|
||||
final List<Asset> existing = [], toUpsert = [];
|
||||
diffSortedListsSync(
|
||||
inDb,
|
||||
assets,
|
||||
// do not compare by modified date because for some assets dates differ on
|
||||
// client and server, thus never reaching "both" case below
|
||||
compare: Asset.compareByOwnerDeviceLocalId,
|
||||
both: (Asset a, Asset b) {
|
||||
if (a.canUpdate(b)) {
|
||||
toUpsert.add(a.updatedCopy(b));
|
||||
return true;
|
||||
} else {
|
||||
existing.add(a);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onlyFirst: (Asset a) => _log.finer(
|
||||
"_linkWithExistingFromDb encountered asset only in DB: $a",
|
||||
null,
|
||||
StackTrace.current,
|
||||
),
|
||||
onlySecond: (Asset b) => toUpsert.add(b),
|
||||
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
|
||||
|
||||
final List<Asset?> inDb = await _db.assets.getAllByChecksumOwnerId(
|
||||
assets.map((a) => a.checksum).toList(growable: false),
|
||||
assets.map((a) => a.ownerId).toInt64List(),
|
||||
);
|
||||
assert(inDb.length == assets.length);
|
||||
final List<Asset> existing = [], toUpsert = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
toUpsert.add(assets[i]);
|
||||
continue;
|
||||
}
|
||||
if (b.canUpdate(assets[i])) {
|
||||
final updated = b.updatedCopy(assets[i]);
|
||||
assert(updated.id != Isar.autoIncrement);
|
||||
toUpsert.add(updated);
|
||||
} else {
|
||||
existing.add(b);
|
||||
}
|
||||
}
|
||||
assert(existing.length + toUpsert.length == assets.length);
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
|
||||
|
@ -627,11 +612,63 @@ class SyncService {
|
|||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.warning(
|
||||
_log.severe(
|
||||
"Failed to upsert ${assets.length} assets into the DB: ${e.toString()}",
|
||||
);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _db.assets.getAllByChecksumOwnerId(
|
||||
assets.map((e) => e.checksum).toList(growable: false),
|
||||
assets.map((e) => e.ownerId).toInt64List(),
|
||||
);
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
final Asset a = assets[i];
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
if (a.id != Isar.autoIncrement) {
|
||||
_log.warning(
|
||||
"Trying to update an asset that does not exist in DB:\n$a",
|
||||
);
|
||||
}
|
||||
} else if (a.id != b.id) {
|
||||
_log.warning(
|
||||
"Trying to insert another asset with the same checksum+owner. In DB:\n$b\nTo insert:\n$a",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < assets.length; i++) {
|
||||
if (Asset.compareByOwnerChecksum(assets[i - 1], assets[i]) == 0) {
|
||||
_log.warning(
|
||||
"Trying to insert duplicate assets:\n${assets[i - 1]}\n${assets[i]}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
assets.uniqueConsecutive(
|
||||
compare: Asset.compareByOwnerChecksum,
|
||||
onDuplicate: (a, b) =>
|
||||
_log.info("Ignoring duplicate assets on device:\n$a\n$b"),
|
||||
);
|
||||
final int duplicates = before - assets.length;
|
||||
if (duplicates > 0) {
|
||||
_log.warning("Ignored $duplicates duplicate assets on device");
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync !=
|
||||
(await _db.eTags.getById(a.eTagKeyAssetCount))?.value?.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a triple(toAdd, toUpdate, toRemove)
|
||||
|
@ -639,7 +676,7 @@ class SyncService {
|
|||
List<Asset> assets,
|
||||
List<Asset> inDb, {
|
||||
bool? remote,
|
||||
int Function(Asset, Asset) compare = Asset.compareByOwnerDeviceLocalId,
|
||||
int Function(Asset, Asset) compare = Asset.compareByChecksum,
|
||||
}) {
|
||||
final List<Asset> toAdd = [];
|
||||
final List<Asset> toUpdate = [];
|
||||
|
@ -663,7 +700,7 @@ class SyncService {
|
|||
}
|
||||
} else if (remote == false && a.isRemote) {
|
||||
if (a.isLocal) {
|
||||
a.isLocal = false;
|
||||
a.localId = null;
|
||||
toUpdate.add(a);
|
||||
}
|
||||
} else {
|
||||
|
@ -685,9 +722,9 @@ class SyncService {
|
|||
return const ([], []);
|
||||
}
|
||||
deleteCandidates.sort(Asset.compareById);
|
||||
deleteCandidates.uniqueConsecutive((a) => a.id);
|
||||
deleteCandidates.uniqueConsecutive(compare: Asset.compareById);
|
||||
existing.sort(Asset.compareById);
|
||||
existing.uniqueConsecutive((a) => a.id);
|
||||
existing.uniqueConsecutive(compare: Asset.compareById);
|
||||
final (tooAdd, toUpdate, toRemove) = _diffAssets(
|
||||
existing,
|
||||
deleteCandidates,
|
||||
|
@ -698,14 +735,6 @@ class SyncService {
|
|||
return (toRemove.map((e) => e.id).toList(), toUpdate);
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
|
||||
return a.name != b.name ||
|
||||
a.lastModified == null ||
|
||||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
|
||||
await a.assetCountAsync != b.assetCount;
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
||||
return dto.assetCount != a.assetCount ||
|
||||
|
|
|
@ -6,12 +6,39 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
|
||||
class TabControllerPage extends ConsumerWidget {
|
||||
class TabControllerPage extends HookConsumerWidget {
|
||||
const TabControllerPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshing = ref.watch(assetProvider);
|
||||
|
||||
Widget buildIcon(Widget icon) {
|
||||
if (!refreshing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -14,
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
navigationRail(TabsRouter tabsRouter) {
|
||||
return NavigationRail(
|
||||
labelType: NavigationRailLabelType.all,
|
||||
|
@ -83,9 +110,12 @@ class TabControllerPage extends ConsumerWidget {
|
|||
icon: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_library,
|
||||
color: Theme.of(context).primaryColor,
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
size: 24,
|
||||
Icons.photo_library,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
|
@ -113,9 +143,11 @@ class TabControllerPage extends ConsumerWidget {
|
|||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
extension DurationExtension on String {
|
||||
|
@ -22,15 +24,20 @@ extension DurationExtension on String {
|
|||
}
|
||||
|
||||
extension ListExtension<E> on List<E> {
|
||||
List<E> uniqueConsecutive<T>([T Function(E element)? key]) {
|
||||
key ??= (E e) => e as T;
|
||||
List<E> uniqueConsecutive({
|
||||
int Function(E a, E b)? compare,
|
||||
void Function(E a, E b)? onDuplicate,
|
||||
}) {
|
||||
compare ??= (E a, E b) => a == b ? 0 : 1;
|
||||
int i = 1, j = 1;
|
||||
for (; i < length; i++) {
|
||||
if (key(this[i]) != key(this[i - 1])) {
|
||||
if (compare(this[i - 1], this[i]) != 0) {
|
||||
if (i != j) {
|
||||
this[j] = this[i];
|
||||
}
|
||||
j++;
|
||||
} else if (onDuplicate != null) {
|
||||
onDuplicate(this[i - 1], this[i]);
|
||||
}
|
||||
}
|
||||
length = length == 0 ? 0 : j;
|
||||
|
@ -45,3 +52,11 @@ extension ListExtension<E> on List<E> {
|
|||
return ListSlice<E>(this, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
extension IntListExtension on Iterable<int> {
|
||||
Int64List toInt64List() {
|
||||
final list = Int64List(length);
|
||||
list.setAll(0, this);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
|||
final int version = Store.get(StoreKey.version, 1);
|
||||
switch (version) {
|
||||
case 1:
|
||||
await _migrateV1ToV2(db);
|
||||
await _migrateTo(db, 2);
|
||||
case 2:
|
||||
await _migrateTo(db, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _migrateV1ToV2(Isar db) async {
|
||||
Future<void> _migrateTo(Isar db, int version) async {
|
||||
await clearAssetsAndAlbums(db);
|
||||
await Store.put(StoreKey.version, 2);
|
||||
await Store.put(StoreKey.version, version);
|
||||
}
|
||||
|
|
|
@ -242,13 +242,13 @@ packages:
|
|||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -333,10 +333,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.0.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -45,6 +45,7 @@ dependencies:
|
|||
isar_flutter_libs: *isar_version # contains Isar Core
|
||||
permission_handler: ^10.2.0
|
||||
device_info_plus: ^8.1.0
|
||||
crypto: ^3.0.3 # TODO remove once native crypto is used on iOS
|
||||
|
||||
openapi:
|
||||
path: openapi
|
||||
|
|
|
@ -13,8 +13,8 @@ void main() {
|
|||
|
||||
testAssets.add(
|
||||
Asset(
|
||||
checksum: "",
|
||||
localId: '$i',
|
||||
deviceId: 1,
|
||||
ownerId: 1,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
|
@ -23,7 +23,6 @@ void main() {
|
|||
type: AssetType.image,
|
||||
fileName: '',
|
||||
isFavorite: false,
|
||||
isLocal: false,
|
||||
isArchived: false,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -43,7 +43,12 @@ void main() {
|
|||
|
||||
test('withKey', () {
|
||||
final a = ["a", "bb", "cc", "ddd"];
|
||||
expect(a.uniqueConsecutive((s) => s.length), ["a", "bb", "ddd"]);
|
||||
expect(
|
||||
a.uniqueConsecutive(
|
||||
compare: (s1, s2) => s1.length.compareTo(s2.length),
|
||||
),
|
||||
["a", "bb", "ddd"],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,32 +6,33 @@ import 'package:immich_mobile/shared/models/exif_info.dart';
|
|||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
void main() {
|
||||
Asset makeAsset({
|
||||
required String localId,
|
||||
required String checksum,
|
||||
String? localId,
|
||||
String? remoteId,
|
||||
int deviceId = 1,
|
||||
int ownerId = 590700560494856554, // hash of "1"
|
||||
bool isLocal = false,
|
||||
}) {
|
||||
final DateTime date = DateTime(2000);
|
||||
return Asset(
|
||||
checksum: checksum,
|
||||
localId: localId,
|
||||
remoteId: remoteId,
|
||||
deviceId: deviceId,
|
||||
ownerId: ownerId,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
updatedAt: date,
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: localId,
|
||||
fileName: localId ?? remoteId ?? "",
|
||||
isFavorite: false,
|
||||
isLocal: isLocal,
|
||||
isArchived: false,
|
||||
);
|
||||
}
|
||||
|
@ -53,6 +54,7 @@ void main() {
|
|||
|
||||
group('Test SyncService grouped', () {
|
||||
late final Isar db;
|
||||
final MockHashService hs = MockHashService();
|
||||
final owner = User(
|
||||
id: "1",
|
||||
updatedAt: DateTime.now(),
|
||||
|
@ -71,11 +73,11 @@ void main() {
|
|||
await Store.put(StoreKey.currentUser, owner);
|
||||
});
|
||||
final List<Asset> initialAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1", isLocal: true),
|
||||
makeAsset(localId: "2", isLocal: true),
|
||||
makeAsset(localId: "3", isLocal: true),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", localId: "1", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", localId: "2"),
|
||||
makeAsset(checksum: "e", localId: "3"),
|
||||
];
|
||||
setUp(() {
|
||||
db.writeTxnSync(() {
|
||||
|
@ -84,11 +86,11 @@ void main() {
|
|||
});
|
||||
});
|
||||
test('test inserting existing assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
|
@ -97,14 +99,14 @@ void main() {
|
|||
});
|
||||
|
||||
test('test inserting new assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(localId: "2", remoteId: "1-2"),
|
||||
makeAsset(localId: "4", remoteId: "1-4"),
|
||||
makeAsset(localId: "1", remoteId: "3-1", deviceId: 3),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", remoteId: "1-2"),
|
||||
makeAsset(checksum: "f", remoteId: "1-4"),
|
||||
makeAsset(checksum: "g", remoteId: "3-1", deviceId: 3),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
|
@ -113,14 +115,14 @@ void main() {
|
|||
});
|
||||
|
||||
test('test syncing duplicate assets', () async {
|
||||
SyncService s = SyncService(db);
|
||||
SyncService s = SyncService(db, hs);
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(localId: "1", remoteId: "1-1"),
|
||||
makeAsset(localId: "1", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1b", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1c", deviceId: 2),
|
||||
makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2),
|
||||
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
|
||||
makeAsset(checksum: "b", remoteId: "1-1"),
|
||||
makeAsset(checksum: "c", remoteId: "2-1", deviceId: 2),
|
||||
makeAsset(checksum: "h", remoteId: "2-1b", deviceId: 2),
|
||||
makeAsset(checksum: "i", remoteId: "2-1c", deviceId: 2),
|
||||
makeAsset(checksum: "j", remoteId: "2-1d", deviceId: 2),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
|
@ -133,11 +135,13 @@ void main() {
|
|||
final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
expect(c3, true);
|
||||
expect(db.assets.countSync(), 7);
|
||||
remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e", deviceId: 2));
|
||||
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2", deviceId: 2));
|
||||
final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||
expect(c4, true);
|
||||
expect(db.assets.countSync(), 9);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
||||
|
|
Loading…
Reference in a new issue