diff --git a/CHANGELOG.md b/CHANGELOG.md index 484acbdfe..a582eef9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. ### Changed - opening app from launcher always show home page +- use dates with western arabic numerals for maghreb arabic locales - upgraded Flutter to stable v3.19.4 ### Fixed diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart index cf92324c9..29d1d447d 100644 --- a/lib/model/entry/extensions/metadata_edition.dart +++ b/lib/model/entry/extensions/metadata_edition.dart @@ -7,6 +7,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/catalog.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/metadata/date_modifier.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/ref/metadata/exif.dart'; import 'package:aves/ref/metadata/iptc.dart'; import 'package:aves/ref/metadata/xmp.dart'; @@ -121,7 +122,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { if (latLng != null && latLng != removalLocation) { final latitude = latLng.latitude; final longitude = latLng.longitude; - const locale = 'en_US'; + const locale = asciiLocale; final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}'; final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}'; iso6709String = '$isoLat$isoLon/'; diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart index 851d58011..ab988fef5 100644 --- a/lib/model/naming_pattern.dart +++ b/lib/model/naming_pattern.dart @@ -20,6 +20,7 @@ class NamingPattern { factory NamingPattern.from({ required String userPattern, required int entryCount, + required String locale, }) { final processors = []; @@ -40,7 +41,7 @@ class NamingPattern { switch (processorKey) { case DateNamingProcessor.key: if (processorOptions != null) { - processors.add(DateNamingProcessor(processorOptions.trim())); + processors.add(DateNamingProcessor(processorOptions.trim(), locale)); } case TagsNamingProcessor.key: processors.add(TagsNamingProcessor(processorOptions?.trim() ?? '')); @@ -156,7 +157,7 @@ class DateNamingProcessor extends NamingProcessor { @override List get props => [format.pattern]; - DateNamingProcessor(String pattern) : format = DateFormat(pattern); + DateNamingProcessor(String pattern, String locale) : format = DateFormat(pattern, locale); @override String? process(AvesEntry entry, int index, Map fieldValues) { diff --git a/lib/model/settings/modules/app.dart b/lib/model/settings/modules/app.dart index a4b1f9f86..347541582 100644 --- a/lib/model/settings/modules/app.dart +++ b/lib/model/settings/modules/app.dart @@ -50,7 +50,7 @@ mixin AppSettings on SettingsAccess { ].join(localeSeparator); } set(SettingKeys.localeKey, tag); - _appliedLocale = null; + resetAppliedLocale(); } List _systemLocalesFallback = []; diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index 56023b89b..6ae8ba583 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -8,6 +8,7 @@ import 'package:aves/model/video/profiles/aac.dart'; import 'package:aves/model/video/profiles/h264.dart'; import 'package:aves/model/video/profiles/hevc.dart'; import 'package:aves/ref/languages.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mp4.dart'; import 'package:aves/services/common/services.dart'; @@ -453,7 +454,7 @@ class VideoMetadataFormatter { static String _formatFilesize(dynamic value) { final size = value is int ? value : int.tryParse(value); - return size != null ? formatFileSize('en_US', size) : value; + return size != null ? formatFileSize(asciiLocale, size) : value; } static String _formatLanguage(String value) { diff --git a/lib/ref/locales.dart b/lib/ref/locales.dart new file mode 100644 index 000000000..1ce72cdd1 --- /dev/null +++ b/lib/ref/locales.dart @@ -0,0 +1,47 @@ +import 'dart:ui'; + +const String asciiLocale = 'en_US'; + +// cf https://en.wikipedia.org/wiki/Eastern_Arabic_numerals +bool shouldUseNativeDigits(Locale? countrifiedLocale) { + switch (countrifiedLocale?.toString()) { + // Maghreb + case 'ar_DZ': // Algeria + case 'ar_EH': // Western Sahara + case 'ar_LY': // Libya + case 'ar_MA': // Morocco + case 'ar_MR': // Mauritania + case 'ar_TN': // Tunisia + return false; + // Mashriq + case 'ar_AE': // United Arab Emirates + case 'ar_BH': // Bahrain + case 'ar_EG': // Egypt + case 'ar_IQ': // Iraq + case 'ar_JO': // Jordan + case 'ar_KW': // Kuwait + case 'ar_LB': // Lebanon + case 'ar_OM': // Oman + case 'ar_PS': // Palestinian Territories + case 'ar_QA': // Qatar + case 'ar_SA': // Saudi Arabia + case 'ar_SD': // Sudan + case 'ar_SS': // South Sudan + case 'ar_SY': // Syria + case 'ar_YE': // Yemen + return true; + // Horn of Africa + case 'ar_DJ': // Djibouti + case 'ar_ER': // Eritrea + case 'ar_KM': // Comoros + case 'ar_SO': // Somalia + return true; + // others + case 'ar_IL': // Israel + case 'ar_TD': // Chad + return true; + case null: + default: + return true; + } +} diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index 72b120b32..b17f3cf5e 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -1,3 +1,4 @@ +import 'package:aves/ref/locales.dart'; import 'package:aves/ref/metadata/xmp.dart'; import 'package:intl/intl.dart'; import 'package:xml/xml.dart'; @@ -60,7 +61,7 @@ class XMP { return '${offsetMinutes.isNegative ? '-' : '+'}${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}'; } - static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss').format(date)}${_xmpTimeZoneDesignator(date)}'; + static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss', asciiLocale).format(date)}${_xmpTimeZoneDesignator(date)}'; static String? getString( List nodes, diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 74c28797b..0f801bf6a 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -6,6 +6,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/flutter_version.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; @@ -176,7 +177,7 @@ class _BugReportState extends State with FeedbackMixin { final result = await Process.run('logcat', ['-d']); final logs = result.stdout; final success = await storageService.createFile( - 'aves-logs-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.txt', + 'aves-logs-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}.txt', MimeTypes.plainText, Uint8List.fromList(utf8.encode(logs)), ); diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index b929eac08..bfe0bcc6a 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -16,6 +16,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; @@ -37,12 +38,14 @@ import 'package:aves/widgets/navigation/tv_rail.dart'; import 'package:aves/widgets/welcome_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:aves_utils/aves_utils.dart'; +import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization_nn/flutter_localization_nn.dart'; +import 'package:intl/intl.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:provider/provider.dart'; import 'package:screen_brightness/screen_brightness.dart'; @@ -158,6 +161,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { final ValueNotifier _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder); final ValueNotifier _tvMediaQueryModifierNotifier = ValueNotifier(null); final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.main); + final ValueNotifier _localeOverridesNotifier = ValueNotifier(LocaleOverrides.none); // observers are not registered when using the same list object with different items // the list itself needs to be reassigned @@ -217,6 +221,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { Provider.value(value: _tvRailController), DurationsProvider(), HighlightInfoProvider(), + ListenableProvider>.value(value: _localeOverridesNotifier), ], child: OverlaySupport( child: FutureBuilder( @@ -239,9 +244,6 @@ class _AvesAppState extends State with WidgetsBindingObserver { ), builder: (context, s, child) { final (settingsLocale, themeBrightness, enableDynamicColor) = s; - - AStyles.updateStylesForLocale(settings.appliedLocale); - return DynamicColorBuilder( builder: (lightScheme, darkScheme) { const defaultAccent = AvesColorsData.defaultAccent; @@ -411,6 +413,33 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } + @override + void didChangeLocales(List? locales) { + _applyLocale(); + } + + void _applyLocale() { + settings.resetAppliedLocale(); + + final appliedLocale = settings.appliedLocale; + AStyles.updateStylesForLocale(appliedLocale); + + Locale? countrifiedLocale; + if (appliedLocale.countryCode == null) { + final languageCode = appliedLocale.languageCode; + countrifiedLocale = WidgetsBinding.instance.platformDispatcher.locales.firstWhereOrNull((v) => v.languageCode == languageCode); + } + + if (appliedLocale.languageCode == 'ar') { + final useNativeDigits = shouldUseNativeDigits(countrifiedLocale); + DateFormat.useNativeDigitsByDefaultFor(appliedLocale.toString(), useNativeDigits); + DateFormat.useNativeDigitsByDefaultFor(countrifiedLocale.toString(), useNativeDigits); + } + _localeOverridesNotifier.value = LocaleOverrides( + countrifiedLocale: countrifiedLocale, + ); + } + Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); Size? _getScreenSize(BuildContext context) { @@ -504,7 +533,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { } void _monitorSettings() { - void applyIsInstalledAppAccessAllowed() { + void _applyIsInstalledAppAccessAllowed() { if (settings.isInstalledAppAccessAllowed) { appInventory.initAppNames(); } else { @@ -512,9 +541,9 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } - void applyDisplayRefreshRateMode() => settings.displayRefreshRateMode.apply(); + void _applyDisplayRefreshRateMode() => settings.displayRefreshRateMode.apply(); - void applyMaxBrightness() { + void _applyMaxBrightness() { switch (settings.maxBrightness) { case MaxBrightness.never: case MaxBrightness.viewerOnly: @@ -524,9 +553,9 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } - void applyKeepScreenOn() => settings.keepScreenOn.apply(); + void _applyKeepScreenOn() => settings.keepScreenOn.apply(); - void applyIsRotationLocked() { + void _applyIsRotationLocked() { if (!settings.isRotationLocked && !settings.useTvLayout) { windowService.requestOrientation(); } @@ -545,20 +574,22 @@ class _AvesAppState extends State with WidgetsBindingObserver { final settingStream = settings.updateStream; // app - settingStream.where((event) => event.key == SettingKeys.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); + settingStream.where((event) => event.key == SettingKeys.isInstalledAppAccessAllowedKey).listen((_) => _applyIsInstalledAppAccessAllowed()); + settingStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => _applyLocale()); // display - settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode()); - settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => applyMaxBrightness()); + settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => _applyDisplayRefreshRateMode()); + settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => _applyMaxBrightness()); settingStream.where((event) => event.key == SettingKeys.forceTvLayoutKey).listen((_) => applyForceTvLayout()); // navigation - settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => applyKeepScreenOn()); + settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => _applyKeepScreenOn()); // platform settings - settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); + settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => _applyIsRotationLocked()); - applyDisplayRefreshRateMode(); - applyMaxBrightness(); - applyKeepScreenOn(); - applyIsRotationLocked(); + _applyLocale(); + _applyDisplayRefreshRateMode(); + _applyMaxBrightness(); + _applyKeepScreenOn(); + _applyIsRotationLocked(); } Future _setupErrorReporting() async { @@ -632,3 +663,18 @@ class AvesScrollBehavior extends MaterialScrollBehavior { } typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData); + +class LocaleOverrides extends Equatable { + final Locale? countrifiedLocale; + + @override + List get props => [countrifiedLocale]; + + const LocaleOverrides({ + required this.countrifiedLocale, + }); + + static const LocaleOverrides none = LocaleOverrides( + countrifiedLocale: null, + ); +} diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 057052675..92814439e 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -672,8 +672,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge final newest = firstKey.date; final oldest = lastKey.date; if (newest != null && oldest != null) { - final localeName = context.l10n.localeName; - final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName); + final locale = context.l10n.localeName; + final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(locale) : DateFormat.MMM(locale); String? lastLabel; sectionLayouts.forEach((section) { final date = (section.sectionKey as EntryDateSectionKey).date; diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index fddbb33a9..38eaf8e60 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -1,3 +1,4 @@ +import 'package:aves/ref/locales.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -15,6 +16,8 @@ class _DebugCacheSectionState extends State with AutomaticKee Widget build(BuildContext context) { super.build(context); + final currentSizeBytes = formatFileSize(asciiLocale, imageCache.currentSizeBytes); + final maxSizeBytes = formatFileSize(asciiLocale, imageCache.maximumSizeBytes); return AvesExpansionTile( title: 'Cache', children: [ @@ -25,7 +28,7 @@ class _DebugCacheSectionState extends State with AutomaticKee Row( children: [ Expanded( - child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFileSize('en_US', imageCache.currentSizeBytes)}/${formatFileSize('en_US', imageCache.maximumSizeBytes)}'), + child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t$currentSizeBytes/$maxSizeBytes'), ), const SizedBox(width: 8), ElevatedButton( diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 2e2eb39b8..b8f6f5836 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -7,6 +7,7 @@ import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/video_playback.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -65,7 +66,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('DB file size: ${formatFileSize('en_US', snapshot.data!)}'), + child: Text('DB file size: ${formatFileSize(asciiLocale, snapshot.data!)}'), ), const SizedBox(width: 8), ElevatedButton( diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 7e242fd05..4b0c49e77 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -1,3 +1,4 @@ +import 'package:aves/ref/locales.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; @@ -47,7 +48,7 @@ class _DebugStorageSectionState extends State with Automati 'isPrimary': '${v.isPrimary}', 'isRemovable': '${v.isRemovable}', 'state': v.state, - if (freeSpace != null) 'freeSpace': formatFileSize('en_US', freeSpace), + if (freeSpace != null) 'freeSpace': formatFileSize(asciiLocale, freeSpace), }, ), ), diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index d1cc5d0db..61e7739a0 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -38,6 +38,7 @@ class RenameEntrySetPage extends StatefulWidget { class _RenameEntrySetPageState extends State { final TextEditingController _patternTextController = TextEditingController(); final ValueNotifier _namingPatternNotifier = ValueNotifier(const NamingPattern([])); + late final String locale; static const int previewMax = 10; static const double thumbnailExtent = 48; @@ -51,7 +52,11 @@ class _RenameEntrySetPageState extends State { super.initState(); _patternTextController.text = settings.entryRenamingPattern; _patternTextController.addListener(_onUserPatternChanged); - _onUserPatternChanged(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + locale = context.l10n.localeName; + _onUserPatternChanged(); + }); } @override @@ -229,6 +234,7 @@ class _RenameEntrySetPageState extends State { _namingPatternNotifier.value = NamingPattern.from( userPattern: _patternTextController.text, entryCount: entryCount, + locale: locale, ); } diff --git a/lib/widgets/settings/settings_mobile_page.dart b/lib/widgets/settings/settings_mobile_page.dart index c4ee713fd..8c29b5c10 100644 --- a/lib/widgets/settings/settings_mobile_page.dart +++ b/lib/widgets/settings/settings_mobile_page.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.dart'; @@ -117,7 +118,7 @@ class _SettingsMobilePageState extends State with FeedbackMi final allJsonString = jsonEncode(allMap); final success = await storageService.createFile( - 'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json', + 'aves-settings-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}.json', MimeTypes.json, Uint8List.fromList(utf8.encode(allJsonString)), ); diff --git a/test/model/naming_pattern_test.dart b/test/model/naming_pattern_test.dart index c7991e805..b5d0dafa0 100644 --- a/test/model/naming_pattern_test.dart +++ b/test/model/naming_pattern_test.dart @@ -1,13 +1,20 @@ import 'package:aves/model/naming_pattern.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() async { + await initializeDateFormatting(); + }); + test('mixed processors', () { const entryCount = 42; + const locale = 'en'; expect( NamingPattern.from( userPattern: 'pure literal', entryCount: entryCount, + locale: locale, ).processors, [ const LiteralNamingProcessor('pure literal'), @@ -17,10 +24,11 @@ void main() { NamingPattern.from( userPattern: 'prefixsuffix', entryCount: entryCount, + locale: locale, ).processors, [ const LiteralNamingProcessor('prefix'), - DateNamingProcessor('yyyy-MM-ddTHH:mm:ss'), + DateNamingProcessor('yyyy-MM-ddTHH:mm:ss', locale), const LiteralNamingProcessor('suffix'), ], ); @@ -28,9 +36,10 @@ void main() { NamingPattern.from( userPattern: ' ', entryCount: entryCount, + locale: locale, ).processors, [ - DateNamingProcessor('yyyy-MM-ddTHH:mm:ss'), + DateNamingProcessor('yyyy-MM-ddTHH:mm:ss', locale), const LiteralNamingProcessor(' '), const NameNamingProcessor(), ],