mirror of
https://gitlab.com/TheOneWithTheBraid/dart_pkpass.git
synced 2025-07-06 05:18:47 +00:00
feat: implement web service
Signed-off-by: The one with the braid <info@braid.business>
This commit is contained in:
parent
44494eaa90
commit
6e7f19a764
26 changed files with 331 additions and 512 deletions
30
lib/pkpass/error.dart
Normal file
30
lib/pkpass/error.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
abstract class PKPassError extends Error {
|
||||
final String message;
|
||||
|
||||
PKPassError({required this.message});
|
||||
|
||||
@override
|
||||
String toString() => 'PKPassError: $message';
|
||||
}
|
||||
|
||||
class InvalidEncodingError extends PKPassError {
|
||||
InvalidEncodingError() : super(message: 'Input not in ZIP format.');
|
||||
}
|
||||
|
||||
class ManifestNotFoundError extends PKPassError {
|
||||
ManifestNotFoundError()
|
||||
: super(message: 'No manifest.json found in PKPass archive.');
|
||||
}
|
||||
|
||||
class ManifestChecksumError extends PKPassError {
|
||||
final String expected;
|
||||
final String actual;
|
||||
|
||||
ManifestChecksumError({
|
||||
required this.expected,
|
||||
required this.actual,
|
||||
}) : super(
|
||||
message:
|
||||
'Manifest sha1 checksum missmatch, expected $expected, actual $actual',
|
||||
);
|
||||
}
|
89
lib/pkpass/models/barcode.dart
Normal file
89
lib/pkpass/models/barcode.dart
Normal file
|
@ -0,0 +1,89 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:barcode/barcode.dart';
|
||||
|
||||
/// Information about a pass’s barcode.
|
||||
///
|
||||
/// Use with Flutter:
|
||||
///
|
||||
/// ```dart
|
||||
/// BarcodeWidget.fromBytes(
|
||||
/// barcode: Barcode.fromType(myPassBarcode.format),
|
||||
/// data: myPassBarcode.barcodeData,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Please not the spec requires you to display the [altText] next to the
|
||||
/// barcode in case available.
|
||||
class PassBarcode {
|
||||
/// the [Encoding] supported for [messageEncoding]. Can be expanded at runtime.
|
||||
///
|
||||
/// Default values are
|
||||
/// - iso-8859-1: [Latin1Codec]
|
||||
/// - utf-8: [Utf8Codec]
|
||||
static Map<String, Encoding> supportedCodecs = {
|
||||
'iso-8859-1': Latin1Codec(),
|
||||
'iso-8859': Latin1Codec(),
|
||||
'iso8859': Latin1Codec(),
|
||||
'utf-8': Utf8Codec(),
|
||||
'utf8': Utf8Codec(),
|
||||
};
|
||||
|
||||
static const _allowedFormats = {
|
||||
'PKBarcodeFormatQR': BarcodeType.QrCode,
|
||||
'PKBarcodeFormatPDF417': BarcodeType.PDF417,
|
||||
'PKBarcodeFormatAztec': BarcodeType.Aztec,
|
||||
'PKBarcodeFormatCode128': BarcodeType.Code128,
|
||||
};
|
||||
|
||||
/// Barcode format. For the barcode dictionary, you can use only the
|
||||
/// following values: PKBarcodeFormatQR, PKBarcodeFormatPDF417, or
|
||||
/// PKBarcodeFormatAztec. For dictionaries in the barcodes array, you may
|
||||
/// also use PKBarcodeFormatCode128.
|
||||
///
|
||||
/// The spec defined keys are converted into the corresponding [BarcodeType]
|
||||
/// representations.
|
||||
final BarcodeType format;
|
||||
|
||||
/// Message or payload to be displayed as a barcode.
|
||||
///
|
||||
/// Do not use directly, use the encoded [barcodeData] instead.
|
||||
final String message;
|
||||
|
||||
/// Text encoding that is used to convert the message from the string
|
||||
/// representation to a data representation to render the barcode.
|
||||
///
|
||||
/// The value is typically iso-8859-1, but you may use another encoding that
|
||||
/// is supported by your barcode scanning infrastructure.
|
||||
///
|
||||
/// Only supported values by this packages are:
|
||||
/// - iso-8859-1
|
||||
/// - utf-8
|
||||
///
|
||||
/// Custom codecs can be provided as [Codec] in [PassBarcode.supportedCodecs].
|
||||
final Encoding messageEncoding;
|
||||
|
||||
/// Text displayed near the barcode. For example, a human-readable version
|
||||
/// of the barcode data in case the barcode doesn’t scan.
|
||||
final String? altText;
|
||||
|
||||
const PassBarcode({
|
||||
required this.format,
|
||||
required this.message,
|
||||
required this.messageEncoding,
|
||||
required this.altText,
|
||||
});
|
||||
|
||||
factory PassBarcode.fromJson(Map<String, Object?> json) => PassBarcode(
|
||||
format: _allowedFormats[json['format']]!,
|
||||
message: json['message'] as String,
|
||||
messageEncoding:
|
||||
supportedCodecs[(json['messageEncoding'] as String).toLowerCase()]!,
|
||||
altText: json['altText'] as String?,
|
||||
);
|
||||
|
||||
/// Correctly encoded byte list to be displayed in the [barcode].
|
||||
Uint8List get barcodeData =>
|
||||
Uint8List.fromList(messageEncoding.encode(message));
|
||||
}
|
30
lib/pkpass/models/beacon.dart
Normal file
30
lib/pkpass/models/beacon.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
/// Information about a location beacon.
|
||||
class Beacon {
|
||||
/// Unique identifier of a Bluetooth Low Energy location beacon.
|
||||
final String proximityUUID;
|
||||
|
||||
/// Major identifier of a Bluetooth Low Energy location beacon.
|
||||
final double? major;
|
||||
|
||||
/// Minor identifier of a Bluetooth Low Energy location beacon.
|
||||
final double? minor;
|
||||
|
||||
/// Text displayed on the lock screen when the pass is currently relevant.
|
||||
/// For example, a description of the nearby location such as
|
||||
/// “Store nearby on 1st and Main.”
|
||||
final String? relevantText;
|
||||
|
||||
const Beacon({
|
||||
required this.proximityUUID,
|
||||
this.major,
|
||||
this.minor,
|
||||
this.relevantText,
|
||||
});
|
||||
|
||||
factory Beacon.fromJson(Map<String, Object?> json) => Beacon(
|
||||
proximityUUID: json['proximityUUID'] as String,
|
||||
major: json['major'] as double?,
|
||||
minor: json['minor'] as double?,
|
||||
relevantText: json['relevantText'] as String?,
|
||||
);
|
||||
}
|
30
lib/pkpass/models/location.dart
Normal file
30
lib/pkpass/models/location.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
/// Information about a location.
|
||||
class Location {
|
||||
/// Latitude, in degrees, of the location.
|
||||
final double latitude;
|
||||
|
||||
/// Longitude, in degrees, of the location.
|
||||
final double longitude;
|
||||
|
||||
/// Altitude, in meters, of the location.
|
||||
final double? altitude;
|
||||
|
||||
/// Text displayed on the lock screen when the pass is currently relevant.
|
||||
/// For example, a description of the nearby location such as
|
||||
/// “Store nearby on 1st and Main.”
|
||||
final String? relevantText;
|
||||
|
||||
const Location({
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.altitude,
|
||||
this.relevantText,
|
||||
});
|
||||
|
||||
factory Location.fromJson(Map<String, Object?> json) => Location(
|
||||
latitude: json['latitude'] as double,
|
||||
longitude: json['longitude'] as double,
|
||||
altitude: json['altitude'] as double?,
|
||||
relevantText: json['relevantText'] as String?,
|
||||
);
|
||||
}
|
210
lib/pkpass/models/pass.dart
Normal file
210
lib/pkpass/models/pass.dart
Normal file
|
@ -0,0 +1,210 @@
|
|||
import 'package:intl/locale.dart';
|
||||
|
||||
import 'package:pkpass/pkpass.dart';
|
||||
import 'package:pkpass/pkpass/utils/mabe_decode.dart';
|
||||
|
||||
/// Information that is required for all passes.
|
||||
class PassMetadata {
|
||||
/// Brief description of the pass, used by accessibility technologies.
|
||||
///
|
||||
/// Don’t try to include all of the data on the pass in its description,
|
||||
/// just include enough detail to distinguish passes of the same type.
|
||||
final String description;
|
||||
|
||||
/// Version of the file format. The value must be 1.
|
||||
final int formatVersion;
|
||||
|
||||
/// Display name of the organization that originated and signed the pass.
|
||||
final String organizationName;
|
||||
|
||||
/// Pass type identifier, as issued by Apple. The value must correspond with
|
||||
/// your signing certificate.
|
||||
final String passTypeIdentifier;
|
||||
|
||||
/// Serial number that uniquely identifies the pass. No two passes with the
|
||||
/// same pass type identifier may have the same serial number.
|
||||
final String serialNumber;
|
||||
|
||||
/// Team identifier of the organization that originated and signed the pass,
|
||||
/// as issued by Apple.
|
||||
final String teamIdentifier;
|
||||
|
||||
/// A URL to be passed to the associated app when launching it.
|
||||
final String? appLaunchURL;
|
||||
|
||||
/// Date and time when the pass expires.
|
||||
final DateTime? expirationDate;
|
||||
|
||||
/// Indicates that the pass is void—for example, a one time use coupon that
|
||||
/// has been redeemed. The default value is false.
|
||||
final bool voided;
|
||||
|
||||
/// Beacons marking locations where the pass is relevant.
|
||||
final List<Beacon> beacons;
|
||||
|
||||
/// Locations where the pass is relevant. For example, the location of your store.
|
||||
final List<Location> locations;
|
||||
|
||||
/// Maximum distance in meters from a relevant latitude and longitude that
|
||||
/// the pass is relevant. This number is compared to the pass’s default
|
||||
/// distance and the smaller value is used.
|
||||
final int? maxDistance;
|
||||
|
||||
/// Recommended for event tickets and boarding passes; otherwise optional.
|
||||
/// Date and time when the pass becomes relevant. For example, the start
|
||||
/// time of a movie.
|
||||
final DateTime? relevantDate;
|
||||
|
||||
/// Information specific to a boarding pass.
|
||||
final PassStructureDictionary? boardingPass;
|
||||
|
||||
/// Information specific to a coupon.
|
||||
final PassStructureDictionary? coupon;
|
||||
|
||||
/// Information specific to an event ticket.
|
||||
final PassStructureDictionary? eventTicket;
|
||||
|
||||
/// Information specific to a generic pass.
|
||||
final PassStructureDictionary? generic;
|
||||
|
||||
/// Information specific to a store card.
|
||||
final PassStructureDictionary? storeCard;
|
||||
|
||||
/// Information specific to the pass’s barcode.
|
||||
/// The system uses the first valid barcode dictionary in the array.
|
||||
/// Additional dictionaries can be added as fallbacks.
|
||||
final List<PassBarcode> barcodes;
|
||||
|
||||
/// Background color of the pass.
|
||||
final int? backgroundColor;
|
||||
|
||||
/// Foreground color of the pass.
|
||||
final int? foregroundColor;
|
||||
|
||||
/// Optional for event tickets and boarding passes; otherwise not allowed.
|
||||
/// Identifier used to group related passes. If a grouping identifier is
|
||||
/// specified, passes with the same style, pass type identifier, and grouping
|
||||
/// identifier are displayed as a group. Otherwise, passes are grouped
|
||||
/// automatically.
|
||||
///
|
||||
/// Use this to group passes that are tightly related, such as the boarding
|
||||
/// passes for different connections of the same trip.
|
||||
final String? groupingIdentifier;
|
||||
|
||||
/// Color of the label text.
|
||||
final int? labelColor;
|
||||
|
||||
/// Text displayed next to the logo on the pass.
|
||||
final String? logoText;
|
||||
|
||||
/// Information used to update passes using the web service.
|
||||
final PassWebService? webService;
|
||||
|
||||
const PassMetadata({
|
||||
required this.description,
|
||||
required this.formatVersion,
|
||||
required this.organizationName,
|
||||
required this.passTypeIdentifier,
|
||||
required this.serialNumber,
|
||||
required this.teamIdentifier,
|
||||
this.appLaunchURL,
|
||||
this.expirationDate,
|
||||
this.voided = false,
|
||||
this.beacons = const [],
|
||||
this.locations = const [],
|
||||
this.maxDistance,
|
||||
this.relevantDate,
|
||||
this.boardingPass,
|
||||
this.coupon,
|
||||
this.eventTicket,
|
||||
this.generic,
|
||||
this.storeCard,
|
||||
this.barcodes = const [],
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.groupingIdentifier,
|
||||
this.labelColor,
|
||||
this.logoText,
|
||||
this.webService,
|
||||
});
|
||||
|
||||
factory PassMetadata.fromJson(Map<String, Object?> json) => PassMetadata(
|
||||
description: json['description'] as String,
|
||||
formatVersion: json['formatVersion'] as int,
|
||||
organizationName: json['organizationName'] as String,
|
||||
passTypeIdentifier: json['passTypeIdentifier'] as String,
|
||||
serialNumber: json['serialNumber'] as String,
|
||||
teamIdentifier: json['teamIdentifier'] as String,
|
||||
boardingPass: json['boardingPass'] == null
|
||||
? null
|
||||
: PassStructureDictionary.fromJson(
|
||||
(json['boardingPass'] as Map).cast<String, Object?>(),
|
||||
),
|
||||
coupon: json['coupon'] == null
|
||||
? null
|
||||
: PassStructureDictionary.fromJson(
|
||||
(json['coupon'] as Map).cast<String, Object?>(),
|
||||
),
|
||||
eventTicket: json['eventTicket'] == null
|
||||
? null
|
||||
: PassStructureDictionary.fromJson(
|
||||
(json['eventTicket'] as Map).cast<String, Object?>(),
|
||||
),
|
||||
generic: json['generic'] == null
|
||||
? null
|
||||
: PassStructureDictionary.fromJson(
|
||||
(json['generic'] as Map).cast<String, Object?>(),
|
||||
),
|
||||
storeCard: json['storeCard'] == null
|
||||
? null
|
||||
: PassStructureDictionary.fromJson(
|
||||
(json['storeCard'] as Map).cast<String, Object?>(),
|
||||
),
|
||||
barcodes: (json['barcodes'] as List? ??
|
||||
[if (json['barcode'] != null) json['barcode']])
|
||||
.map((i) => PassBarcode.fromJson(i))
|
||||
.toList(),
|
||||
locations: (json['locations'] as List? ?? [])
|
||||
.map((i) => Location.fromJson(i))
|
||||
.toList(),
|
||||
appLaunchURL: json['appLaunchURL'] as String?,
|
||||
expirationDate:
|
||||
MaybeDecode.maybeDateTime(json['expirationDate'] as String?),
|
||||
voided: json['voided'] as bool? ?? false,
|
||||
beacons: (json['beacons'] as List? ?? [])
|
||||
.map((i) => Beacon.fromJson(i))
|
||||
.toList(),
|
||||
maxDistance: json['maxDistance'] as int?,
|
||||
relevantDate:
|
||||
MaybeDecode.maybeDateTime(json['relevantDate'] as String?),
|
||||
backgroundColor:
|
||||
MaybeDecode.maybeColor(json['backgroundColor'] as String?),
|
||||
foregroundColor:
|
||||
MaybeDecode.maybeColor(json['foregroundColor'] as String?),
|
||||
groupingIdentifier: json['locoText'] as String?,
|
||||
labelColor: MaybeDecode.maybeColor(json['labelColor'] as String?),
|
||||
logoText: json['locoText'] as String?,
|
||||
webService: PassWebService.maybe(
|
||||
authenticationToken: json['authenticationToken'] as String?,
|
||||
webServiceURL: json['webServiceURL'] as String?,
|
||||
),
|
||||
);
|
||||
|
||||
/// Localized version of [description] based on given [locale] and [pass].
|
||||
String getLocalizedDescription(PassFile pass, Locale? locale) {
|
||||
final localizations = pass.getLocalizations(locale);
|
||||
return localizations?[description] ?? description;
|
||||
}
|
||||
|
||||
/// Localized version of [organizationName] based on given [locale] and [pass].
|
||||
String getLocalizedOrganizationName(PassFile pass, Locale? locale) {
|
||||
final localizations = pass.getLocalizations(locale);
|
||||
return localizations?[organizationName] ?? organizationName;
|
||||
}
|
||||
|
||||
/// Localized version of [logoText] based on given [locale] and [pass].
|
||||
String? getLocalizedLogoText(PassFile pass, Locale? locale) {
|
||||
final localizations = pass.getLocalizations(locale);
|
||||
return localizations?[logoText] ?? logoText;
|
||||
}
|
||||
}
|
235
lib/pkpass/models/pass_structure_dictionary.dart
Normal file
235
lib/pkpass/models/pass_structure_dictionary.dart
Normal file
|
@ -0,0 +1,235 @@
|
|||
import 'package:intl/locale.dart';
|
||||
|
||||
import 'package:pkpass/pkpass.dart';
|
||||
import 'package:pkpass/pkpass/utils/mabe_decode.dart';
|
||||
|
||||
/// Keys that define the structure of the pass.
|
||||
///
|
||||
/// These keys are used for all pass styles and partition the fields into the various parts of the pass.
|
||||
class PassStructureDictionary {
|
||||
/// Fields to be displayed in the header on the front of the pass.
|
||||
///
|
||||
/// Use header fields sparingly; unlike all other fields, they remain visible when a stack of passes are displayed.
|
||||
final List<DictionaryField> headerFields;
|
||||
|
||||
/// Fields to be displayed prominently on the front of the pass.
|
||||
final List<DictionaryField> primaryFields;
|
||||
|
||||
/// Fields to be displayed on the front of the pass.
|
||||
final List<DictionaryField> secondaryFields;
|
||||
|
||||
/// Fields to be on the back of the pass.
|
||||
final List<DictionaryField> backFields;
|
||||
|
||||
/// Additional fields to be displayed on the front of the pass.
|
||||
final List<DictionaryField> auxiliaryFields;
|
||||
|
||||
/// Required for boarding passes; otherwise not allowed. Type of transit.
|
||||
final TransitType? transitType;
|
||||
|
||||
const PassStructureDictionary({
|
||||
this.headerFields = const [],
|
||||
this.primaryFields = const [],
|
||||
this.secondaryFields = const [],
|
||||
this.backFields = const [],
|
||||
this.auxiliaryFields = const [],
|
||||
this.transitType,
|
||||
});
|
||||
|
||||
factory PassStructureDictionary.fromJson(Map<String, Object?> json) =>
|
||||
PassStructureDictionary(
|
||||
headerFields: (json['headerFields'] as List?)
|
||||
?.map((i) => DictionaryField.fromJson(i))
|
||||
.toList() ??
|
||||
[],
|
||||
primaryFields: (json['primaryFields'] as List?)
|
||||
?.map((i) => DictionaryField.fromJson(i))
|
||||
.toList() ??
|
||||
[],
|
||||
secondaryFields: (json['secondaryFields'] as List?)
|
||||
?.map((i) => DictionaryField.fromJson(i))
|
||||
.toList() ??
|
||||
[],
|
||||
backFields: (json['backFields'] as List?)
|
||||
?.map((i) => DictionaryField.fromJson(i))
|
||||
.toList() ??
|
||||
[],
|
||||
auxiliaryFields: (json['auxiliaryFields'] as List?)
|
||||
?.map((i) => DictionaryField.fromJson(i))
|
||||
.toList() ??
|
||||
[],
|
||||
transitType: _TarnsitType.parse(json['transitType'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
/// Information about a field.
|
||||
class DictionaryField {
|
||||
/// The key must be unique within the scope of the entire pass. For example, “departure-gate.”
|
||||
final String key;
|
||||
|
||||
/// Value of the field, for example, 42.
|
||||
final DictionaryValue value;
|
||||
|
||||
/// Label text for the field.
|
||||
final String? label;
|
||||
|
||||
/// Format string for the alert text that is displayed when the pass is updated. The format string must contain the escape %@, which is replaced with the field’s new value. For example, “Gate changed to %@.”
|
||||
///
|
||||
/// If you don’t specify a change message, the user isn’t notified when the field changes.
|
||||
final String? changeMessage;
|
||||
|
||||
/// Alignment for the field’s contents.
|
||||
final PassTextAlign? textAlignment;
|
||||
|
||||
/// 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 key’s value overrides the text specified by the value key.
|
||||
final DictionaryValue? attributedValue;
|
||||
|
||||
const DictionaryField({
|
||||
required this.key,
|
||||
required this.value,
|
||||
this.label,
|
||||
this.changeMessage,
|
||||
this.textAlignment,
|
||||
this.attributedValue,
|
||||
});
|
||||
|
||||
factory DictionaryField.fromJson(Map<String, Object?> json) =>
|
||||
DictionaryField(
|
||||
key: json['key'] as String,
|
||||
value: DictionaryValue.parse(json['value'] as String),
|
||||
label: json['label'] as String?,
|
||||
changeMessage: json['changeMessage'] as String?,
|
||||
textAlignment:
|
||||
MaybeDecode.maybeTextAlign(json['textAlignment'] as String?),
|
||||
attributedValue: json['attributedValue'] == null
|
||||
? null
|
||||
: DictionaryValue.parse(json['attributedValue'] as String),
|
||||
);
|
||||
|
||||
/// Localized version of [label] based on given [locale] and [pass].
|
||||
String? getLocalizedLabel(PassFile pass, Locale? locale) {
|
||||
final localizations = pass.getLocalizations(locale);
|
||||
return localizations?[label] ?? label;
|
||||
}
|
||||
|
||||
/// Localized version of [changeMessage] based on given [locale] and [pass].
|
||||
String? getLocalizedChangeMessage(PassFile pass, Locale? locale) {
|
||||
final localizations = pass.getLocalizations(locale);
|
||||
return localizations?[changeMessage] ?? changeMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// represents the possible values of a [DictionaryField].
|
||||
abstract class DictionaryValue {
|
||||
const DictionaryValue();
|
||||
|
||||
/// parses the correct [DictionaryValue] implementor based on a given [value].
|
||||
factory DictionaryValue.parse(String value) {
|
||||
final number = int.tryParse(value);
|
||||
if (number != null) return NumberDictionaryValue(number);
|
||||
|
||||
final dateTime = DateTime.tryParse(value);
|
||||
if (dateTime != null) return DateTimeDictionaryValue(dateTime);
|
||||
|
||||
return StringDictionaryValue(value);
|
||||
}
|
||||
|
||||
/// Localized value based on given [locale] and [pass].
|
||||
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale);
|
||||
}
|
||||
|
||||
/// [String] content of a [DictionaryField].
|
||||
class StringDictionaryValue extends DictionaryValue {
|
||||
final String 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].
|
||||
class DateTimeDictionaryValue extends DictionaryValue {
|
||||
final DateTime 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].
|
||||
class NumberDictionaryValue extends DictionaryValue {
|
||||
final int 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].
|
||||
enum TransitType {
|
||||
/// PKTransitTypeAir
|
||||
air,
|
||||
|
||||
/// PKTransitTypeBoat
|
||||
boat,
|
||||
|
||||
/// PKTransitTypeBus
|
||||
bus,
|
||||
|
||||
/// PKTransitTypeGeneric
|
||||
generic,
|
||||
|
||||
/// PKTransitTypeTrain
|
||||
train,
|
||||
}
|
||||
|
||||
/// Possible types of [DictionaryField.textAlignment].
|
||||
enum PassTextAlign {
|
||||
/// PKTextAlignmentLeft, corresponds `dart:ui` [TextAlign.left]
|
||||
left,
|
||||
|
||||
/// PKTextAlignmentCenter, corresponds `dart:ui` [TextAlign.center]
|
||||
center,
|
||||
|
||||
/// PKTextAlignmentRight, corresponds `dart:ui` [TextAlign.left]
|
||||
right,
|
||||
|
||||
/// PKTextAlignmentNatural, corresponds `dart:ui` [TextAlign.start]
|
||||
natural,
|
||||
}
|
||||
|
||||
extension _TarnsitType on TransitType {
|
||||
static TransitType? parse(String? type) {
|
||||
if (type == null) return null;
|
||||
switch (type) {
|
||||
case 'PKTransitTypeAir':
|
||||
return TransitType.air;
|
||||
case 'PKTransitTypeBoat':
|
||||
return TransitType.boat;
|
||||
case 'PKTransitTypeBus':
|
||||
return TransitType.bus;
|
||||
case 'PKTransitTypeTrain':
|
||||
return TransitType.train;
|
||||
default:
|
||||
return TransitType.generic;
|
||||
}
|
||||
}
|
||||
}
|
31
lib/pkpass/models/pass_web_service.dart
Normal file
31
lib/pkpass/models/pass_web_service.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
/// Metadata required for Pass Web Service
|
||||
///
|
||||
/// https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988
|
||||
class PassWebService {
|
||||
/// The authentication token to use with the web service.
|
||||
/// The token must be 16 characters or longer.
|
||||
final String authenticationToken;
|
||||
|
||||
/// The URL of a web service that conforms to the API described in PassKit Web Service Reference.
|
||||
final Uri webServiceURL;
|
||||
|
||||
const PassWebService({
|
||||
required this.authenticationToken,
|
||||
required this.webServiceURL,
|
||||
});
|
||||
|
||||
/// returns a [PassWebService] in case [authenticationToken] and
|
||||
/// [webServiceURL] are both valid values.
|
||||
static PassWebService? maybe({
|
||||
String? authenticationToken,
|
||||
String? webServiceURL,
|
||||
}) {
|
||||
if (authenticationToken == null || webServiceURL == null) return null;
|
||||
final uri = Uri.tryParse(webServiceURL);
|
||||
if (uri == null || uri.scheme != 'https') return null;
|
||||
return PassWebService(
|
||||
authenticationToken: authenticationToken,
|
||||
webServiceURL: uri,
|
||||
);
|
||||
}
|
||||
}
|
134
lib/pkpass/pass_file.dart
Normal file
134
lib/pkpass/pass_file.dart
Normal file
|
@ -0,0 +1,134 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:intl/locale.dart';
|
||||
|
||||
import 'package:pkpass/pkpass.dart';
|
||||
import 'package:pkpass/pkpass/utils/file_matcher.dart';
|
||||
import 'package:pkpass/pkpass/utils/lproj_parser.dart';
|
||||
|
||||
final _utf8codec = Utf8Codec();
|
||||
final _jsonCodec = JsonCodec();
|
||||
|
||||
class PassFile {
|
||||
const PassFile(this.metadata, this._folder);
|
||||
|
||||
static Future<PassFile> parse(Uint8List pass) async {
|
||||
final codec = ZipDecoder();
|
||||
Archive archive;
|
||||
try {
|
||||
archive = codec.decodeBytes(pass);
|
||||
} catch (e) {
|
||||
throw InvalidEncodingError();
|
||||
}
|
||||
|
||||
Map<String, String> manifest;
|
||||
|
||||
try {
|
||||
final file = archive.files
|
||||
.singleWhere((element) => element.name == 'manifest.json');
|
||||
manifest = (_jsonCodec.decode(
|
||||
_utf8codec.decode(
|
||||
file.rawContent?.toUint8List() ?? (file.content as Uint8List),
|
||||
),
|
||||
) as Map)
|
||||
.cast<String, String>();
|
||||
} catch (e) {
|
||||
throw ManifestNotFoundError();
|
||||
}
|
||||
|
||||
final folder = <ArchiveFile>[];
|
||||
|
||||
await Future.wait(
|
||||
manifest.entries.map(
|
||||
(manifestEntry) async {
|
||||
final file = archive.files
|
||||
.singleWhere((element) => element.name == manifestEntry.key);
|
||||
|
||||
final content =
|
||||
file.rawContent?.toUint8List() ?? file.content as Uint8List;
|
||||
|
||||
String hash = sha1.convert(content).toString();
|
||||
|
||||
final checksum = manifestEntry.value;
|
||||
|
||||
if (hash != checksum) {
|
||||
throw ManifestChecksumError(expected: checksum, actual: hash);
|
||||
}
|
||||
|
||||
folder.add(file);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final passFile =
|
||||
archive.singleWhere((element) => element.name == 'pass.json');
|
||||
|
||||
final PassMetadata metadata = PassMetadata.fromJson(
|
||||
_jsonCodec.decode(passFile.stringContent) as Map<String, Object?>,
|
||||
);
|
||||
|
||||
return PassFile(metadata, folder);
|
||||
}
|
||||
|
||||
final PassMetadata metadata;
|
||||
|
||||
final List<ArchiveFile> _folder;
|
||||
|
||||
Uint8List? _matchUtf8List({
|
||||
required String name,
|
||||
required Locale? locale,
|
||||
required int scale,
|
||||
}) {
|
||||
final files = _folder.map((e) => e.name).toList();
|
||||
final path = FileMatcher.matchFile(
|
||||
files: files,
|
||||
name: name,
|
||||
scale: scale,
|
||||
locale: locale,
|
||||
extension: 'png',
|
||||
);
|
||||
if (path == null) return null;
|
||||
final file = _folder.singleWhere((element) => element.name == path);
|
||||
final content = file.rawContent?.toUint8List() ?? file.content as Uint8List;
|
||||
return content;
|
||||
}
|
||||
|
||||
Uint8List? getBackground({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'background', locale: locale, scale: scale);
|
||||
|
||||
Uint8List? getFooter({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'footer', locale: locale, scale: scale);
|
||||
|
||||
Uint8List? getIcon({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'icon', locale: locale, scale: scale);
|
||||
|
||||
Uint8List? getLogo({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'logo', locale: locale, scale: scale);
|
||||
|
||||
Uint8List? getStrip({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'strip', locale: locale, scale: scale);
|
||||
|
||||
Uint8List? getThumbnail({Locale? locale, int scale = 1}) =>
|
||||
_matchUtf8List(name: 'thumbnail', locale: locale, scale: scale);
|
||||
|
||||
Map<String, String>? getLocalizations(Locale? locale) {
|
||||
final files = _folder.map((e) => e.name).toList();
|
||||
final paths = FileMatcher.matchLocale(
|
||||
files: files,
|
||||
name: 'pass',
|
||||
extension: 'strings',
|
||||
locale: locale,
|
||||
);
|
||||
if (paths.isEmpty) return null;
|
||||
final file = _folder.singleWhere((element) => element.name == paths.first);
|
||||
return LProjParser.parse(file.stringContent);
|
||||
}
|
||||
}
|
||||
|
||||
extension on ArchiveFile {
|
||||
String get stringContent =>
|
||||
_utf8codec.decode(rawContent?.toUint8List() ?? (content as Uint8List));
|
||||
}
|
408
lib/pkpass/utils/color_helper.dart
Normal file
408
lib/pkpass/utils/color_helper.dart
Normal file
|
@ -0,0 +1,408 @@
|
|||
/// Creates in [int] with a parsed [Color] value from a CSS color String.
|
||||
///
|
||||
/// Credits : https://pub.dev/packages/from_css_color - ported since dependency
|
||||
/// on `dart:ui`, here reimplemented without but with raw int.
|
||||
int fromCssColor(String color) {
|
||||
color = color.trim();
|
||||
|
||||
switch (_recognizeCssColorFormat(color)) {
|
||||
case ColorFormat.hex:
|
||||
return _hexToColor(color);
|
||||
case ColorFormat.rgb:
|
||||
case ColorFormat.rgba:
|
||||
return _rgbToColor(color);
|
||||
case ColorFormat.keyword:
|
||||
return colorKeywords[color]!;
|
||||
default:
|
||||
return _hslToColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Translation options from [Color] to a string format recognizable according to https://drafts.csswg.org/css-color-3 and forthcoming drafts.
|
||||
enum CssColorString {
|
||||
/// Hex format that truncates to a short form (3-4 digits) if possible and contains alpha digits if color is not fully opaque.
|
||||
hex,
|
||||
|
||||
/// RGB/RGBA format that contains alpha value if color is not fully opaque.
|
||||
rgb,
|
||||
}
|
||||
|
||||
/// Color formats available to construct [Color] instance from.
|
||||
enum ColorFormat {
|
||||
hex,
|
||||
rgb,
|
||||
rgba,
|
||||
hsl,
|
||||
hsla,
|
||||
keyword,
|
||||
}
|
||||
|
||||
ColorFormat _recognizeCssColorFormat(String color) {
|
||||
if (color.startsWith('#')) {
|
||||
return ColorFormat.hex;
|
||||
} else if (color.startsWith('rgba')) {
|
||||
return ColorFormat.rgba;
|
||||
} else if (color.startsWith('rgb')) {
|
||||
return ColorFormat.rgb;
|
||||
} else if (color.startsWith('hsla')) {
|
||||
return ColorFormat.hsla;
|
||||
} else if (color.startsWith('hsl')) {
|
||||
return ColorFormat.hsl;
|
||||
} else if (colorKeywords.containsKey(color)) {
|
||||
return ColorFormat.keyword;
|
||||
}
|
||||
|
||||
throw FormatException('Unable to recognize this CSS color format.', color);
|
||||
}
|
||||
|
||||
/// Check correctness of color string according to https://drafts.csswg.org/css-color-3
|
||||
bool isCssColor(String color) {
|
||||
color = color.trim();
|
||||
final chNumExpr = '-?[0-9]{1,3}(\\.[0-9]+)?';
|
||||
final opNumExpr = '-?([01]+(\\.[0-9]+)?|\\.[0-9]+)';
|
||||
|
||||
if (RegExp(
|
||||
r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$',
|
||||
).hasMatch(color)) {
|
||||
return true;
|
||||
} else if (RegExp(
|
||||
'^rgba?\\($chNumExpr%,\\s?$chNumExpr%,\\s?$chNumExpr%(,\\s?$opNumExpr)?\\)\$',
|
||||
).hasMatch(color)) {
|
||||
return true;
|
||||
} else if (RegExp(
|
||||
'^rgba?\\($chNumExpr,\\s?$chNumExpr,\\s?$chNumExpr(,\\s?$opNumExpr)?\\)\$',
|
||||
).hasMatch(color)) {
|
||||
return true;
|
||||
} else if (RegExp(
|
||||
'^hsla?\\($chNumExpr,\\s?$chNumExpr%,\\s?$chNumExpr%(,\\s?$opNumExpr)?\\)\$',
|
||||
).hasMatch(color)) {
|
||||
return true;
|
||||
} else if (colorKeywords.containsKey(color)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Call isCssColor() instead.'
|
||||
'This feature was deprecated since v1.2.0 to meet the https://dart.dev/guides/language/effective-dart/style#do-capitalize-acronyms-and-abbreviations-longer-than-two-letters-like-words')
|
||||
|
||||
/// Check correctness of color string according to https://drafts.csswg.org/css-color-3
|
||||
bool isCSSColor(String color) {
|
||||
return isCssColor(color);
|
||||
}
|
||||
|
||||
/// Creates [Color] instance from hexadecimal color value.
|
||||
int _hexToColor(String color) {
|
||||
color = color.substring(1);
|
||||
String alpha = 'FF';
|
||||
|
||||
if (color.length == 4) {
|
||||
alpha = color[3] * 2;
|
||||
color = color.substring(0, 3);
|
||||
} else if (color.length == 8) {
|
||||
alpha = color.substring(6);
|
||||
color = color.substring(0, 6);
|
||||
}
|
||||
|
||||
if (color.length == 3) {
|
||||
color = color.splitMapJoin('', onNonMatch: (m) => m * 2);
|
||||
} else if (color.length != 6) {
|
||||
throw FormatException(
|
||||
'Hex color string has incorrect length, only strings of 3 or 6 characters are allowed.',
|
||||
'#$color',
|
||||
);
|
||||
}
|
||||
return 0x1000000 *
|
||||
int.parse(
|
||||
alpha,
|
||||
radix: 16,
|
||||
) +
|
||||
int.parse(
|
||||
color,
|
||||
radix: 16,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses channels from RGBA/HSLA string representation.
|
||||
List<String>? _parseChannels(color) {
|
||||
return color.substring(color.indexOf('(') + 1, color.length - 1).split(',');
|
||||
}
|
||||
|
||||
/// Converts RGB channel numeric [value] to hexadecimal integer form.
|
||||
int _rgbChannelNumToHex(String value) {
|
||||
return double.parse(value).clamp(0, 255).floor();
|
||||
}
|
||||
|
||||
/// Returns `true` if all [rgb] channels are in percent format, `false` in non of them, throws otherwise.
|
||||
bool _isPercentFormat(List<String> rgb) {
|
||||
if (rgb.every((ch) => ch.endsWith('%'))) {
|
||||
return true;
|
||||
} else if (rgb.every((ch) => !ch.endsWith('%'))) {
|
||||
return false;
|
||||
} else {
|
||||
throw FormatException(
|
||||
'Mixing integer and percentage values in the same RGB color definition is forbidden.',
|
||||
rgb.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts RGBA/HSLA opacity channel [value] to hexadecimal integer form.
|
||||
int _opacityChannelToHex(String value) {
|
||||
return (double.parse(value).clamp(0, 1) * 255).floor();
|
||||
}
|
||||
|
||||
/// Converts RGB channel percentage [value] to hexadecimal integer form.
|
||||
int _rgbChannelPercentToHex(String value) {
|
||||
return (_parsePercent(value) * 255 / 100).floor();
|
||||
}
|
||||
|
||||
/// Parses numeric value from string [percent] representation.
|
||||
num _parsePercent(String percent) {
|
||||
return (double.parse(percent.substring(0, percent.length - 1)).clamp(0, 100));
|
||||
}
|
||||
|
||||
/// Creates [Color] instance from RGB(A) color value.
|
||||
int _rgbToColor(String color) {
|
||||
var channels = _parseChannels(color)!;
|
||||
var result = 0xFF000000;
|
||||
var shift = 16;
|
||||
|
||||
if (channels.length == 4) {
|
||||
result = (_opacityChannelToHex(channels.removeLast()) << 24) & result;
|
||||
} else if (channels.length != 3) {
|
||||
throw FormatException(
|
||||
'Incorrect number of values in RGB color string, there must be 3 or 4 of them.',
|
||||
color,
|
||||
);
|
||||
}
|
||||
if (_isPercentFormat(channels)) {
|
||||
for (var ch in channels) {
|
||||
result = (_rgbChannelPercentToHex(ch) << shift) | result;
|
||||
shift -= 8;
|
||||
}
|
||||
} else {
|
||||
for (var ch in channels) {
|
||||
result = (_rgbChannelNumToHex(ch) << shift) | result;
|
||||
shift -= 8;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Creates [Color] instance from HSL(A) color value.
|
||||
int _hslToColor(String color) {
|
||||
var channels = _parseChannels(color)!;
|
||||
var result = 0xFF000000;
|
||||
var shift = 16;
|
||||
|
||||
if (channels.length == 4) {
|
||||
result = (_opacityChannelToHex(channels.removeLast()) << 24) & result;
|
||||
} else if (channels.length != 3) {
|
||||
throw FormatException(
|
||||
'Incorrect number of values in HSL color string, there must be 3 or 4 of them.',
|
||||
color,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Translate HSL to RGB according to CSS3 draft
|
||||
final h = double.parse(channels[0]) % 360 / 360;
|
||||
final s = _parsePercent(channels[1]) / 100;
|
||||
final l = _parsePercent(channels[2]) / 100;
|
||||
final m2 = l < 0.5 ? l * (s + 1) : l + s - l * s;
|
||||
final m1 = l * 2 - m2;
|
||||
final hexChannels = [
|
||||
_hueToRGB(m1, m2, h + 1 / 3),
|
||||
_hueToRGB(m1, m2, h),
|
||||
_hueToRGB(m1, m2, h - 1 / 3),
|
||||
];
|
||||
|
||||
for (var ch in hexChannels) {
|
||||
result = (ch << shift) | result;
|
||||
shift -= 8;
|
||||
}
|
||||
|
||||
return result;
|
||||
} on FormatException catch (e) {
|
||||
throw FormatException(
|
||||
'Incorrect format of HSL color string.',
|
||||
'${e.message} ${e.source}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts hue parameters of HSL to RGB channel hexadecimal integer form.
|
||||
int _hueToRGB(num m1, num m2, num h) {
|
||||
int result;
|
||||
|
||||
if (h < 0) {
|
||||
h = h + 1;
|
||||
} else if (h > 1) {
|
||||
h = h - 1;
|
||||
}
|
||||
|
||||
if (h * 6 < 1) {
|
||||
result = ((m1 + (m2 - m1) * h * 6) * 255).floor();
|
||||
} else if (h * 2 < 1) {
|
||||
result = (m2 * 255).floor();
|
||||
} else if (h * 3 < 2) {
|
||||
result = ((m1 + (m2 - m1) * (2 / 3 - h) * 6) * 255).floor();
|
||||
} else {
|
||||
result = (m1 * 255).floor();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// A map of X11 color keywords and their 8-digit hexadecimal forms.
|
||||
Map<String, int> colorKeywords = {
|
||||
"transparent": 0x00000000,
|
||||
"aliceblue": 0xFFF0F8FF,
|
||||
"antiquewhite": 0xFFFAEBD7,
|
||||
"aqua": 0xFF00FFFF,
|
||||
"aquamarine": 0xFF7FFFD4,
|
||||
"azure": 0xFFF0FFFF,
|
||||
"beige": 0xFFF5F5DC,
|
||||
"bisque": 0xFFFFE4C4,
|
||||
"black": 0xFF000000,
|
||||
"blanchedalmond": 0xFFFFEBCD,
|
||||
"blue": 0xFF0000FF,
|
||||
"blueviolet": 0xFF8A2BE2,
|
||||
"brown": 0xFFA52A2A,
|
||||
"burlywood": 0xFFDEB887,
|
||||
"cadetblue": 0xFF5F9EA0,
|
||||
"chartreuse": 0xFF7FFF00,
|
||||
"chocolate": 0xFFD2691E,
|
||||
"coral": 0xFFFF7F50,
|
||||
"cornflowerblue": 0xFF6495ED,
|
||||
"cornsilk": 0xFFFFF8DC,
|
||||
"crimson": 0xFFDC143C,
|
||||
"cyan": 0xFF00FFFF,
|
||||
"darkblue": 0xFF00008B,
|
||||
"darkcyan": 0xFF008B8B,
|
||||
"darkgoldenrod": 0xFFB8860B,
|
||||
"darkgray": 0xFFA9A9A9,
|
||||
"darkgreen": 0xFF006400,
|
||||
"darkgrey": 0xFFA9A9A9,
|
||||
"darkkhaki": 0xFFBDB76B,
|
||||
"darkmagenta": 0xFF8B008B,
|
||||
"darkolivegreen": 0xFF556B2F,
|
||||
"darkorange": 0xFFFF8C00,
|
||||
"darkorchid": 0xFF9932CC,
|
||||
"darkred": 0xFF8B0000,
|
||||
"darksalmon": 0xFFE9967A,
|
||||
"darkseagreen": 0xFF8FBC8F,
|
||||
"darkslateblue": 0xFF483D8B,
|
||||
"darkslategray": 0xFF2F4F4F,
|
||||
"darkslategrey": 0xFF2F4F4F,
|
||||
"darkturquoise": 0xFF00CED1,
|
||||
"darkviolet": 0xFF9400D3,
|
||||
"deeppink": 0xFFFF1493,
|
||||
"deepskyblue": 0xFF00BFFF,
|
||||
"dimgray": 0xFF696969,
|
||||
"dimgrey": 0xFF696969,
|
||||
"dodgerblue": 0xFF1E90FF,
|
||||
"firebrick": 0xFFB22222,
|
||||
"floralwhite": 0xFFFFFAF0,
|
||||
"forestgreen": 0xFF228B22,
|
||||
"fuchsia": 0xFFFF00FF,
|
||||
"gainsboro": 0xFFDCDCDC,
|
||||
"ghostwhite": 0xFFF8F8FF,
|
||||
"gold": 0xFFFFD700,
|
||||
"goldenrod": 0xFFDAA520,
|
||||
"gray": 0xFF808080,
|
||||
"green": 0xFF008000,
|
||||
"greenyellow": 0xFFADFF2F,
|
||||
"grey": 0xFF808080,
|
||||
"honeydew": 0xFFF0FFF0,
|
||||
"hotpink": 0xFFFF69B4,
|
||||
"indianred": 0xFFCD5C5C,
|
||||
"indigo": 0xFF4B0082,
|
||||
"ivory": 0xFFFFFFF0,
|
||||
"khaki": 0xFFF0E68C,
|
||||
"lavender": 0xFFE6E6FA,
|
||||
"lavenderblush": 0xFFFFF0F5,
|
||||
"lawngreen": 0xFF7CFC00,
|
||||
"lemonchiffon": 0xFFFFFACD,
|
||||
"lightblue": 0xFFADD8E6,
|
||||
"lightcoral": 0xFFF08080,
|
||||
"lightcyan": 0xFFE0FFFF,
|
||||
"lightgoldenrodyellow": 0xFFFAFAD2,
|
||||
"lightgray": 0xFFD3D3D3,
|
||||
"lightgreen": 0xFF90EE90,
|
||||
"lightgrey": 0xFFD3D3D3,
|
||||
"lightpink": 0xFFFFB6C1,
|
||||
"lightsalmon": 0xFFFFA07A,
|
||||
"lightseagreen": 0xFF20B2AA,
|
||||
"lightskyblue": 0xFF87CEFA,
|
||||
"lightslategray": 0xFF778899,
|
||||
"lightslategrey": 0xFF778899,
|
||||
"lightsteelblue": 0xFFB0C4DE,
|
||||
"lightyellow": 0xFFFFFFE0,
|
||||
"lime": 0xFF00FF00,
|
||||
"limegreen": 0xFF32CD32,
|
||||
"linen": 0xFFFAF0E6,
|
||||
"magenta": 0xFFFF00FF,
|
||||
"maroon": 0xFF800000,
|
||||
"mediumaquamarine": 0xFF66CDAA,
|
||||
"mediumblue": 0xFF0000CD,
|
||||
"mediumorchid": 0xFFBA55D3,
|
||||
"mediumpurple": 0xFF9370DB,
|
||||
"mediumseagreen": 0xFF3CB371,
|
||||
"mediumslateblue": 0xFF7B68EE,
|
||||
"mediumspringgreen": 0xFF00FA9A,
|
||||
"mediumturquoise": 0xFF48D1CC,
|
||||
"mediumvioletred": 0xFFC71585,
|
||||
"midnightblue": 0xFF191970,
|
||||
"mintcream": 0xFFF5FFFA,
|
||||
"mistyrose": 0xFFFFE4E1,
|
||||
"moccasin": 0xFFFFE4B5,
|
||||
"navajowhite": 0xFFFFDEAD,
|
||||
"navy": 0xFF000080,
|
||||
"oldlace": 0xFFFDF5E6,
|
||||
"olive": 0xFF808000,
|
||||
"olivedrab": 0xFF6B8E23,
|
||||
"orange": 0xFFFFA500,
|
||||
"orangered": 0xFFFF4500,
|
||||
"orchid": 0xFFDA70D6,
|
||||
"palegoldenrod": 0xFFEEE8AA,
|
||||
"palegreen": 0xFF98FB98,
|
||||
"paleturquoise": 0xFFAFEEEE,
|
||||
"palevioletred": 0xFFDB7093,
|
||||
"papayawhip": 0xFFFFEFD5,
|
||||
"peachpuff": 0xFFFFDAB9,
|
||||
"peru": 0xFFCD853F,
|
||||
"pink": 0xFFFFC0CB,
|
||||
"plum": 0xFFDDA0DD,
|
||||
"powderblue": 0xFFB0E0E6,
|
||||
"purple": 0xFF800080,
|
||||
"red": 0xFFFF0000,
|
||||
"rosybrown": 0xFFBC8F8F,
|
||||
"royalblue": 0xFF4169E1,
|
||||
"saddlebrown": 0xFF8B4513,
|
||||
"salmon": 0xFFFA8072,
|
||||
"sandybrown": 0xFFF4A460,
|
||||
"seagreen": 0xFF2E8B57,
|
||||
"seashell": 0xFFFFF5EE,
|
||||
"sienna": 0xFFA0522D,
|
||||
"silver": 0xFFC0C0C0,
|
||||
"skyblue": 0xFF87CEEB,
|
||||
"slateblue": 0xFF6A5ACD,
|
||||
"slategray": 0xFF708090,
|
||||
"slategrey": 0xFF708090,
|
||||
"snow": 0xFFFFFAFA,
|
||||
"springgreen": 0xFF00FF7F,
|
||||
"steelblue": 0xFF4682B4,
|
||||
"tan": 0xFFD2B48C,
|
||||
"teal": 0xFF008080,
|
||||
"thistle": 0xFFD8BFD8,
|
||||
"tomato": 0xFFFF6347,
|
||||
"turquoise": 0xFF40E0D0,
|
||||
"violet": 0xFFEE82EE,
|
||||
"wheat": 0xFFF5DEB3,
|
||||
"white": 0xFFFFFFFF,
|
||||
"whitesmoke": 0xFFF5F5F5,
|
||||
"yellow": 0xFFFFFF00,
|
||||
"yellowgreen": 0xFF9ACD32,
|
||||
};
|
133
lib/pkpass/utils/file_matcher.dart
Normal file
133
lib/pkpass/utils/file_matcher.dart
Normal file
|
@ -0,0 +1,133 @@
|
|||
import 'package:intl/locale.dart';
|
||||
|
||||
abstract class FileMatcher {
|
||||
const FileMatcher._();
|
||||
|
||||
static String? matchFile({
|
||||
required List<String> files,
|
||||
required String name,
|
||||
String extension = 'png',
|
||||
int scale = 1,
|
||||
Locale? locale,
|
||||
}) {
|
||||
final localized = matchLocale(files: files, name: 'logo', extension: 'png');
|
||||
final scaled = matchScale(files: localized, name: 'logo', extension: 'png');
|
||||
final file = files.singleWhere((element) => element == scaled);
|
||||
return file;
|
||||
}
|
||||
|
||||
static List<String> matchLocale({
|
||||
required List<String> files,
|
||||
Locale? locale,
|
||||
required String name,
|
||||
required String extension,
|
||||
}) {
|
||||
files.sort();
|
||||
files = files.reversed.toList();
|
||||
List<RegExp> expressions = <RegExp>[];
|
||||
|
||||
// adding the fallbacks
|
||||
// - match just *any* language
|
||||
// - match the five mostly spoken languages of the world, copied from Wikipedia
|
||||
expressions.addAll(
|
||||
[
|
||||
RegExp(
|
||||
'^([a-z]+(-[a-z]+)?\\.lproj\\/)?$name(@\\d+x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
),
|
||||
...['en', 'zh', 'hi', 'es', 'fr'].reversed.map(
|
||||
(language) => RegExp(
|
||||
'^$language(-[a-z]+)?\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (locale != null) {
|
||||
final language = locale.languageCode;
|
||||
expressions.add(
|
||||
RegExp(
|
||||
'^$language(-[a-z]+)?\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
),
|
||||
);
|
||||
|
||||
final region = locale.countryCode;
|
||||
if (region != null) {
|
||||
expressions.add(
|
||||
RegExp(
|
||||
'^$language-$region\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final regex in expressions.reversed) {
|
||||
final matches = <String>[];
|
||||
for (final file in files) {
|
||||
final match = regex.stringMatch(file);
|
||||
if (match != null) matches.add(match);
|
||||
}
|
||||
if (matches.isNotEmpty) return matches;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static String matchScale({
|
||||
required List<String> files,
|
||||
int scale = 1,
|
||||
required String name,
|
||||
required String extension,
|
||||
}) {
|
||||
files.sort();
|
||||
files = files.reversed.toList();
|
||||
|
||||
final regex = RegExp(
|
||||
'$name(@(\\d+)x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
int closestScale = 0;
|
||||
|
||||
final matches = <String>[];
|
||||
for (final file in files) {
|
||||
final match = regex.firstMatch(file);
|
||||
if (match != null) {
|
||||
String? group = match[2];
|
||||
if (group == null) {
|
||||
if (scale == 1) return match.input;
|
||||
group = '1';
|
||||
}
|
||||
|
||||
final matchedScale = int.parse(group);
|
||||
if (matchedScale == scale) return match.input;
|
||||
|
||||
if (matchedScale < scale) {
|
||||
if (closestScale < matchedScale) closestScale = matchedScale;
|
||||
} else if (closestScale < matchedScale) {
|
||||
closestScale = matchedScale;
|
||||
}
|
||||
|
||||
matches.add(match.input);
|
||||
}
|
||||
}
|
||||
final scaledMatches = matches
|
||||
.where(
|
||||
(element) => RegExp(
|
||||
'$name(@${closestScale}x)?\\.$extension\$',
|
||||
unicode: true,
|
||||
caseSensitive: false,
|
||||
).hasMatch(element),
|
||||
)
|
||||
.toList();
|
||||
scaledMatches.sort();
|
||||
return scaledMatches.last;
|
||||
}
|
||||
}
|
10
lib/pkpass/utils/lproj_parser.dart
Normal file
10
lib/pkpass/utils/lproj_parser.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
abstract class LProjParser {
|
||||
const LProjParser._();
|
||||
static Map<String, String> parse(String stringsFile) => Map.fromEntries(
|
||||
RegExp(r'"((?:\\"|[^"])*)"\s?=\s?"((?:\\"|[^"])*)"\s?;')
|
||||
.allMatches(stringsFile)
|
||||
.map(
|
||||
(match) => MapEntry(match.group(1)!, match.group(2)!),
|
||||
),
|
||||
);
|
||||
}
|
29
lib/pkpass/utils/mabe_decode.dart
Normal file
29
lib/pkpass/utils/mabe_decode.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue