l10n: number format

This commit is contained in:
Thibault Deckers 2021-12-01 17:25:42 +09:00
parent 2628192e06
commit 94659ae8af
14 changed files with 105 additions and 102 deletions

View file

@ -1,20 +1,23 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
class OverlayMetadata { @immutable
final String? aperture, exposureTime, focalLength, iso; class OverlayMetadata extends Equatable {
final double? aperture, focalLength;
final String? exposureTime;
final int? iso;
static final apertureFormat = NumberFormat('0.0', 'en_US'); @override
static final focalLengthFormat = NumberFormat('0.#', 'en_US'); List<Object?> get props => [aperture, exposureTime, focalLength, iso];
OverlayMetadata({ bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null;
double? aperture,
const OverlayMetadata({
this.aperture,
this.exposureTime, this.exposureTime,
double? focalLength, this.focalLength,
int? iso, this.iso,
}) : aperture = aperture != null ? 'ƒ/${apertureFormat.format(aperture)}' : null, });
focalLength = focalLength != null ? '${focalLengthFormat.format(focalLength)} mm' : null,
iso = iso != null ? 'ISO$iso' : null;
factory OverlayMetadata.fromMap(Map map) { factory OverlayMetadata.fromMap(Map map) {
return OverlayMetadata( return OverlayMetadata(
@ -24,9 +27,4 @@ class OverlayMetadata {
iso: map['iso'] as int?, 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}';
} }

View file

@ -2,6 +2,7 @@ import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'enums.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}) { String format(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int dmsSecondDecimals = 2}) {
switch (this) { switch (this) {
case CoordinateFormat.dms: 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: 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'] // returns coordinates formatted as DMS, e.g. ['41° 24 12.2″ N', '2° 10 26.5″ E']
static List<String> toDMS(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int secondDecimals = 2}) { static List<String> toDMS(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int secondDecimals = 2}) {
final locale = l10n.localeName;
final lat = latLng.latitude; final lat = latLng.latitude;
final lng = latLng.longitude; final lng = latLng.longitude;
final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals); final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale);
final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals); final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale);
return [ return [
l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth), l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth),
l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast), l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast),
]; ];
} }
static List<String> _toDecimal(AppLocalizations l10n, LatLng latLng) {
final locale = l10n.localeName;
final formatter = NumberFormat('0.000000°', locale);
return [
formatter.format(latLng.latitude),
formatter.format(latLng.longitude),
];
}
} }

View file

@ -374,7 +374,7 @@ class VideoMetadataFormatter {
static String _formatFilesize(String value) { static String _formatFilesize(String value) {
final size = int.tryParse(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) { static String _formatLanguage(String value) {
@ -399,20 +399,10 @@ class VideoMetadataFormatter {
if (parsed == null) return size; if (parsed == null) return size;
size = parsed; size = parsed;
} }
const divider = 1000; const divider = 1000;
if (size < divider) return '$size $unit'; if (size < divider) return '$size $unit';
if (size < divider * divider) return '${(size / divider).toStringAsFixed(round)} K$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';
}
return '${(size / divider / divider).toStringAsFixed(round)} M$unit'; return '${(size / divider / divider).toStringAsFixed(round)} M$unit';
} }
} }

View file

@ -1,38 +1,16 @@
String formatFilesize(int size, {int round = 2}) { import 'package:intl/intl.dart';
var divider = 1024;
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) { String formatFileSize(String locale, int size, {int round = 2}) {
return '${(size / divider).toStringAsFixed(0)} KB'; if (size < _kiloDivider) return '$size B';
}
if (size < divider * divider) {
return '${(size / divider).toStringAsFixed(round)} KB';
}
if (size < divider * divider * divider && size % divider == 0) { final formatter = NumberFormat('0${round > 0 ? '.${'0' * round}' : ''}', locale);
return '${(size / (divider * divider)).toStringAsFixed(0)} MB'; if (size < _megaDivider) return '${formatter.format(size / _kiloDivider)} KB';
} if (size < _gigaDivider) return '${formatter.format(size / _megaDivider)} MB';
if (size < divider * divider * divider) { if (size < _teraDivider) return '${formatter.format(size / _gigaDivider)} GB';
return '${(size / divider / divider).toStringAsFixed(round)} MB'; return '${formatter.format(size / _teraDivider)} TB';
}
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';
} }

View file

@ -1,33 +1,23 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/utils/math_utils.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class GeoUtils { class GeoUtils {
static String decimal2sexagesimal(final double degDecimal, final bool minuteSecondPadding, final int secondDecimals) { static String decimal2sexagesimal(
List<int> _split(final double value) { double degDecimal,
// NumberFormat is necessary to create digit after comma if the value bool minuteSecondPadding,
// has no decimal point (only necessary for browser) int secondDecimals,
final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.'); String locale,
return <int>[ ) {
int.parse(tmp[0]).abs(), final degAbs = degDecimal.abs();
int.parse(tmp[1]), final deg = degAbs.toInt();
]; final minDecimal = (degAbs - deg) * 60;
} final min = minDecimal.toInt();
final deg = _split(degDecimal)[0];
final minDecimal = (degDecimal.abs() - deg) * 60;
final min = _split(minDecimal)[0];
final sec = (minDecimal - min) * 60; final sec = (minDecimal - min) * 60;
final secRounded = roundToPrecision(sec, decimals: secondDecimals); var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min);
var minText = '$min'; var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec);
var secText = secRounded.toStringAsFixed(secondDecimals);
if (minuteSecondPadding) {
minText = minText.padLeft(2, '0');
secText = secText.padLeft(secondDecimals > 0 ? 3 + secondDecimals : 2, '0');
}
return '$deg° $minText $secText'; return '$deg° $minText $secText';
} }

View file

@ -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/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/utils/file_utils.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/draggable_thumb_label.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -48,7 +49,7 @@ class CollectionDraggableThumbLabel extends StatelessWidget {
]; ];
case EntrySortFactor.size: case EntrySortFactor.size:
return [ return [
if (entry.sizeBytes != null) formatFilesize(entry.sizeBytes!, round: 0), if (entry.sizeBytes != null) formatFileSize(context.l10n.localeName, entry.sizeBytes!, round: 0),
]; ];
} }
}, },

View file

@ -75,13 +75,15 @@ mixin SizeAwareMixin {
await showDialog( await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
final neededSize = formatFilesize(needed); final l10n = context.l10n;
final freeSize = formatFilesize(free); final locale = l10n.localeName;
final neededSize = formatFileSize(locale, needed);
final freeSize = formatFileSize(locale, free);
final volume = destinationVolume.getDescription(context); final volume = destinationVolume.getDescription(context);
return AvesDialog( return AvesDialog(
context: context, context: context,
title: context.l10n.notEnoughSpaceDialogTitle, title: l10n.notEnoughSpaceDialogTitle,
content: Text(context.l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),

View file

@ -25,7 +25,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
Row( Row(
children: [ children: [
Expanded( 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), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(

View file

@ -53,7 +53,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: Text('DB file size: ${formatFilesize(snapshot.data!)}'), child: Text('DB file size: ${formatFileSize('en_US', snapshot.data!)}'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(

View file

@ -46,7 +46,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
'isPrimary': '${v.isPrimary}', 'isPrimary': '${v.isPrimary}',
'isRemovable': '${v.isRemovable}', 'isRemovable': '${v.isRemovable}',
'state': v.state, 'state': v.state,
if (freeSpace != null) 'freeSpace': formatFilesize(freeSpace), if (freeSpace != null) 'freeSpace': formatFileSize('en_US', freeSpace),
}, },
), ),
), ),

View file

@ -60,7 +60,7 @@ class BasicSection extends StatelessWidget {
final date = entry.bestDate; final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown; final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown;
final showResolution = !entry.isSvg && entry.isSized; 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; final path = entry.path;
return Column( return Column(

View file

@ -20,6 +20,7 @@ import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -423,14 +424,25 @@ class _ShootingRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( return Row(
children: [ children: [
const DecoratedIcon(AIcons.shooting, shadows: Constants.embossShadows, size: _iconSize), const DecoratedIcon(AIcons.shooting, shadows: Constants.embossShadows, size: _iconSize),
const SizedBox(width: _iconPadding), 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.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)),
Expanded(child: Text(details.focalLength ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(focalLengthText, strutStyle: Constants.overflowStrutStyle)),
Expanded(child: Text(details.iso ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(isoText, strutStyle: Constants.overflowStrutStyle)),
], ],
); );
} }

View file

@ -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');
});
}

View file

@ -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(-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(-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)), ['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', () { test('bounds center', () {