瀏覽代碼

fix(mobile): shows asset datetime with original timezone (#4774)

waclaw66 1 年之前
父節點
當前提交
33ce2b7bba

+ 3 - 0
mobile/lib/main.dart

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:timezone/data/latest.dart';
 import 'package:immich_mobile/constants/locales.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
@@ -77,6 +78,8 @@ Future<void> initApp() async {
     log.severe('Catch all error: ${error.toString()} - $error', error, stack);
     return true;
   };
+
+  initializeTimeZones();
 }
 
 Future<Isar> loadDb() async {

+ 29 - 4
mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:timezone/timezone.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
 import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -26,12 +27,36 @@ class ExifBottomSheet extends HookConsumerWidget {
       exifInfo.latitude != 0 &&
       exifInfo.longitude != 0;
 
+  String formatTimeZone(Duration d) =>
+      "GMT${d.isNegative ? '-': '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
+
   String get formattedDateTime {
-    final fileCreatedAt = asset.fileCreatedAt.toLocal();
-    final date = DateFormat.yMMMEd().format(fileCreatedAt);
-    final time = DateFormat.jm().format(fileCreatedAt);
+    DateTime dt = asset.fileCreatedAt.toLocal();
+    String? timeZone;
+    if (asset.exifInfo?.dateTimeOriginal != null) {
+      dt = asset.exifInfo!.dateTimeOriginal!;
+      if (asset.exifInfo?.timeZone != null) {
+        dt = dt.toUtc();
+        try {
+          final location = getLocation(asset.exifInfo!.timeZone!);
+          dt = TZDateTime.from(dt, location);
+        } on LocationNotFoundException {
+          RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false);
+          final m = re.firstMatch(asset.exifInfo!.timeZone!);
+          if (m != null) {
+            final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0'));
+            dt = dt.add(duration);
+            timeZone = formatTimeZone(duration);
+          }
+        }
+      }
+    }
+
+    final date = DateFormat.yMMMEd().format(dt);
+    final time = DateFormat.jm().format(dt);
+    timeZone ??= formatTimeZone(dt.timeZoneOffset);
 
-    return '$date • $time';
+    return '$date • $time $timeZone';
   }
 
   Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {

+ 14 - 0
mobile/lib/shared/models/exif_info.dart

@@ -8,6 +8,8 @@ part 'exif_info.g.dart';
 class ExifInfo {
   Id? id;
   int? fileSize;
+  DateTime? dateTimeOriginal;
+  String? timeZone;
   String? make;
   String? model;
   String? lens;
@@ -47,6 +49,8 @@ class ExifInfo {
 
   ExifInfo.fromDto(ExifResponseDto dto)
       : fileSize = dto.fileSizeInByte,
+        dateTimeOriginal = dto.dateTimeOriginal,
+        timeZone = dto.timeZone,
         make = dto.make,
         model = dto.model,
         lens = dto.lensModel,
@@ -64,6 +68,8 @@ class ExifInfo {
   ExifInfo({
     this.id,
     this.fileSize,
+    this.dateTimeOriginal,
+    this.timeZone,
     this.make,
     this.model,
     this.lens,
@@ -82,6 +88,8 @@ class ExifInfo {
   ExifInfo copyWith({
     Id? id,
     int? fileSize,
+    DateTime? dateTimeOriginal,
+    String? timeZone,
     String? make,
     String? model,
     String? lens,
@@ -99,6 +107,8 @@ class ExifInfo {
       ExifInfo(
         id: id ?? this.id,
         fileSize: fileSize ?? this.fileSize,
+        dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
+        timeZone: timeZone ?? this.timeZone,
         make: make ?? this.make,
         model: model ?? this.model,
         lens: lens ?? this.lens,
@@ -119,6 +129,8 @@ class ExifInfo {
     if (other is! ExifInfo) return false;
     return id == other.id &&
         fileSize == other.fileSize &&
+        dateTimeOriginal == other.dateTimeOriginal &&
+        timeZone == other.timeZone &&
         make == other.make &&
         model == other.model &&
         lens == other.lens &&
@@ -139,6 +151,8 @@ class ExifInfo {
   int get hashCode =>
       id.hashCode ^
       fileSize.hashCode ^
+      dateTimeOriginal.hashCode ^
+      timeZone.hashCode ^
       make.hashCode ^
       model.hashCode ^
       lens.hashCode ^

+ 364 - 46
mobile/lib/shared/models/exif_info.g.dart

@@ -27,65 +27,75 @@ const ExifInfoSchema = CollectionSchema(
       name: r'country',
       type: IsarType.string,
     ),
-    r'description': PropertySchema(
+    r'dateTimeOriginal': PropertySchema(
       id: 2,
+      name: r'dateTimeOriginal',
+      type: IsarType.dateTime,
+    ),
+    r'description': PropertySchema(
+      id: 3,
       name: r'description',
       type: IsarType.string,
     ),
     r'exposureSeconds': PropertySchema(
-      id: 3,
+      id: 4,
       name: r'exposureSeconds',
       type: IsarType.float,
     ),
     r'f': PropertySchema(
-      id: 4,
+      id: 5,
       name: r'f',
       type: IsarType.float,
     ),
     r'fileSize': PropertySchema(
-      id: 5,
+      id: 6,
       name: r'fileSize',
       type: IsarType.long,
     ),
     r'iso': PropertySchema(
-      id: 6,
+      id: 7,
       name: r'iso',
       type: IsarType.int,
     ),
     r'lat': PropertySchema(
-      id: 7,
+      id: 8,
       name: r'lat',
       type: IsarType.float,
     ),
     r'lens': PropertySchema(
-      id: 8,
+      id: 9,
       name: r'lens',
       type: IsarType.string,
     ),
     r'long': PropertySchema(
-      id: 9,
+      id: 10,
       name: r'long',
       type: IsarType.float,
     ),
     r'make': PropertySchema(
-      id: 10,
+      id: 11,
       name: r'make',
       type: IsarType.string,
     ),
     r'mm': PropertySchema(
-      id: 11,
+      id: 12,
       name: r'mm',
       type: IsarType.float,
     ),
     r'model': PropertySchema(
-      id: 12,
+      id: 13,
       name: r'model',
       type: IsarType.string,
     ),
     r'state': PropertySchema(
-      id: 13,
+      id: 14,
       name: r'state',
       type: IsarType.string,
+    ),
+    r'timeZone': PropertySchema(
+      id: 15,
+      name: r'timeZone',
+      type: IsarType.string,
     )
   },
   estimateSize: _exifInfoEstimateSize,
@@ -150,6 +160,12 @@ int _exifInfoEstimateSize(
       bytesCount += 3 + value.length * 3;
     }
   }
+  {
+    final value = object.timeZone;
+    if (value != null) {
+      bytesCount += 3 + value.length * 3;
+    }
+  }
   return bytesCount;
 }
 
@@ -161,18 +177,20 @@ void _exifInfoSerialize(
 ) {
   writer.writeString(offsets[0], object.city);
   writer.writeString(offsets[1], object.country);
-  writer.writeString(offsets[2], object.description);
-  writer.writeFloat(offsets[3], object.exposureSeconds);
-  writer.writeFloat(offsets[4], object.f);
-  writer.writeLong(offsets[5], object.fileSize);
-  writer.writeInt(offsets[6], object.iso);
-  writer.writeFloat(offsets[7], object.lat);
-  writer.writeString(offsets[8], object.lens);
-  writer.writeFloat(offsets[9], object.long);
-  writer.writeString(offsets[10], object.make);
-  writer.writeFloat(offsets[11], object.mm);
-  writer.writeString(offsets[12], object.model);
-  writer.writeString(offsets[13], object.state);
+  writer.writeDateTime(offsets[2], object.dateTimeOriginal);
+  writer.writeString(offsets[3], object.description);
+  writer.writeFloat(offsets[4], object.exposureSeconds);
+  writer.writeFloat(offsets[5], object.f);
+  writer.writeLong(offsets[6], object.fileSize);
+  writer.writeInt(offsets[7], object.iso);
+  writer.writeFloat(offsets[8], object.lat);
+  writer.writeString(offsets[9], object.lens);
+  writer.writeFloat(offsets[10], object.long);
+  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);
 }
 
 ExifInfo _exifInfoDeserialize(
@@ -184,19 +202,21 @@ ExifInfo _exifInfoDeserialize(
   final object = ExifInfo(
     city: reader.readStringOrNull(offsets[0]),
     country: reader.readStringOrNull(offsets[1]),
-    description: reader.readStringOrNull(offsets[2]),
-    exposureSeconds: reader.readFloatOrNull(offsets[3]),
-    f: reader.readFloatOrNull(offsets[4]),
-    fileSize: reader.readLongOrNull(offsets[5]),
+    dateTimeOriginal: reader.readDateTimeOrNull(offsets[2]),
+    description: reader.readStringOrNull(offsets[3]),
+    exposureSeconds: reader.readFloatOrNull(offsets[4]),
+    f: reader.readFloatOrNull(offsets[5]),
+    fileSize: reader.readLongOrNull(offsets[6]),
     id: id,
-    iso: reader.readIntOrNull(offsets[6]),
-    lat: reader.readFloatOrNull(offsets[7]),
-    lens: reader.readStringOrNull(offsets[8]),
-    long: reader.readFloatOrNull(offsets[9]),
-    make: reader.readStringOrNull(offsets[10]),
-    mm: reader.readFloatOrNull(offsets[11]),
-    model: reader.readStringOrNull(offsets[12]),
-    state: reader.readStringOrNull(offsets[13]),
+    iso: reader.readIntOrNull(offsets[7]),
+    lat: reader.readFloatOrNull(offsets[8]),
+    lens: reader.readStringOrNull(offsets[9]),
+    long: reader.readFloatOrNull(offsets[10]),
+    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]),
   );
   return object;
 }
@@ -213,29 +233,33 @@ P _exifInfoDeserializeProp<P>(
     case 1:
       return (reader.readStringOrNull(offset)) as P;
     case 2:
-      return (reader.readStringOrNull(offset)) as P;
+      return (reader.readDateTimeOrNull(offset)) as P;
     case 3:
-      return (reader.readFloatOrNull(offset)) as P;
+      return (reader.readStringOrNull(offset)) as P;
     case 4:
       return (reader.readFloatOrNull(offset)) as P;
     case 5:
-      return (reader.readLongOrNull(offset)) as P;
+      return (reader.readFloatOrNull(offset)) as P;
     case 6:
-      return (reader.readIntOrNull(offset)) as P;
+      return (reader.readLongOrNull(offset)) as P;
     case 7:
-      return (reader.readFloatOrNull(offset)) as P;
+      return (reader.readIntOrNull(offset)) as P;
     case 8:
-      return (reader.readStringOrNull(offset)) as P;
-    case 9:
       return (reader.readFloatOrNull(offset)) as P;
-    case 10:
+    case 9:
       return (reader.readStringOrNull(offset)) as P;
-    case 11:
+    case 10:
       return (reader.readFloatOrNull(offset)) as P;
-    case 12:
+    case 11:
       return (reader.readStringOrNull(offset)) as P;
+    case 12:
+      return (reader.readFloatOrNull(offset)) as P;
     case 13:
       return (reader.readStringOrNull(offset)) as P;
+    case 14:
+      return (reader.readStringOrNull(offset)) as P;
+    case 15:
+      return (reader.readStringOrNull(offset)) as P;
     default:
       throw IsarError('Unknown property with id $propertyId');
   }
@@ -622,6 +646,80 @@ extension ExifInfoQueryFilter
     });
   }
 
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalIsNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNull(
+        property: r'dateTimeOriginal',
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalIsNotNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNotNull(
+        property: r'dateTimeOriginal',
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalEqualTo(DateTime? value) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'dateTimeOriginal',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalGreaterThan(
+    DateTime? value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'dateTimeOriginal',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalLessThan(
+    DateTime? value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'dateTimeOriginal',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
+      dateTimeOriginalBetween(
+    DateTime? lower,
+    DateTime? upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'dateTimeOriginal',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+      ));
+    });
+  }
+
   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionIsNull() {
     return QueryBuilder.apply(this, (query) {
       return query.addFilterCondition(const FilterCondition.isNull(
@@ -1956,6 +2054,152 @@ extension ExifInfoQueryFilter
       ));
     });
   }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneIsNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNull(
+        property: r'timeZone',
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneIsNotNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNotNull(
+        property: r'timeZone',
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneEqualTo(
+    String? value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneGreaterThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneLessThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneBetween(
+    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'timeZone',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneStartsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.startsWith(
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneEndsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.endsWith(
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneContains(
+      String value,
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.contains(
+        property: r'timeZone',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneMatches(
+      String pattern,
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.matches(
+        property: r'timeZone',
+        wildcard: pattern,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneIsEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'timeZone',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> timeZoneIsNotEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        property: r'timeZone',
+        value: '',
+      ));
+    });
+  }
 }
 
 extension ExifInfoQueryObject
@@ -1989,6 +2233,18 @@ extension ExifInfoQuerySortBy on QueryBuilder<ExifInfo, ExifInfo, QSortBy> {
     });
   }
 
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDateTimeOriginal() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'dateTimeOriginal', Sort.asc);
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDateTimeOriginalDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'dateTimeOriginal', Sort.desc);
+    });
+  }
+
   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDescription() {
     return QueryBuilder.apply(this, (query) {
       return query.addSortBy(r'description', Sort.asc);
@@ -2132,6 +2388,18 @@ extension ExifInfoQuerySortBy on QueryBuilder<ExifInfo, ExifInfo, QSortBy> {
       return query.addSortBy(r'state', Sort.desc);
     });
   }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByTimeZone() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'timeZone', Sort.asc);
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByTimeZoneDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'timeZone', Sort.desc);
+    });
+  }
 }
 
 extension ExifInfoQuerySortThenBy
@@ -2160,6 +2428,18 @@ extension ExifInfoQuerySortThenBy
     });
   }
 
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDateTimeOriginal() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'dateTimeOriginal', Sort.asc);
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDateTimeOriginalDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'dateTimeOriginal', Sort.desc);
+    });
+  }
+
   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDescription() {
     return QueryBuilder.apply(this, (query) {
       return query.addSortBy(r'description', Sort.asc);
@@ -2315,6 +2595,18 @@ extension ExifInfoQuerySortThenBy
       return query.addSortBy(r'state', Sort.desc);
     });
   }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByTimeZone() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'timeZone', Sort.asc);
+    });
+  }
+
+  QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByTimeZoneDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'timeZone', Sort.desc);
+    });
+  }
 }
 
 extension ExifInfoQueryWhereDistinct
@@ -2333,6 +2625,12 @@ extension ExifInfoQueryWhereDistinct
     });
   }
 
+  QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByDateTimeOriginal() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'dateTimeOriginal');
+    });
+  }
+
   QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByDescription(
       {bool caseSensitive = true}) {
     return QueryBuilder.apply(this, (query) {
@@ -2409,6 +2707,13 @@ extension ExifInfoQueryWhereDistinct
       return query.addDistinctBy(r'state', caseSensitive: caseSensitive);
     });
   }
+
+  QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByTimeZone(
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'timeZone', caseSensitive: caseSensitive);
+    });
+  }
 }
 
 extension ExifInfoQueryProperty
@@ -2431,6 +2736,13 @@ extension ExifInfoQueryProperty
     });
   }
 
+  QueryBuilder<ExifInfo, DateTime?, QQueryOperations>
+      dateTimeOriginalProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'dateTimeOriginal');
+    });
+  }
+
   QueryBuilder<ExifInfo, String?, QQueryOperations> descriptionProperty() {
     return QueryBuilder.apply(this, (query) {
       return query.addPropertyName(r'description');
@@ -2502,4 +2814,10 @@ extension ExifInfoQueryProperty
       return query.addPropertyName(r'state');
     });
   }
+
+  QueryBuilder<ExifInfo, String?, QQueryOperations> timeZoneProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'timeZone');
+    });
+  }
 }

+ 1 - 1
mobile/pubspec.lock

@@ -1401,7 +1401,7 @@ packages:
     source: hosted
     version: "2.1.3"
   timezone:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: timezone
       sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"

+ 1 - 0
mobile/pubspec.yaml

@@ -52,6 +52,7 @@ dependencies:
   crypto: ^3.0.3 # TODO remove once native crypto is used on iOS
   wakelock_plus: ^1.1.1
   flutter_local_notifications: ^15.1.0+1
+  timezone: ^0.9.2
 
   openapi:
     path: openapi