diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 8be636efb659b..23bf23604635d 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -57,64 +57,69 @@ const AssetSchema = CollectionSchema( name: r'isFavorite', type: IsarType.bool, ), - r'isTrashed': PropertySchema( + r'isOffline': PropertySchema( id: 8, + name: r'isOffline', + type: IsarType.bool, + ), + r'isTrashed': PropertySchema( + id: 9, name: r'isTrashed', type: IsarType.bool, ), r'livePhotoVideoId': PropertySchema( - id: 9, + id: 10, name: r'livePhotoVideoId', type: IsarType.string, ), r'localId': PropertySchema( - id: 10, + id: 11, name: r'localId', type: IsarType.string, ), r'ownerId': PropertySchema( - id: 11, + id: 12, name: r'ownerId', type: IsarType.long, ), r'remoteId': PropertySchema( - id: 12, + id: 13, name: r'remoteId', type: IsarType.string, ), r'stackCount': PropertySchema( - id: 13, + id: 14, name: r'stackCount', type: IsarType.long, ), r'stackId': PropertySchema( - id: 14, + id: 15, name: r'stackId', type: IsarType.string, ), r'stackPrimaryAssetId': PropertySchema( - id: 15, + id: 16, name: r'stackPrimaryAssetId', type: IsarType.string, ), r'thumbhash': PropertySchema( - id: 16, + id: 17, name: r'thumbhash', type: IsarType.string, ), r'type': PropertySchema( - id: 17, + id: 18, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 18, + id: 19, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 19, + id: 20, name: r'width', type: IsarType.int, ) @@ -239,18 +244,19 @@ 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.isTrashed); - 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.writeLong(offsets[13], object.stackCount); - writer.writeString(offsets[14], object.stackId); - writer.writeString(offsets[15], object.stackPrimaryAssetId); - writer.writeString(offsets[16], object.thumbhash); - writer.writeByte(offsets[17], object.type.index); - writer.writeDateTime(offsets[18], object.updatedAt); - writer.writeInt(offsets[19], object.width); + writer.writeBool(offsets[8], object.isOffline); + writer.writeBool(offsets[9], object.isTrashed); + writer.writeString(offsets[10], object.livePhotoVideoId); + writer.writeString(offsets[11], object.localId); + writer.writeLong(offsets[12], object.ownerId); + writer.writeString(offsets[13], object.remoteId); + writer.writeLong(offsets[14], object.stackCount); + writer.writeString(offsets[15], object.stackId); + writer.writeString(offsets[16], object.stackPrimaryAssetId); + writer.writeString(offsets[17], object.thumbhash); + writer.writeByte(offsets[18], object.type.index); + writer.writeDateTime(offsets[19], object.updatedAt); + writer.writeInt(offsets[20], object.width); } Asset _assetDeserialize( @@ -269,19 +275,20 @@ Asset _assetDeserialize( id: id, isArchived: reader.readBoolOrNull(offsets[6]) ?? false, isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[8]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[9]), - localId: reader.readStringOrNull(offsets[10]), - ownerId: reader.readLong(offsets[11]), - remoteId: reader.readStringOrNull(offsets[12]), - stackCount: reader.readLongOrNull(offsets[13]) ?? 0, - stackId: reader.readStringOrNull(offsets[14]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[15]), - thumbhash: reader.readStringOrNull(offsets[16]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? + isOffline: reader.readBoolOrNull(offsets[8]) ?? false, + isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, + livePhotoVideoId: reader.readStringOrNull(offsets[10]), + localId: reader.readStringOrNull(offsets[11]), + ownerId: reader.readLong(offsets[12]), + remoteId: reader.readStringOrNull(offsets[13]), + stackCount: reader.readLongOrNull(offsets[14]) ?? 0, + stackId: reader.readStringOrNull(offsets[15]), + stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), + thumbhash: reader.readStringOrNull(offsets[17]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[18]), - width: reader.readIntOrNull(offsets[19]), + updatedAt: reader.readDateTime(offsets[19]), + width: reader.readIntOrNull(offsets[20]), ); return object; } @@ -312,27 +319,29 @@ P _assetDeserializeProp

( case 8: return (reader.readBoolOrNull(offset) ?? false) as P; case 9: - return (reader.readStringOrNull(offset)) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 10: return (reader.readStringOrNull(offset)) as P; case 11: - return (reader.readLong(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 12: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLong(offset)) as P; case 13: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 14: return (reader.readStringOrNull(offset)) as P; + case 14: + return (reader.readLongOrNull(offset) ?? 0) as P; case 15: return (reader.readStringOrNull(offset)) as P; case 16: return (reader.readStringOrNull(offset)) as P; case 17: + return (reader.readStringOrNull(offset)) as P; + case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 18: - return (reader.readDateTime(offset)) as P; case 19: + return (reader.readDateTime(offset)) as P; + case 20: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1353,6 +1362,16 @@ extension AssetQueryFilter on QueryBuilder { }); } + QueryBuilder isOfflineEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isOffline', + value: value, + )); + }); + } + QueryBuilder isTrashedEqualTo( bool value) { return QueryBuilder.apply(this, (query) { @@ -2628,6 +2647,18 @@ extension AssetQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.asc); + }); + } + + QueryBuilder sortByIsOfflineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.desc); + }); + } + QueryBuilder sortByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -2882,6 +2913,18 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.asc); + }); + } + + QueryBuilder thenByIsOfflineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.desc); + }); + } + QueryBuilder thenByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -3078,6 +3121,12 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isOffline'); + }); + } + QueryBuilder distinctByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'isTrashed'); @@ -3214,6 +3263,12 @@ extension AssetQueryProperty on QueryBuilder { }); } + QueryBuilder isOfflineProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isOffline'); + }); + } + QueryBuilder isTrashedProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'isTrashed'); diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart index 63d06f5d2c1aa..583e627c5d543 100644 --- a/mobile/lib/entities/exif_info.entity.dart +++ b/mobile/lib/entities/exif_info.entity.dart @@ -23,6 +23,7 @@ class ExifInfo { String? state; String? country; String? description; + String? orientation; @ignore bool get hasCoordinates => @@ -45,6 +46,9 @@ class ExifInfo { @ignore String get focalLength => mm != null ? mm!.toStringAsFixed(1) : ""; + @ignore + bool get isFlipped => _isOrientationFlipped(orientation); + @ignore double? get latitude => lat; @@ -67,7 +71,8 @@ class ExifInfo { city = dto.city, state = dto.state, country = dto.country, - description = dto.description; + description = dto.description, + orientation = dto.orientation; ExifInfo({ this.id, @@ -87,6 +92,7 @@ class ExifInfo { this.state, this.country, this.description, + this.orientation, }); ExifInfo copyWith({ @@ -107,6 +113,7 @@ class ExifInfo { String? state, String? country, String? description, + String? orientation, }) => ExifInfo( id: id ?? this.id, @@ -126,6 +133,7 @@ class ExifInfo { state: state ?? this.state, country: country ?? this.country, description: description ?? this.description, + orientation: orientation ?? this.orientation, ); @override @@ -147,7 +155,8 @@ class ExifInfo { city == other.city && state == other.state && country == other.country && - description == other.description; + description == other.description && + orientation == other.orientation; } @override @@ -169,7 +178,8 @@ class ExifInfo { city.hashCode ^ state.hashCode ^ country.hashCode ^ - description.hashCode; + description.hashCode ^ + orientation.hashCode; @override String toString() { @@ -192,10 +202,21 @@ class ExifInfo { state: $state, country: $country, description: $description, + orientation: $orientation }"""; } } +bool _isOrientationFlipped(String? orientation) { + final value = orientation != null ? int.tryParse(orientation) : null; + if (value == null) { + return false; + } + final isRotated90CW = value == 5 || value == 6 || value == 90; + final isRotated270CW = value == 7 || value == 8 || value == -90; + return isRotated90CW || isRotated270CW; +} + double? _exposureTimeToSeconds(String? s) { if (s == null) { return null; diff --git a/mobile/lib/entities/exif_info.entity.g.dart b/mobile/lib/entities/exif_info.entity.g.dart index 016f6d71260d0..2c63f91cff258 100644 --- a/mobile/lib/entities/exif_info.entity.g.dart +++ b/mobile/lib/entities/exif_info.entity.g.dart @@ -87,13 +87,18 @@ const ExifInfoSchema = CollectionSchema( name: r'model', type: IsarType.string, ), - r'state': PropertySchema( + r'orientation': PropertySchema( id: 14, + name: r'orientation', + type: IsarType.string, + ), + r'state': PropertySchema( + id: 15, name: r'state', type: IsarType.string, ), r'timeZone': PropertySchema( - id: 15, + id: 16, name: r'timeZone', type: IsarType.string, ) @@ -154,6 +159,12 @@ int _exifInfoEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.orientation; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.state; if (value != null) { @@ -189,8 +200,9 @@ void _exifInfoSerialize( writer.writeString(offsets[11], object.make); writer.writeFloat(offsets[12], object.mm); writer.writeString(offsets[13], object.model); - writer.writeString(offsets[14], object.state); - writer.writeString(offsets[15], object.timeZone); + writer.writeString(offsets[14], object.orientation); + writer.writeString(offsets[15], object.state); + writer.writeString(offsets[16], object.timeZone); } ExifInfo _exifInfoDeserialize( @@ -215,8 +227,9 @@ ExifInfo _exifInfoDeserialize( make: reader.readStringOrNull(offsets[11]), mm: reader.readFloatOrNull(offsets[12]), model: reader.readStringOrNull(offsets[13]), - state: reader.readStringOrNull(offsets[14]), - timeZone: reader.readStringOrNull(offsets[15]), + orientation: reader.readStringOrNull(offsets[14]), + state: reader.readStringOrNull(offsets[15]), + timeZone: reader.readStringOrNull(offsets[16]), ); return object; } @@ -260,6 +273,8 @@ P _exifInfoDeserializeProp

( return (reader.readStringOrNull(offset)) as P; case 15: return (reader.readStringOrNull(offset)) as P; + case 16: + return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1909,6 +1924,155 @@ extension ExifInfoQueryFilter }); } + QueryBuilder orientationIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'orientation', + )); + }); + } + + QueryBuilder + orientationIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'orientation', + )); + }); + } + + QueryBuilder orientationEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + orientationGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationBetween( + 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'orientation', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'orientation', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'orientation', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder orientationIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'orientation', + value: '', + )); + }); + } + + QueryBuilder + orientationIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'orientation', + value: '', + )); + }); + } + QueryBuilder stateIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2377,6 +2541,18 @@ extension ExifInfoQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByOrientation() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.asc); + }); + } + + QueryBuilder sortByOrientationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.desc); + }); + } + QueryBuilder sortByState() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'state', Sort.asc); @@ -2584,6 +2760,18 @@ extension ExifInfoQuerySortThenBy }); } + QueryBuilder thenByOrientation() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.asc); + }); + } + + QueryBuilder thenByOrientationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'orientation', Sort.desc); + }); + } + QueryBuilder thenByState() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'state', Sort.asc); @@ -2701,6 +2889,13 @@ extension ExifInfoQueryWhereDistinct }); } + QueryBuilder distinctByOrientation( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'orientation', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByState( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2809,6 +3004,12 @@ extension ExifInfoQueryProperty }); } + QueryBuilder orientationProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'orientation'); + }); + } + QueryBuilder stateProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'state'); diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index df1b03f21825e..f6c66aa608591 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart'; import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; import 'package:native_video_player/native_video_player.dart'; @@ -76,6 +77,16 @@ class NativeVideoViewerPage extends HookConsumerWidget { type: VideoSourceType.file, ); } else { + final assetWithExif = + await ref.read(assetServiceProvider).loadExif(asset); + final shouldFlip = assetWithExif.exifInfo?.isFlipped ?? false; + width.value = (shouldFlip ? assetWithExif.height : assetWithExif.width) + ?.toDouble() ?? + width.value; + height.value = (shouldFlip ? assetWithExif.width : assetWithExif.height) + ?.toDouble() ?? + height.value; + // Use a network URL for the video player controller final serverEndpoint = Store.get(StoreKey.serverEndpoint); final String videoUrl = asset.livePhotoVideoId != null @@ -93,10 +104,14 @@ class NativeVideoViewerPage extends HookConsumerWidget { // When the volume changes, set the volume ref.listen(videoPlayerControlsProvider.select((value) => value.mute), (_, mute) { - if (mute) { - controller.value?.setVolume(0.0); - } else { - controller.value?.setVolume(0.7); + try { + if (mute) { + controller.value?.setVolume(0.0); + } else { + controller.value?.setVolume(0.7); + } + } catch (_) { + // Consume error from the controller } }); @@ -110,16 +125,24 @@ class NativeVideoViewerPage extends HookConsumerWidget { // Find the position to seek to final Duration seek = asset.duration * (position / 100.0); - controller.value?.seekTo(seek.inSeconds); + try { + controller.value?.seekTo(seek.inSeconds); + } catch (_) { + // Consume error from the controller + } }); // When the custom video controls paus or plays ref.listen(videoPlayerControlsProvider.select((value) => value.pause), (_, pause) { - if (pause) { - controller.value?.pause(); - } else { - controller.value?.play(); + try { + if (pause) { + controller.value?.pause(); + } else { + controller.value?.play(); + } + } catch (_) { + // Consume error from the controller } }); @@ -153,8 +176,12 @@ class NativeVideoViewerPage extends HookConsumerWidget { } void onPlaybackReady() { - controller.value?.play(); - controller.value?.setVolume(0.9); + try { + controller.value?.play(); + controller.value?.setVolume(0.9); + } catch (_) { + // Consume error from the controller + } } void onPlaybackPositionChanged() { @@ -162,8 +189,12 @@ class NativeVideoViewerPage extends HookConsumerWidget { } void onPlaybackEnded() { - if (loopVideo) { - controller.value?.play(); + try { + if (loopVideo) { + controller.value?.play(); + } + } catch (_) { + // Consume error from the controller } } @@ -199,12 +230,17 @@ class NativeVideoViewerPage extends HookConsumerWidget { return () { bufferingTimer.value.cancel(); - controller.value?.onPlaybackPositionChanged - .removeListener(onPlaybackPositionChanged); - controller.value?.onPlaybackStatusChanged - .removeListener(onPlaybackPositionChanged); - controller.value?.onPlaybackReady.removeListener(onPlaybackReady); - controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded); + try { + controller.value?.onPlaybackPositionChanged + .removeListener(onPlaybackPositionChanged); + controller.value?.onPlaybackStatusChanged + .removeListener(onPlaybackPositionChanged); + controller.value?.onPlaybackReady.removeListener(onPlaybackReady); + controller.value?.onPlaybackEnded.removeListener(onPlaybackEnded); + controller.value?.stop(); + } catch (_) { + // Consume error from the controller + } }; }, [], diff --git a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart index dad46593925cb..bffe6c7cf6f9f 100644 --- a/mobile/lib/providers/asset_viewer/video_player_value_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_value_provider.dart @@ -33,8 +33,14 @@ class VideoPlaybackValue { factory VideoPlaybackValue.fromNativeController( NativeVideoPlayerController controller, ) { - final playbackInfo = controller.playbackInfo; - final videoInfo = controller.videoInfo; + PlaybackInfo? playbackInfo; + VideoInfo? videoInfo; + try { + playbackInfo = controller.playbackInfo; + videoInfo = controller.videoInfo; + } catch (_) { + // Consume error from the controller + } late VideoPlaybackState s; if (playbackInfo?.status == null) { s = VideoPlaybackState.initializing; diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 2b02a5ff8f290..67ff060075383 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/utils/db.dart'; import 'package:isar/isar.dart'; -const int targetVersion = 6; +const int targetVersion = 7; Future migrateDatabaseIfNeeded(Isar db) async { final int version = Store.get(StoreKey.version, 1);