Feature: implement advanced number and date formatting

This commit is contained in:
Florian Werner 2024-04-05 17:20:01 +00:00 committed by The one with the braid
parent 0b994f6f9b
commit 2666eccf3e
4 changed files with 205 additions and 67 deletions

View file

@ -1,7 +1,7 @@
import 'package:intl/locale.dart'; import 'package:intl/locale.dart';
import 'package:pkpass/pkpass.dart'; import 'package:pkpass/pkpass.dart';
import 'package:pkpass/pkpass/utils/mabe_decode.dart'; import 'package:pkpass/pkpass/utils/maybe_decode.dart';
/// Information that is required for all passes. /// Information that is required for all passes.
class PassMetadata { class PassMetadata {

View file

@ -1,7 +1,8 @@
import 'package:intl/intl.dart';
import 'package:intl/locale.dart'; import 'package:intl/locale.dart';
import 'package:pkpass/pkpass.dart'; import 'package:pkpass/pkpass.dart';
import 'package:pkpass/pkpass/utils/mabe_decode.dart'; import 'package:pkpass/pkpass/utils/maybe_decode.dart';
/// Keys that define the structure of the pass. /// Keys that define the structure of the pass.
/// ///
@ -70,6 +71,15 @@ class DictionaryField {
/// Value of the field, for example, 42. /// Value of the field, for example, 42.
final DictionaryValue value; final DictionaryValue value;
/// Attributed value of the field.
///
/// The value may contain HTML markup for links. Only the <a> tag and its href attribute are supported. For example, the following is key-value pair specifies a link with the text Edit my profile:
///
/// "attributedValue": "<a href='http://example.com/customers/123'>Edit my profile</a>"
///
/// This keys value overrides the text specified by the value key.
final DictionaryValue? attributedValue;
/// Label text for the field. /// Label text for the field.
final String? label; final String? label;
@ -81,35 +91,50 @@ class DictionaryField {
/// Alignment for the fields contents. /// Alignment for the fields contents.
final PassTextAlign? textAlignment; final PassTextAlign? textAlignment;
/// Attributed value of the field. /// Number Style for a numeric value.
/// final PassTextNumberStyle? numberStyle;
/// The value may contain HTML markup for links. Only the <a> tag and its href attribute are supported. For example, the following is key-value pair specifies a link with the text Edit my profile:
/// /// ISO 4217 Currency Code for a numeric value.
/// "attributedValue": "<a href='http://example.com/customers/123'>Edit my profile</a>" final String? currencyCode;
///
/// This keys value overrides the text specified by the value key. /// Date and Time Style for a Date/Time value.
final DictionaryValue? attributedValue; final PassTextDateStyle? dateStyle;
final PassTextDateStyle? timeStyle;
/// Wether to display the Date/Time value in the device timezone or in the timezone of the value.
final bool ignoresTimeZone;
const DictionaryField({ const DictionaryField({
required this.key, required this.key,
required this.value, required this.value,
this.attributedValue,
this.label, this.label,
this.changeMessage, this.changeMessage,
this.textAlignment, this.textAlignment,
this.attributedValue, this.numberStyle,
this.currencyCode,
this.dateStyle,
this.timeStyle,
this.ignoresTimeZone = false,
}); });
factory DictionaryField.fromJson(Map<String, Object?> json) => factory DictionaryField.fromJson(Map<String, Object?> json) =>
DictionaryField( DictionaryField(
key: json['key'] as String, key: json['key'] as String,
value: DictionaryValue.parse(json['value'] as String), value: DictionaryValue.parse(json['value'] as String),
attributedValue: json['attributedValue'] == null
? null
: DictionaryValue.parse(json['attributedValue'] as String),
label: json['label'] as String?, label: json['label'] as String?,
changeMessage: json['changeMessage'] as String?, changeMessage: json['changeMessage'] as String?,
textAlignment: textAlignment:
MaybeDecode.maybeTextAlign(json['textAlignment'] as String?), MaybeDecode.maybeTextAlign(json['textAlignment'] as String?),
attributedValue: json['attributedValue'] == null numberStyle:
? null MaybeDecode.maybeNumberStyle(json['numberStyle'] as String?),
: DictionaryValue.parse(json['attributedValue'] as String), currencyCode: json['currencyCode'] as String?,
dateStyle: MaybeDecode.maybeDateStyle(json['dateStyle'] as String?),
timeStyle: MaybeDecode.maybeDateStyle(json['timeStyle'] as String?),
ignoresTimeZone: json['ignoresTimeZone'] == true ? true : false,
); );
/// Localized version of [label] based on given [locale] and [pass]. /// Localized version of [label] based on given [locale] and [pass].
@ -118,6 +143,75 @@ class DictionaryField {
return localizations?[label] ?? label; return localizations?[label] ?? label;
} }
/// Localized version of [value] based on given [locale] and [pass].
String? getLocalizedValue(PassFile pass, Locale? locale) {
final language = locale?.toLanguageTag();
if (value is StringDictionaryValue) {
String string = (value as StringDictionaryValue).string;
final localizations = pass.getLocalizations(locale);
return localizations?[string] ?? string;
} else if (value is DateTimeDictionaryValue) {
DateTime dateTime = (value as DateTimeDictionaryValue).dateTime;
DateFormat format = DateFormat(null, language);
switch (dateStyle) {
case PassTextDateStyle.None:
break;
case PassTextDateStyle.Short:
format = DateFormat.yMd(language);
break;
case PassTextDateStyle.Medium:
format = DateFormat.yMMMd(language);
break;
case PassTextDateStyle.Long:
format = DateFormat.yMMMMd(language);
break;
case PassTextDateStyle.Full:
default:
format = DateFormat.yMMMMEEEEd(language);
break;
}
switch (timeStyle) {
case PassTextDateStyle.None:
break;
case PassTextDateStyle.Short:
format.add_jm();
break;
case PassTextDateStyle.Medium:
case PassTextDateStyle.Long:
case PassTextDateStyle.Full:
default:
format.add_jms();
break;
}
if (ignoresTimeZone) {
/// violating standard: return in UTC instead of original timezone
return format.format(dateTime);
} else {
return format.format(dateTime.toLocal());
}
} else if (value is NumberDictionaryValue) {
num number = (value as NumberDictionaryValue).number;
if (currencyCode != null) {
return NumberFormat.simpleCurrency(locale: language, name: currencyCode)
.format(number);
} else if (numberStyle == PassTextNumberStyle.Percent) {
return NumberFormat.percentPattern(language).format(number);
} else if (numberStyle == PassTextNumberStyle.Scientific) {
return NumberFormat.scientificPattern(language).format(number);
} else {
/// PassTextNumberStyle.SpellOut not implemented
/// default to decimal format
return NumberFormat.decimalPattern(language).format(number);
}
}
return null;
}
/// Localized version of [changeMessage] based on given [locale] and [pass]. /// Localized version of [changeMessage] based on given [locale] and [pass].
String? getLocalizedChangeMessage(PassFile pass, Locale? locale) { String? getLocalizedChangeMessage(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale); final localizations = pass.getLocalizations(locale);
@ -131,7 +225,7 @@ abstract class DictionaryValue {
/// parses the correct [DictionaryValue] implementor based on a given [value]. /// parses the correct [DictionaryValue] implementor based on a given [value].
factory DictionaryValue.parse(String value) { factory DictionaryValue.parse(String value) {
final number = int.tryParse(value); final number = num.tryParse(value);
if (number != null) return NumberDictionaryValue(number); if (number != null) return NumberDictionaryValue(number);
final dateTime = DateTime.tryParse(value); final dateTime = DateTime.tryParse(value);
@ -139,9 +233,6 @@ abstract class DictionaryValue {
return StringDictionaryValue(value); return StringDictionaryValue(value);
} }
/// Localized value based on given [locale] and [pass].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale);
} }
/// [String] content of a [DictionaryField]. /// [String] content of a [DictionaryField].
@ -149,14 +240,6 @@ class StringDictionaryValue extends DictionaryValue {
final String string; final String string;
const StringDictionaryValue(this.string); const StringDictionaryValue(this.string);
@override
/// Localized value based on given [locale] and [pass].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return StringDictionaryValue(localizations?[string] ?? string);
}
} }
/// [DateTime] content of a [DictionaryField]. /// [DateTime] content of a [DictionaryField].
@ -164,23 +247,13 @@ class DateTimeDictionaryValue extends DictionaryValue {
final DateTime dateTime; final DateTime dateTime;
const DateTimeDictionaryValue(this.dateTime); const DateTimeDictionaryValue(this.dateTime);
@override
/// Localized value based on given [locale] and [pass]. Same as [dateTime].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) => this;
} }
/// [int] content of a [DictionaryField]. /// [num] content of a [DictionaryField].
class NumberDictionaryValue extends DictionaryValue { class NumberDictionaryValue extends DictionaryValue {
final int number; final num number;
const NumberDictionaryValue(this.number); const NumberDictionaryValue(this.number);
@override
/// Localized value based on given [locale] and [pass]. Same as [number].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) => this;
} }
/// Possible types of [PassStructureDictionary.transitType]. /// Possible types of [PassStructureDictionary.transitType].
@ -216,6 +289,39 @@ enum PassTextAlign {
natural, natural,
} }
/// Possible types of [DictionaryField.dateStyle] and [DictionaryField.timeStyle].
enum PassTextDateStyle {
/// PKDateStyleNone
None,
/// PKDateStyleShort
Short,
/// PKDateStyleMedium
Medium,
/// PKDateStyleLong
Long,
/// PKDateStyleFull
Full,
}
/// Possible types of [DictionaryField.numberStyle].
enum PassTextNumberStyle {
/// PKNumberStyleDecimal
Decimal,
/// PKNumberStylePercent
Percent,
/// PKNumberStyleScientific
Scientific,
/// PKNumberStyleSpellOut
SpellOut,
}
extension _TarnsitType on TransitType { extension _TarnsitType on TransitType {
static TransitType? parse(String? type) { static TransitType? parse(String? type) {
if (type == null) return null; if (type == null) return null;

View file

@ -1,29 +0,0 @@
import '../models/pass_structure_dictionary.dart';
import 'color_helper.dart';
abstract class MaybeDecode {
const MaybeDecode._();
static int? maybeColor(String? colorCode) {
if (colorCode == null) return null;
return fromCssColor(colorCode);
}
static DateTime? maybeDateTime(String? timeStamp) {
if (timeStamp == null) return null;
return DateTime.tryParse(timeStamp);
}
static PassTextAlign? maybeTextAlign(String? align) {
switch (align) {
case 'PKTextAlignmentLeft':
return PassTextAlign.left;
case 'PKTextAlignmentCenter':
return PassTextAlign.center;
case 'PKTextAlignmentRight':
return PassTextAlign.right;
default:
return PassTextAlign.natural;
}
}
}

View file

@ -0,0 +1,61 @@
import '../models/pass_structure_dictionary.dart';
import 'color_helper.dart';
abstract class MaybeDecode {
const MaybeDecode._();
static int? maybeColor(String? colorCode) {
if (colorCode == null) return null;
return fromCssColor(colorCode);
}
static DateTime? maybeDateTime(String? timeStamp) {
if (timeStamp == null) return null;
return DateTime.tryParse(timeStamp);
}
static PassTextAlign? maybeTextAlign(String? align) {
switch (align) {
case 'PKTextAlignmentLeft':
return PassTextAlign.left;
case 'PKTextAlignmentCenter':
return PassTextAlign.center;
case 'PKTextAlignmentRight':
return PassTextAlign.right;
default:
return PassTextAlign.natural;
}
}
static PassTextDateStyle? maybeDateStyle(String? style) {
switch (style) {
case 'PKDateStyleNone':
return PassTextDateStyle.None;
case 'PKDateStyleShort':
return PassTextDateStyle.Short;
case 'PKDateStyleMedium':
return PassTextDateStyle.Medium;
case 'PKDateStyleLong':
return PassTextDateStyle.Long;
case 'PKDateStyleFull':
return PassTextDateStyle.Full;
default:
return null;
}
}
static PassTextNumberStyle? maybeNumberStyle(String? style) {
switch (style) {
case 'PKNumberStyleDecimal':
return PassTextNumberStyle.Decimal;
case 'PKNumberStylePercent':
return PassTextNumberStyle.Percent;
case 'PKNumberStyleScientific':
return PassTextNumberStyle.Scientific;
case 'PKNumberStyleSpellOut':
return PassTextNumberStyle.SpellOut;
default:
return null;
}
}
}