diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart
index 63d06f5d2c..583e627c5d 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 016f6d7126..2c63f91cff 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 df1b03f218..f6c66aa608 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 dad4659392..bffe6c7cf6 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 2b02a5ff8f..67ff060075 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);