diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart index 1b231ea59..b231b5616 100644 --- a/lib/model/entry/extensions/props.dart +++ b/lib/model/entry/extensions/props.dart @@ -11,6 +11,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/text.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:intl/intl.dart'; extension ExtraAvesEntryProps on AvesEntry { bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0; @@ -51,9 +52,10 @@ extension ExtraAvesEntryProps on AvesEntry { // text - String get resolutionText { - final ws = width; - final hs = height; + String getResolutionText(String locale) { + final numberFormat = NumberFormat('0', locale); + final ws = numberFormat.format(width); + final hs = numberFormat.format(height); return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs'; } diff --git a/lib/model/settings/enums/coordinate_format.dart b/lib/model/settings/enums/coordinate_format.dart index ca5835744..cf90b4936 100644 --- a/lib/model/settings/enums/coordinate_format.dart +++ b/lib/model/settings/enums/coordinate_format.dart @@ -47,10 +47,11 @@ extension ExtraCoordinateFormat on CoordinateFormat { final min = minDecimal.toInt(); final sec = (minDecimal - min) * 60; - var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); - var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); + final degText = NumberFormat('0', locale).format(deg); + final minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); + final secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); - return '$deg° $minText′ $secText″'; + return '$degText° $minText′ $secText″'; } static List _toDecimal(AppLocalizations l10n, LatLng latLng) { diff --git a/lib/ref/locales.dart b/lib/ref/locales.dart index 1ce72cdd1..fc58e5c11 100644 --- a/lib/ref/locales.dart +++ b/lib/ref/locales.dart @@ -45,3 +45,13 @@ bool shouldUseNativeDigits(Locale? countrifiedLocale) { return true; } } + +bool canHaveLetterSpacing(String locale) { + switch (locale) { + case 'ar': + case 'fa': + return false; + default: + return true; + } +} diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 6a425e67c..8e7c2fb1c 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -1,5 +1,5 @@ - import 'package:aves/model/device.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/about/policy_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; @@ -10,13 +10,6 @@ import 'package:flutter/material.dart'; class AppReference extends StatelessWidget { static const avesGithub = 'https://github.com/deckerst/aves'; - static const _appTitleStyle = TextStyle( - fontSize: 20, - fontWeight: FontWeight.normal, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], - ); - const AppReference({super.key}); @override @@ -38,27 +31,35 @@ class AppReference extends StatelessWidget { } Widget _buildAvesLine(BuildContext context) { + final locale = context.l10n.localeName; final textScaler = MediaQuery.textScalerOf(context); return Row( mainAxisSize: MainAxisSize.min, children: [ AvesLogo( - size: textScaler.scale(_appTitleStyle.fontSize!) * 1.3, + size: textScaler.scale(_getAppTitleStyle(locale).fontSize!) * 1.3, ), const SizedBox(width: 8), Text( context.l10n.appName, - style: _appTitleStyle, + style: _getAppTitleStyle(locale), ), const SizedBox(width: 8), Text( device.packageVersion, - style: _appTitleStyle, + style: _getAppTitleStyle(locale), ), ], ); } + TextStyle _getAppTitleStyle(String locale) => TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + letterSpacing: canHaveLetterSpacing(locale) ? 1 : 0, + fontFeatures: const [FontFeature.enable('smcp')], + ); + static List buildLinks(BuildContext context) { final l10n = context.l10n; return [ diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 68802ae8d..bda875f29 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -122,6 +122,9 @@ class _BugReportState extends State with FeedbackMixin { } Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) { + final locale = context.l10n.localeName; + final numberFormat = NumberFormat.decimalPattern(locale); + final isMonochrome = settings.themeColorMode == AvesThemeColorMode.monochrome; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), @@ -136,7 +139,7 @@ class _BugReportState extends State with FeedbackMixin { )), shape: BoxShape.circle, ), - child: Text('$step'), + child: Text(numberFormat.format(step)), ), const SizedBox(width: 8), Expanded(child: Text(text)), diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 90495cb1f..a478780b9 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -218,6 +218,9 @@ class _ReportOverlayState extends State> with SingleTickerPr @override Widget build(BuildContext context) { + final locale = context.l10n.localeName; + final percentFormat = NumberFormat.percentPattern(locale); + final theme = Theme.of(context); final colorScheme = theme.colorScheme; final progressColor = colorScheme.primary; @@ -230,7 +233,6 @@ class _ReportOverlayState extends State> with SingleTickerPr final processedCount = processed.length.toDouble(); final total = widget.itemCount; final percent = total == null || total == 0 ? 0.0 : min(1.0, processedCount / total); - final percentFormat = NumberFormat.percentPattern(); return FadeTransition( opacity: _animation, child: Stack( @@ -351,6 +353,9 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro @override Widget build(BuildContext context) { + final locale = context.l10n.localeName; + final numberFormat = NumberFormat('0', locale); + final textScaler = MediaQuery.textScalerOf(context); final theme = Theme.of(context); final colorScheme = theme.colorScheme; @@ -388,7 +393,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro // because we cannot use the app context theme here foreground: widget.progressColor, center: ChangeHighlightText( - '${(remainingDurationMillis / 1000).ceil()}', + numberFormat.format((remainingDurationMillis / 1000).ceil()), style: contentTextStyle.copyWith( shadows: [ Shadow( diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index 76c1fa4c9..53ad36e0a 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -1,8 +1,9 @@ - import 'package:aves/model/settings/settings.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/theme.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; import 'package:aves_model/aves_model.dart'; @@ -40,7 +41,7 @@ class HighlightTitle extends StatelessWidget { final style = TextStyle( shadows: shadows(context), fontSize: fontSize, - letterSpacing: 1.0, + letterSpacing: canHaveLetterSpacing(context.l10n.localeName) ? 1 : 0, fontFeatures: const [FontFeature.enable('smcp')], ); diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index d59476297..ee9bec0aa 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -146,9 +146,11 @@ class _GeoMapState extends State { selector: (context, s) => s.mapStyle, builder: (context, mapStyle, child) { final isHeavy = ExtraEntryMapStyle.isHeavy(mapStyle); + final locale = context.l10n.localeName; Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( key: key, count: key.count, + locale: locale, buildThumbnailImage: (extent) => ThumbnailImage( entry: key.entry, extent: extent, @@ -482,9 +484,11 @@ class _GeoMapState extends State { } else { markerEntry = geoEntry.entry!; } + final locale = context.l10n.localeName; final markerLocation = LatLng(geoEntry.latitude!, geoEntry.longitude!); Widget markerBuilder(BuildContext context) => ImageMarker( count: geoEntry.pointsSize, + locale: locale, drawArrow: false, buildThumbnailImage: (extent) => ThumbnailImage( entry: markerEntry, diff --git a/lib/widgets/navigation/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart index 7a0978786..c04705690 100644 --- a/lib/widgets/navigation/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -1,4 +1,3 @@ - import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/settings/settings.dart'; @@ -8,6 +7,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/location/country.dart'; import 'package:aves/model/source/location/place.dart'; import 'package:aves/model/source/tag.dart'; +import 'package:aves/ref/locales.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -111,6 +111,9 @@ class _AppDrawerState extends State { } Widget _buildHeader(BuildContext context) { + final l10n = context.l10n; + final locale = l10n.localeName; + Future goTo(String routeName, WidgetBuilder pageBuilder) async { Navigator.maybeOf(context)?.pop(); await Future.delayed(ADurations.drawerTransitionAnimation); @@ -145,13 +148,13 @@ class _AppDrawerState extends State { OutlinedText( textSpans: [ TextSpan( - text: context.l10n.appName, - style: const TextStyle( + text: l10n.appName, + style: TextStyle( color: Colors.white, fontSize: 38, fontWeight: FontWeight.w300, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], + letterSpacing: canHaveLetterSpacing(locale) ? 1 : 0, + fontFeatures: const [FontFeature.enable('smcp')], ), ), ], @@ -177,7 +180,7 @@ class _AppDrawerState extends State { onPressed: () => goTo(AboutPage.routeName, (_) => const AboutPage()), style: drawerButtonStyle, icon: const Icon(AIcons.info), - label: Text(context.l10n.drawerAboutButton), + label: Text(l10n.drawerAboutButton), ), OutlinedButton.icon( // key is expected by test driver @@ -185,7 +188,7 @@ class _AppDrawerState extends State { onPressed: () => goTo(SettingsPage.routeName, (_) => const SettingsPage()), style: drawerButtonStyle, icon: const Icon(AIcons.settings), - label: Text(context.l10n.drawerSettingsButton), + label: Text(l10n.drawerSettingsButton), ), ], ), diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index ff4c458f9..ee75292a1 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -6,6 +6,7 @@ import 'package:aves/model/settings/enums/home_page.dart'; 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/ref/locales.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -90,6 +91,9 @@ class _TvRailState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; + final locale = l10n.localeName; + final navEntries = _getNavEntries(context); return DirectionalSafeArea( end: false, @@ -103,13 +107,13 @@ class _TvRailState extends State { logo, const SizedBox(width: 16), Text( - context.l10n.appName, - style: const TextStyle( + l10n.appName, + style: TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.w300, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], + letterSpacing: canHaveLetterSpacing(locale) ? 1 : 0, + fontFeatures: const [FontFeature.enable('smcp')], ), ), ], diff --git a/lib/widgets/stats/percent_text.dart b/lib/widgets/stats/percent_text.dart index 362194d12..7bc62038c 100644 --- a/lib/widgets/stats/percent_text.dart +++ b/lib/widgets/stats/percent_text.dart @@ -1,21 +1,24 @@ import 'package:aves/theme/styles.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/theme.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class LinearPercentIndicatorText extends StatelessWidget { final double percent; - final percentFormat = NumberFormat.percentPattern(); - LinearPercentIndicatorText({ + const LinearPercentIndicatorText({ super.key, required this.percent, }); @override Widget build(BuildContext context) { + final locale = context.l10n.localeName; + final percentFormat = NumberFormat.percentPattern(locale); + return OutlinedText( textSpans: [ TextSpan( diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 89b085091..123891051 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -32,6 +32,7 @@ import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class BasicSection extends StatefulWidget { @@ -277,15 +278,6 @@ class _BasicInfoState extends State<_BasicInfo> { AvesEntry get entry => widget.entry; - int get megaPixels => (entry.width * entry.height / 1000000).round(); - - // guess whether this is a photo, according to file type - bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(entry.mimeType) || entry.isRaw; - - bool get showMegaPixels => isPhoto && megaPixels > 0; - - String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; - static const ownerPackageNamePropKey = 'owner_package_name'; static const iconSize = 20.0; @@ -331,7 +323,7 @@ class _BasicInfoState extends State<_BasicInfo> { l10n.viewerInfoLabelTitle: title, l10n.viewerInfoLabelDate: dateText, if (entry.isVideo) ..._buildVideoRows(context), - if (showResolution) l10n.viewerInfoLabelResolution: context.applyDirectionality(rasterResolutionText), + if (showResolution) l10n.viewerInfoLabelResolution: context.applyDirectionality(getRasterResolutionText(locale)), l10n.viewerInfoLabelSize: context.applyDirectionality(sizeText), if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri, if (path != null) l10n.viewerInfoLabelPath: path, @@ -384,4 +376,20 @@ class _BasicInfoState extends State<_BasicInfo> { ), ]; } + + String getRasterResolutionText(String locale) { + var s = entry.getResolutionText(locale); + + // guess whether this is a photo, according to file type + final isPhoto = [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(entry.mimeType) || entry.isRaw; + if (isPhoto) { + final numberFormat = NumberFormat('0', locale); + final megaPixels = (entry.width * entry.height / 1000000).round(); + if (megaPixels > 0) { + s += ' • ${numberFormat.format(megaPixels)} MP'; + } + } + + return s; + } } diff --git a/lib/widgets/viewer/info/color_section.dart b/lib/widgets/viewer/info/color_section.dart index ca8d669e2..087e15f3e 100644 --- a/lib/widgets/viewer/info/color_section.dart +++ b/lib/widgets/viewer/info/color_section.dart @@ -69,9 +69,12 @@ class _ColorSectionSliverState extends State { children: [ ColorIndicator(value: v.color), const SizedBox(width: 8), - SelectableText( - '#${v.color.hex}', - style: const TextStyle(fontFamily: 'monospace'), + Directionality( + textDirection: TextDirection.ltr, + child: SelectableText( + '#${v.color.hex}', + style: const TextStyle(fontFamily: 'monospace'), + ), ), ], ), diff --git a/lib/widgets/viewer/overlay/details/date.dart b/lib/widgets/viewer/overlay/details/date.dart index 480ce4c2e..af2425fc5 100644 --- a/lib/widgets/viewer/overlay/details/date.dart +++ b/lib/widgets/viewer/overlay/details/date.dart @@ -29,7 +29,7 @@ class OverlayDateRow extends StatelessWidget { final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.isSized - ? entry.resolutionText + ? entry.getResolutionText(locale) : ''; return Row( diff --git a/plugins/aves_map/lib/src/marker/image.dart b/plugins/aves_map/lib/src/marker/image.dart index 7b7e0fe9c..8ead5d7a2 100644 --- a/plugins/aves_map/lib/src/marker/image.dart +++ b/plugins/aves_map/lib/src/marker/image.dart @@ -4,10 +4,12 @@ import 'package:collection/collection.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:intl/intl.dart' as intl; import 'package:latlong2/latlong.dart'; class ImageMarker extends StatelessWidget { final int? count; + final intl.NumberFormat numberFormat; final bool drawArrow; final Widget Function(double extent) buildThumbnailImage; @@ -20,12 +22,13 @@ class ImageMarker extends StatelessWidget { static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); static const innerBorderRadius = BorderRadius.all(innerRadius); - const ImageMarker({ + ImageMarker({ super.key, required this.count, + required String locale, this.drawArrow = true, required this.buildThumbnailImage, - }); + }) : numberFormat = intl.NumberFormat.decimalPattern(locale); @override Widget build(BuildContext context) { @@ -107,7 +110,7 @@ class ImageMarker extends StatelessWidget { ), ), child: Text( - '$count', + numberFormat.format(count), style: TextStyle( fontSize: 12, color: theme.colorScheme.onPrimary, diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index 1ebdf7fbc..798d09d32 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -102,7 +102,7 @@ packages: source: hosted version: "4.0.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml index e817211e7..906ea0919 100644 --- a/plugins/aves_map/pubspec.yaml +++ b/plugins/aves_map/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: equatable: fluster: flutter_map: + intl: latlong2: provider: