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 9122942bd..138e386c7 100644 Binary files a/mobile/lib/shared/models/exif_info.g.dart and b/mobile/lib/shared/models/exif_info.g.dart differ 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