fix(mobile): respect 24h system setting (#30792)

* fix(mobile): respect 24h system setting

* refactor: extract into utils

* chore: remove comment

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

* refactor: resolveAssetDateTime

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

* refactor: alwaysUse24HourFormat

* chore: impl suggestions

* refactor: less comments & resolveDateTimeLocale

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
This commit is contained in:
Yaros
2026-08-23 17:51:03 +05:30
committed by GitHub
co-authored by shenlong
parent 44a9384758
commit c98c20e963
6 changed files with 65 additions and 59 deletions
+31 -33
View File
@@ -1,6 +1,10 @@
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
/// The active locale for date/time formatting, falling back to `en_US` for
/// supported locales that intl has no date-formatting data for.
String resolvedDateTimeLocale() =>
Intl.verifiedLocale(Intl.defaultLocale, DateFormat.localeExists, onFailure: (_) => 'en_US')!;
extension TimeAgoExtension on DateTime {
/// Displays the time difference of this [DateTime] object to the current time as a [String]
String timeAgo({bool numericDates = true}) {
@@ -39,7 +43,22 @@ extension TimeAgoExtension on DateTime {
}
}
/// Extension to format date ranges according to UI requirements
extension TimeFormatting on DateTime {
/// 24-hour `HH:mm` when [alwaysUse24HourFormat], otherwise the locale's 12-hour format.
String formatTime({required bool alwaysUse24HourFormat}) {
final locale = resolvedDateTimeLocale();
return alwaysUse24HourFormat ? DateFormat.Hm(locale).format(this) : DateFormat.jm(locale).format(this);
}
}
extension DateFormatting on DateTime {
/// Formats a single date, omitting the year when it is the current year.
String formatDate() {
final locale = resolvedDateTimeLocale();
return year == DateTime.now().year ? DateFormat.MMMd(locale).format(this) : DateFormat.yMMMd(locale).format(this);
}
}
extension DateRangeFormatting on DateTime {
/// Formats a date range according to specific rules:
/// - Single date of this year: "Aug 28"
@@ -47,41 +66,20 @@ extension DateRangeFormatting on DateTime {
/// - Date range of this year: "Mar 23-May 31"
/// - Date range of other year: "Aug 28 - Sep 30, 2023"
/// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
static String formatDateRange(DateTime startDate, DateTime endDate, Locale? locale) {
final now = DateTime.now();
final currentYear = now.year;
final localeString = locale?.toString() ?? 'en_US';
// Check if it's a single date (same day)
static String formatDateRange(DateTime startDate, DateTime endDate) {
if (startDate.year == endDate.year && startDate.month == endDate.month && startDate.day == endDate.day) {
if (startDate.year == currentYear) {
// Single date of this year: "Aug 28"
return DateFormat.MMMd(localeString).format(startDate);
} else {
// Single date of other year: "Aug 28, 2023"
return DateFormat.yMMMd(localeString).format(startDate);
}
return startDate.formatDate();
}
// It's a date range
final locale = resolvedDateTimeLocale();
if (startDate.year == endDate.year) {
// Same year
if (startDate.year == currentYear) {
// Date range of this year: "Mar 23-May 31"
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted';
} else {
// Date range of other year: "Aug 28 - Sep 30, 2023"
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted, ${startDate.year}';
}
} else {
// Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted';
final format = DateFormat.MMMd(locale);
final range = '${format.format(startDate)} - ${format.format(endDate)}';
return startDate.year == DateTime.now().year ? range : '$range, ${startDate.year}';
}
final format = DateFormat.yMMMd(locale);
return '${format.format(startDate)} - ${format.format(endDate)}';
}
}
@@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart';
@@ -45,18 +46,12 @@ class DateTimeDetails extends ConsumerWidget {
}
static String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) {
DateTime dateTime = asset.createdAt.toLocal();
Duration timeZoneOffset = dateTime.timeZoneOffset;
final alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(ctx);
if (exifInfo?.dateTimeOriginal != null) {
(dateTime, timeZoneOffset) = applyTimezoneOffset(
dateTime: exifInfo!.dateTimeOriginal!,
timeZone: exifInfo.timeZone,
);
}
final (dateTime, timeZoneOffset) = resolveAssetDateTime(asset, exifInfo);
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
final date = DateFormat.yMMMEd(resolvedDateTimeLocale()).format(dateTime);
final time = dateTime.formatTime(alwaysUse24HourFormat: alwaysUse24HourFormat);
final timezone = 'GMT${timeZoneOffset.formatAsOffset()}';
return '$date$_kSeparator$time $timezone';
}
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/presentation/actions/action.widget.dart';
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
@@ -156,17 +156,13 @@ class _AssetInfoTitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
DateTime dateTime = asset.createdAt.toLocal();
final currentYear = DateTime.now().year;
final exifInfo = ref.watch(assetExifProvider(asset)).valueOrNull;
final alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(context);
if (exifInfo?.dateTimeOriginal != null) {
(dateTime, _) = applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo.timeZone);
}
final (dateTime, _) = resolveAssetDateTime(asset, exifInfo);
final isCurrentYear = dateTime.year == currentYear;
final dateFormatted = isCurrentYear ? DateFormat.MMMd().format(dateTime) : DateFormat.yMMMd().format(dateTime);
final timeFormatted = DateFormat.jm().format(dateTime);
final dateFormatted = dateTime.formatDate();
final timeFormatted = dateTime.formatTime(alwaysUse24HourFormat: alwaysUse24HourFormat);
return Column(
mainAxisSize: MainAxisSize.min,
+17
View File
@@ -1,3 +1,5 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:timezone/timezone.dart';
/// Applies timezone conversion to a DateTime using EXIF timezone information.
@@ -35,3 +37,18 @@ import 'package:timezone/timezone.dart';
// If timezone is invalid, return UTC
return (dt, dt.timeZoneOffset);
}
/// Resolves the effective local [DateTime] (and its timezone offset) for an asset.
///
/// Prefers the EXIF `dateTimeOriginal` with its timezone when available,
/// otherwise falls back to the asset's [BaseAsset.createdAt] in local time.
///
/// Returns a tuple of (resolved DateTime, timezone offset Duration).
(DateTime, Duration) resolveAssetDateTime(BaseAsset asset, ExifInfo? exifInfo) {
if (exifInfo?.dateTimeOriginal == null) {
final dateTime = asset.createdAt.toLocal();
return (dateTime, dateTime.timeZoneOffset);
}
return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo.timeZone);
}
@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -240,7 +239,6 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
DateRangeFormatting.formatDateRange(
dateRange.value!.$1.toLocal(),
dateRange.value!.$2.toLocal(),
context.locale,
),
style: const TextStyle(
color: Colors.white,
@@ -1,10 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
void main() {
setUpAll(() async {
await initializeDateFormatting();
Intl.defaultLocale = 'en_US';
});
group('DateRangeFormatting.formatDateRange', () {
@@ -12,34 +14,34 @@ void main() {
test('returns single date format for this year', () {
final date = DateTime(currentYear, 8, 28); // Aug 28 this year
final result = DateRangeFormatting.formatDateRange(date, date, null);
final result = DateRangeFormatting.formatDateRange(date, date);
expect(result, 'Aug 28');
});
test('returns single date format for other year', () {
final date = DateTime(2023, 8, 28); // Aug 28, 2023
final result = DateRangeFormatting.formatDateRange(date, date, null);
final result = DateRangeFormatting.formatDateRange(date, date);
expect(result, 'Aug 28, 2023');
});
test('returns date range format for this year', () {
final startDate = DateTime(currentYear, 3, 23); // Mar 23
final endDate = DateTime(currentYear, 5, 31); // May 31
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
final result = DateRangeFormatting.formatDateRange(startDate, endDate);
expect(result, 'Mar 23 - May 31');
});
test('returns date range format for other year (same year)', () {
final startDate = DateTime(2023, 8, 28); // Aug 28
final endDate = DateTime(2023, 9, 30); // Sep 30
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
final result = DateRangeFormatting.formatDateRange(startDate, endDate);
expect(result, 'Aug 28 - Sep 30, 2023');
});
test('returns date range format over multiple years', () {
final startDate = DateTime(2021, 4, 17); // Apr 17, 2021
final endDate = DateTime(2022, 4, 9); // Apr 9, 2022
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
final result = DateRangeFormatting.formatDateRange(startDate, endDate);
expect(result, 'Apr 17, 2021 - Apr 9, 2022');
});
});