From 94659ae8af88afb74c383cf185679b6b216ffbc6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 1 Dec 2021 17:25:42 +0900 Subject: [PATCH] l10n: number format --- lib/model/metadata/overlay.dart | 32 ++++++------- lib/model/settings/coordinate_format.dart | 21 +++++++-- lib/model/video/metadata.dart | 16 ++----- lib/utils/file_utils.dart | 46 +++++-------------- lib/utils/geo_utils.dart | 34 +++++--------- .../collection/draggable_thumb_label.dart | 3 +- .../common/action_mixins/size_aware.dart | 10 ++-- lib/widgets/debug/cache.dart | 2 +- lib/widgets/debug/database.dart | 2 +- lib/widgets/debug/storage.dart | 2 +- lib/widgets/viewer/info/basic_section.dart | 2 +- lib/widgets/viewer/overlay/bottom/common.dart | 18 ++++++-- test/utils/file_utils_test.dart | 16 +++++++ test/utils/geo_utils_test.dart | 3 ++ 14 files changed, 105 insertions(+), 102 deletions(-) create mode 100644 test/utils/file_utils_test.dart diff --git a/lib/model/metadata/overlay.dart b/lib/model/metadata/overlay.dart index bebcb6e1c..9dc983af3 100644 --- a/lib/model/metadata/overlay.dart +++ b/lib/model/metadata/overlay.dart @@ -1,20 +1,23 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; -class OverlayMetadata { - final String? aperture, exposureTime, focalLength, iso; +@immutable +class OverlayMetadata extends Equatable { + final double? aperture, focalLength; + final String? exposureTime; + final int? iso; - static final apertureFormat = NumberFormat('0.0', 'en_US'); - static final focalLengthFormat = NumberFormat('0.#', 'en_US'); + @override + List get props => [aperture, exposureTime, focalLength, iso]; - OverlayMetadata({ - double? aperture, + bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null; + + const OverlayMetadata({ + this.aperture, this.exposureTime, - double? focalLength, - int? iso, - }) : aperture = aperture != null ? 'ƒ/${apertureFormat.format(aperture)}' : null, - focalLength = focalLength != null ? '${focalLengthFormat.format(focalLength)} mm' : null, - iso = iso != null ? 'ISO$iso' : null; + this.focalLength, + this.iso, + }); factory OverlayMetadata.fromMap(Map map) { return OverlayMetadata( @@ -24,9 +27,4 @@ class OverlayMetadata { iso: map['iso'] as int?, ); } - - bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null; - - @override - String toString() => '$runtimeType#${shortHash(this)}{aperture=$aperture, exposureTime=$exposureTime, focalLength=$focalLength, iso=$iso}'; } diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/coordinate_format.dart index 5a65e6818..4a47c50ae 100644 --- a/lib/model/settings/coordinate_format.dart +++ b/lib/model/settings/coordinate_format.dart @@ -2,6 +2,7 @@ import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'enums.dart'; @@ -16,24 +17,36 @@ extension ExtraCoordinateFormat on CoordinateFormat { } } + static const _separator = ', '; + String format(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int dmsSecondDecimals = 2}) { switch (this) { case CoordinateFormat.dms: - return toDMS(l10n, latLng, minuteSecondPadding: minuteSecondPadding, secondDecimals: dmsSecondDecimals).join(', '); + return toDMS(l10n, latLng, minuteSecondPadding: minuteSecondPadding, secondDecimals: dmsSecondDecimals).join(_separator); case CoordinateFormat.decimal: - return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', '); + return _toDecimal(l10n, latLng).join(_separator); } } // returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E'] static List toDMS(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int secondDecimals = 2}) { + final locale = l10n.localeName; final lat = latLng.latitude; final lng = latLng.longitude; - final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals); - final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals); + final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); + final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); return [ l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth), l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast), ]; } + + static List _toDecimal(AppLocalizations l10n, LatLng latLng) { + final locale = l10n.localeName; + final formatter = NumberFormat('0.000000°', locale); + return [ + formatter.format(latLng.latitude), + formatter.format(latLng.longitude), + ]; + } } diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index c9729fcb8..be38b825d 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -374,7 +374,7 @@ class VideoMetadataFormatter { static String _formatFilesize(String value) { final size = int.tryParse(value); - return size != null ? formatFilesize(size) : value; + return size != null ? formatFileSize('en_US', size) : value; } static String _formatLanguage(String value) { @@ -399,20 +399,10 @@ class VideoMetadataFormatter { if (parsed == null) return size; size = parsed; } + const divider = 1000; - if (size < divider) return '$size $unit'; - - if (size < divider * divider && size % divider == 0) { - return '${(size / divider).toStringAsFixed(0)} K$unit'; - } - if (size < divider * divider) { - return '${(size / divider).toStringAsFixed(round)} K$unit'; - } - - if (size < divider * divider * divider && size % divider == 0) { - return '${(size / (divider * divider)).toStringAsFixed(0)} M$unit'; - } + if (size < divider * divider) return '${(size / divider).toStringAsFixed(round)} K$unit'; return '${(size / divider / divider).toStringAsFixed(round)} M$unit'; } } diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart index 50e1319df..ca707920f 100644 --- a/lib/utils/file_utils.dart +++ b/lib/utils/file_utils.dart @@ -1,38 +1,16 @@ -String formatFilesize(int size, {int round = 2}) { - var divider = 1024; +import 'package:intl/intl.dart'; - if (size < divider) return '$size B'; +const _kiloDivider = 1024; +const _megaDivider = _kiloDivider * _kiloDivider; +const _gigaDivider = _megaDivider * _kiloDivider; +const _teraDivider = _gigaDivider * _kiloDivider; - if (size < divider * divider && size % divider == 0) { - return '${(size / divider).toStringAsFixed(0)} KB'; - } - if (size < divider * divider) { - return '${(size / divider).toStringAsFixed(round)} KB'; - } +String formatFileSize(String locale, int size, {int round = 2}) { + if (size < _kiloDivider) return '$size B'; - if (size < divider * divider * divider && size % divider == 0) { - return '${(size / (divider * divider)).toStringAsFixed(0)} MB'; - } - if (size < divider * divider * divider) { - return '${(size / divider / divider).toStringAsFixed(round)} MB'; - } - - if (size < divider * divider * divider * divider && size % divider == 0) { - return '${(size / (divider * divider * divider)).toStringAsFixed(0)} GB'; - } - if (size < divider * divider * divider * divider) { - return '${(size / divider / divider / divider).toStringAsFixed(round)} GB'; - } - - if (size < divider * divider * divider * divider * divider && size % divider == 0) { - return '${(size / divider / divider / divider / divider).toStringAsFixed(0)} TB'; - } - if (size < divider * divider * divider * divider * divider) { - return '${(size / divider / divider / divider / divider).toStringAsFixed(round)} TB'; - } - - if (size < divider * divider * divider * divider * divider * divider && size % divider == 0) { - return '${(size / divider / divider / divider / divider / divider).toStringAsFixed(0)} PB'; - } - return '${(size / divider / divider / divider / divider / divider).toStringAsFixed(round)} PB'; + final formatter = NumberFormat('0${round > 0 ? '.${'0' * round}' : ''}', locale); + if (size < _megaDivider) return '${formatter.format(size / _kiloDivider)} KB'; + if (size < _gigaDivider) return '${formatter.format(size / _megaDivider)} MB'; + if (size < _teraDivider) return '${formatter.format(size / _gigaDivider)} GB'; + return '${formatter.format(size / _teraDivider)} TB'; } diff --git a/lib/utils/geo_utils.dart b/lib/utils/geo_utils.dart index 7316abf5f..f8ca04e33 100644 --- a/lib/utils/geo_utils.dart +++ b/lib/utils/geo_utils.dart @@ -1,33 +1,23 @@ import 'dart:math'; -import 'package:aves/utils/math_utils.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; class GeoUtils { - static String decimal2sexagesimal(final double degDecimal, final bool minuteSecondPadding, final int secondDecimals) { - List _split(final double value) { - // NumberFormat is necessary to create digit after comma if the value - // has no decimal point (only necessary for browser) - final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.'); - return [ - int.parse(tmp[0]).abs(), - int.parse(tmp[1]), - ]; - } - - final deg = _split(degDecimal)[0]; - final minDecimal = (degDecimal.abs() - deg) * 60; - final min = _split(minDecimal)[0]; + static String decimal2sexagesimal( + double degDecimal, + bool minuteSecondPadding, + int secondDecimals, + String locale, + ) { + final degAbs = degDecimal.abs(); + final deg = degAbs.toInt(); + final minDecimal = (degAbs - deg) * 60; + final min = minDecimal.toInt(); final sec = (minDecimal - min) * 60; - final secRounded = roundToPrecision(sec, decimals: secondDecimals); - var minText = '$min'; - var secText = secRounded.toStringAsFixed(secondDecimals); - if (minuteSecondPadding) { - minText = minText.padLeft(2, '0'); - secText = secText.padLeft(secondDecimals > 0 ? 3 + secondDecimals : 2, '0'); - } + var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); + var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); return '$deg° $minText′ $secText″'; } diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 089ddf604..b96b7e599 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -3,6 +3,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/material.dart'; @@ -48,7 +49,7 @@ class CollectionDraggableThumbLabel extends StatelessWidget { ]; case EntrySortFactor.size: return [ - if (entry.sizeBytes != null) formatFilesize(entry.sizeBytes!, round: 0), + if (entry.sizeBytes != null) formatFileSize(context.l10n.localeName, entry.sizeBytes!, round: 0), ]; } }, diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index e3793e77b..03c4eb7f3 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -75,13 +75,15 @@ mixin SizeAwareMixin { await showDialog( context: context, builder: (context) { - final neededSize = formatFilesize(needed); - final freeSize = formatFilesize(free); + final l10n = context.l10n; + final locale = l10n.localeName; + final neededSize = formatFileSize(locale, needed); + final freeSize = formatFileSize(locale, free); final volume = destinationVolume.getDescription(context); return AvesDialog( context: context, - title: context.l10n.notEnoughSpaceDialogTitle, - content: Text(context.l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), + title: l10n.notEnoughSpaceDialogTitle, + content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index 90a08dd43..9dca26878 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -25,7 +25,7 @@ class _DebugCacheSectionState extends State with AutomaticKee Row( children: [ Expanded( - child: Text('Image cache:\n\t${imageCache!.currentSize}/${imageCache!.maximumSize} items\n\t${formatFilesize(imageCache!.currentSizeBytes)}/${formatFilesize(imageCache!.maximumSizeBytes)}'), + child: Text('Image cache:\n\t${imageCache!.currentSize}/${imageCache!.maximumSize} items\n\t${formatFileSize('en_US', imageCache!.currentSizeBytes)}/${formatFileSize('en_US', imageCache!.maximumSizeBytes)}'), ), const SizedBox(width: 8), ElevatedButton( diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 12c0c973d..d734ac5d5 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -53,7 +53,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('DB file size: ${formatFilesize(snapshot.data!)}'), + child: Text('DB file size: ${formatFileSize('en_US', snapshot.data!)}'), ), const SizedBox(width: 8), ElevatedButton( diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index da2f29f55..2dccf852d 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -46,7 +46,7 @@ class _DebugStorageSectionState extends State with Automati 'isPrimary': '${v.isPrimary}', 'isRemovable': '${v.isRemovable}', 'state': v.state, - if (freeSpace != null) 'freeSpace': formatFilesize(freeSpace), + if (freeSpace != null) 'freeSpace': formatFileSize('en_US', freeSpace), }, ), ), diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 5d7aeb60b..677b1f759 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -60,7 +60,7 @@ class BasicSection extends StatelessWidget { final date = entry.bestDate; final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown; final showResolution = !entry.isSvg && entry.isSized; - final sizeText = entry.sizeBytes != null ? formatFilesize(entry.sizeBytes!) : infoUnknown; + final sizeText = entry.sizeBytes != null ? formatFileSize(locale, entry.sizeBytes!) : infoUnknown; final path = entry.path; return Column( diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index fa9ec0861..511a52a2f 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -20,6 +20,7 @@ import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -423,14 +424,25 @@ class _ShootingRow extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = context.l10n.localeName; + + final aperture = details.aperture; + final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; + + final focalLength = details.focalLength; + final focalLengthText = focalLength != null ? '${NumberFormat('0.#', locale).format(focalLength)} mm' : Constants.overlayUnknown; + + final iso = details.iso; + final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; + return Row( children: [ const DecoratedIcon(AIcons.shooting, shadows: Constants.embossShadows, size: _iconSize), const SizedBox(width: _iconPadding), - Expanded(child: Text(details.aperture ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(details.focalLength ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(details.iso ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(focalLengthText, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(isoText, strutStyle: Constants.overflowStrutStyle)), ], ); } diff --git a/test/utils/file_utils_test.dart b/test/utils/file_utils_test.dart new file mode 100644 index 000000000..1927bfc48 --- /dev/null +++ b/test/utils/file_utils_test.dart @@ -0,0 +1,16 @@ +import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/utils/file_utils.dart'; +import 'package:aves/utils/geo_utils.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:test/test.dart'; + +void main() { + test('format file size', () { + final l10n = lookupAppLocalizations(AppLocalizations.supportedLocales.first); + final locale = l10n.localeName; + expect(formatFileSize(locale, 1024), '1.00 KB'); + expect(formatFileSize(locale, 1536), '1.50 KB'); + expect(formatFileSize(locale, 1073741824), '1.00 GB'); + }); +} diff --git a/test/utils/geo_utils_test.dart b/test/utils/geo_utils_test.dart index 2c3a87eb7..5f9ec8dac 100644 --- a/test/utils/geo_utils_test.dart +++ b/test/utils/geo_utils_test.dart @@ -12,6 +12,9 @@ void main() { expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(0, 0)), ['0° 0′ 0.00″ N', '0° 0′ 0.00″ E']); + expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(0, 0), minuteSecondPadding: true), ['0° 00′ 00.00″ N', '0° 00′ 00.00″ E']); + expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(0, 0), secondDecimals: 0), ['0° 0′ 0″ N', '0° 0′ 0″ E']); + expect(ExtraCoordinateFormat.toDMS(l10n, LatLng(0, 0), secondDecimals: 4), ['0° 0′ 0.0000″ N', '0° 0′ 0.0000″ E']); }); test('bounds center', () {