From 33ce2b7bba7fa148d54a3af1cfbe40e1eba927c2 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 3 Nov 2023 15:04:41 +0100 Subject: [PATCH] fix(mobile): shows asset datetime with original timezone (#4774) --- mobile/lib/main.dart | 3 ++ .../asset_viewer/ui/exif_bottom_sheet.dart | 35 +++++++++++++++--- mobile/lib/shared/models/exif_info.dart | 14 +++++++ mobile/lib/shared/models/exif_info.g.dart | Bin 70571 -> 79865 bytes mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 1 + 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 3ad2f4b62..13eda9d6e 100644 --- a/mobile/lib/main.dart +++ b/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 initApp() async { log.severe('Catch all error: ${error.toString()} - $error', error, stack); return true; }; + + initializeTimeZones(); } Future loadDb() async { diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index df1c8ba6f..f194738a2 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/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 get formattedDateTime { - final fileCreatedAt = asset.fileCreatedAt.toLocal(); - final date = DateFormat.yMMMEd().format(fileCreatedAt); - final time = DateFormat.jm().format(fileCreatedAt); + String formatTimeZone(Duration d) => + "GMT${d.isNegative ? '-': '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; - return '$date • $time'; + String get formattedDateTime { + 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 $timeZone'; } Future _createCoordinatesUri(ExifInfo? exifInfo) async { diff --git a/mobile/lib/shared/models/exif_info.dart b/mobile/lib/shared/models/exif_info.dart index 568e4ce13..a61fd2c28 100644 --- a/mobile/lib/shared/models/exif_info.dart +++ b/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 ^ diff --git a/mobile/lib/shared/models/exif_info.g.dart b/mobile/lib/shared/models/exif_info.g.dart index 9122942bd983d18f907f4d0bcb6eea9dd2376e1d..138e386c79286d2179f18a3085424b9f09677780 100644 GIT binary patch delta 1969 zcma)7U2IcT9M9R>-rIK2`&C*eN>{S2gKn%_%L?^lOaWaO>$c(&2xeDyVKZgR+HMOW zGt37cbS4`=O-%^FJ@H|plIwzrL6GQ7z<^79s8Pcfcu>Z0L}O5c^RDZ?m$enSFX#Vt ze&_f9IOl$F((vc6hL4XKY+^!+bRU$OW8DY3d&DSw^b@#b;BxFIC@0;S3C3H7I@B3i z+}tVP6lKG8+77GHOZ*5LyJ0YKkc}*M;CW^0SQV`TyHKA++4>D|9SE+yi7YW8|vy|26~?Mqatd1*!HU|ggY zFWdaMVzc77t%?Njjje)g#2>Z|qz+|_mjuzqRFX{?W-1GO)%b|nKzz8)R3XF`;W5^Y zC~L(wwv6%X2gMT#3n=KkFQ8j98OVoOLi}0%P%@OnPAOKkxTrZa$9DEdqk1NYa{-0# zX)cH&-hzu<37+Q7u=5^;@iY9!qPo2mOJ$3SiM0D$-cl&_#1ZD)_(uu-o-akEU`E#M2&?+@dJBtxmRf6>#k2&WG^sf?e zi|5kv?}~jL&!*)XM+xi>HwGQ6HNULb)P27xK6PKf>DKH6imci{bv97@#JJ@0`EkJ+ z%E$rvf|FT~r?%5MLp*kP%$C(`Oix7e$NH216}u*@Nn88+#b~70qZOfkV1&s!CyL?r zHMld-xOkW+3t+p_-Gmg2bcsEl^muDXQ(adcid33~v_p!=^TLc`ba(KfP{Ayy1}QNp zNm+!RbwzszrP$ImWmq14y%7tsH*%^q^Vw@ya5D%uZj*m_u4xqYl_C9RD!tH^nTZIc zUS#(8d&?ZU^3&F@v?$l9F;$ku69q(7pg8m$M?8N&=ul;O>av~aQT3rax)L6_tcERO zY4Vn}apq-Bf!{pzl@_rc*5L&7PL0Q=i!2x)G!zOAjsj?zL$t zoxRaUXY3O0re4w<;?31d{674-{*n0&|MJNE+T^*}^DO4)^79&>|36+|-VYb#savvx zkQF%>A=y_*UYy{`eOj5)(&R-G5!7-d=Z)l$n)bOFnw%-1GBr+q!4kV8^F!)f-lJ&p ptd)q_g+C`+$qS2T6en#g(SKBn!5ziw%UWI$N`nri*CET0xiw;B_(=s%AN1Uie71K5fkBxMZHA(UtY z*XeF@2=}SWTUMz<4x>#=WC$r%BEzUN7bjT+cUTTLnU2@2AJ4cySUy^AIGJk~uUx+5 z4a|5`o?q`H1oMW2rlI1UG000gMiFg8L)I+tlDb*hQEBXRvVY3g`u z=JC?huwmxVGF6yX0lKAO#LA+|ov{jh^F@n)y0mkW;6gHwxp)dKJBRo2^Ej8tqLxVE zL82dX-0pmZ+r78l%Xu8NyLm5~+?Jik>i;YxtNw@N*CJ*ob$@yE)V+mo`-$yOcQ!+K z_(${GzZW Cqofu9 diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 73bfd11b0..3fc34f62e 100644 --- a/mobile/pubspec.lock +++ b/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" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 63c6f312f..6d5aa735d 100644 --- a/mobile/pubspec.yaml +++ b/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