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:
@@ -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)}';
|
||||
}
|
||||
}
|
||||
|
||||
+5
-10
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user