diff --git a/CLAUDE.md b/CLAUDE.md index 987ae558..e19154ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,7 @@ When upgrading a toolkit, move all three platforms together where API surface ov - **Bridge serialization**: Readium-owned objects (`Locator`, `Decoration`, …) → JSON strings via `json.encode`; plugin-owned flat structures (preferences, action configs) → Maps. Rationale + Web-TS `.serialize()` rules: `docs/architecture.md#bridge-serialization`. - **Models**: hand-written `toJson`/`fromJson`. No `json_serializable`/`freezed`/build_runner codegen — don't reintroduce. - **PDF locator**: position = 1-based page in `Locator.locations.position` (matches upstream); don't invent plugin-side parallels to upstream models. Detail: `docs/api-reference/locator.md#pdf-locators`. +- **copyWith sentinel**: use the shared `const unset` from `utils/constants.dart` for parameters that default to "not set" — never declare a new `_unset = Object()`. Merge additional-properties via `AdditionalProperties.copyAdditionalProperties()` instead of inline merge logic. ### Android diff --git a/flutter_readium/example/ios/Podfile.lock b/flutter_readium/example/ios/Podfile.lock index 5b8dc68c..229dfe81 100644 --- a/flutter_readium/example/ios/Podfile.lock +++ b/flutter_readium/example/ios/Podfile.lock @@ -108,6 +108,6 @@ SPEC CHECKSUMS: SwiftSoup: 959c9ac70d7053fbf476341bfac90ab228523f2a wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 -PODFILE CHECKSUM: 2f4fb5dcab74326e2b88472463efb20c7535dda1 +PODFILE CHECKSUM: c4906ba94e31390c41a115760de24db9cfc2379e COCOAPODS: 1.16.2 diff --git a/flutter_readium_platform_interface/lib/src/extensions/readium_locations_extension.dart b/flutter_readium_platform_interface/lib/src/extensions/readium_locations_extension.dart index eba46692..8701b72d 100644 --- a/flutter_readium_platform_interface/lib/src/extensions/readium_locations_extension.dart +++ b/flutter_readium_platform_interface/lib/src/extensions/readium_locations_extension.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; import '../shared/index.dart'; import '../shared/publication.dart'; @@ -15,7 +16,7 @@ extension LocationExtension on Locations { if (fragment != null) fragment.fragment, ]; - return copyWith(fragments: newFragments.isEmpty ? null : newFragments); + return copyWith(fragments: newFragments); } Locations copyWithPhysicalPageNumber(final String? index) { @@ -24,7 +25,7 @@ extension LocationExtension on Locations { if (index != null) 'physicalPage=$index', ]; - return copyWith(fragments: newFragments.isEmpty ? null : newFragments); + return copyWith(fragments: newFragments); } Locations copyWithPage(final int? index) { @@ -33,7 +34,7 @@ extension LocationExtension on Locations { if (index != null) 'page=$index', ]; - return copyWith(fragments: newFragments.isEmpty ? null : newFragments); + return copyWith(fragments: newFragments); } /// Duration must be in seconds. @@ -44,7 +45,7 @@ extension LocationExtension on Locations { if (duration != null) 'duration=$duration', ]; - return copyWith(fragments: newFragments.isEmpty ? null : newFragments); + return copyWith(fragments: newFragments); } String? get physicalPage => fragments.firstWhereOrNull((final f) => f.startsWith('physicalPage='))?.split('=').last; @@ -64,6 +65,7 @@ extension LocationExtension on Locations { ); } +@immutable class TimeFragment { const TimeFragment({this.begin = Duration.zero, this.end}); diff --git a/flutter_readium_platform_interface/lib/src/reader/audio_recovery_policy.dart b/flutter_readium_platform_interface/lib/src/reader/audio_recovery_policy.dart index 5e767777..50375672 100644 --- a/flutter_readium_platform_interface/lib/src/reader/audio_recovery_policy.dart +++ b/flutter_readium_platform_interface/lib/src/reader/audio_recovery_policy.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../utils/constants.dart'; /// Configures the automatic audio-stream error recovery loop (retry attempts, /// backoff, and stall detection) shared by the iOS/Android/web audio @@ -59,15 +60,19 @@ class AudioRecoveryPolicy with Equatable { }; AudioRecoveryPolicy copyWith({ - int? maxAttempts, - double? backoffBaseSeconds, - double? stallTimeoutSeconds, - double? connectionTimeoutSeconds, + Object? maxAttempts = unset, + Object? backoffBaseSeconds = unset, + Object? stallTimeoutSeconds = unset, + Object? connectionTimeoutSeconds = unset, }) => AudioRecoveryPolicy( - maxAttempts: maxAttempts ?? this.maxAttempts, - backoffBaseSeconds: backoffBaseSeconds ?? this.backoffBaseSeconds, - stallTimeoutSeconds: stallTimeoutSeconds ?? this.stallTimeoutSeconds, - connectionTimeoutSeconds: connectionTimeoutSeconds ?? this.connectionTimeoutSeconds, + maxAttempts: identical(maxAttempts, unset) ? this.maxAttempts : (maxAttempts as int), + backoffBaseSeconds: identical(backoffBaseSeconds, unset) ? this.backoffBaseSeconds : (backoffBaseSeconds as double), + stallTimeoutSeconds: identical(stallTimeoutSeconds, unset) + ? this.stallTimeoutSeconds + : (stallTimeoutSeconds as double), + connectionTimeoutSeconds: identical(connectionTimeoutSeconds, unset) + ? this.connectionTimeoutSeconds + : (connectionTimeoutSeconds as double), ); @override diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_audio_preferences.dart b/flutter_readium_platform_interface/lib/src/reader/reader_audio_preferences.dart index 677b066a..55f3bc86 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_audio_preferences.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_audio_preferences.dart @@ -132,25 +132,33 @@ class AudioPreferences with Equatable implements JSONable { ]; AudioPreferences copyWith({ - double? volume, - double? speed, - double? pitch, - double? seekInterval, - bool? continuousSeeking, - bool? allowExternalSeeking, - double? updateIntervalSecs, - ControlPanelInfoType? controlPanelInfoType, - ControlPanelTimebase? controlPanelTimebase, + Object? volume = unset, + Object? speed = unset, + Object? pitch = unset, + Object? seekInterval = unset, + Object? continuousSeeking = unset, + Object? allowExternalSeeking = unset, + Object? updateIntervalSecs = unset, + Object? controlPanelInfoType = unset, + Object? controlPanelTimebase = unset, }) => AudioPreferences( - volume: volume ?? this.volume, - speed: speed ?? this.speed, - pitch: pitch ?? this.pitch, - seekInterval: seekInterval ?? this.seekInterval, - continuousSeeking: continuousSeeking ?? this.continuousSeeking, - allowExternalSeeking: allowExternalSeeking ?? this.allowExternalSeeking, - updateIntervalSecs: updateIntervalSecs ?? this.updateIntervalSecs, - controlPanelInfoType: controlPanelInfoType ?? this.controlPanelInfoType, - controlPanelTimebase: controlPanelTimebase ?? this.controlPanelTimebase, + volume: identical(volume, unset) ? this.volume : (volume as double?)!, + speed: identical(speed, unset) ? this.speed : (speed as double?)!, + pitch: identical(pitch, unset) ? this.pitch : (pitch as double?)!, + seekInterval: identical(seekInterval, unset) ? this.seekInterval : (seekInterval as double?)!, + continuousSeeking: identical(continuousSeeking, unset) ? this.continuousSeeking : (continuousSeeking as bool?), + allowExternalSeeking: identical(allowExternalSeeking, unset) + ? this.allowExternalSeeking + : (allowExternalSeeking as bool?), + updateIntervalSecs: identical(updateIntervalSecs, unset) + ? this.updateIntervalSecs + : (updateIntervalSecs as double?)!, + controlPanelInfoType: identical(controlPanelInfoType, unset) + ? this.controlPanelInfoType + : (controlPanelInfoType as ControlPanelInfoType?)!, + controlPanelTimebase: identical(controlPanelTimebase, unset) + ? this.controlPanelTimebase + : (controlPanelTimebase as ControlPanelTimebase?)!, ); } diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_decoration.dart b/flutter_readium_platform_interface/lib/src/reader/reader_decoration.dart index 74c2699b..5c214018 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_decoration.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_decoration.dart @@ -50,8 +50,11 @@ class ReaderDecoration implements JSONable { @override Map toJson() => {'id': id, 'locator': locator.toJson(), 'style': style.toJson()}; - ReaderDecoration copyWith({String? id, Locator? locator, ReaderDecorationStyle? style}) => - ReaderDecoration(id: id ?? this.id, locator: locator ?? this.locator, style: style ?? this.style); + ReaderDecoration copyWith({Object id = unset, Object locator = unset, Object style = unset}) => ReaderDecoration( + id: identical(id, unset) ? this.id : (id as String), + locator: identical(locator, unset) ? this.locator : (locator as Locator), + style: identical(style, unset) ? this.style : (style as ReaderDecorationStyle), + ); } class ReaderDecorationStyle implements JSONable { @@ -86,9 +89,10 @@ class ReaderDecorationStyle implements JSONable { isActive: map['isActive'] as bool? ?? false, ); - ReaderDecorationStyle copyWith({DecorationStyle? style, Color? tint, bool? isActive}) => ReaderDecorationStyle( - style: style ?? this.style, - tint: tint ?? this.tint, - isActive: isActive ?? this.isActive, - ); + ReaderDecorationStyle copyWith({Object style = unset, Object? tint = unset, Object isActive = unset}) => + ReaderDecorationStyle( + style: identical(style, unset) ? this.style : (style as DecorationStyle), + tint: identical(tint, unset) ? this.tint : tint as Color?, + isActive: identical(isActive, unset) ? this.isActive : (isActive as bool), + ); } diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_pdf_preferences.dart b/flutter_readium_platform_interface/lib/src/reader/reader_pdf_preferences.dart index 81d49273..682a0b24 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_pdf_preferences.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_pdf_preferences.dart @@ -84,21 +84,23 @@ class PDFPreferences with Equatable implements JSONable { ..putOpt('visibleScrollbar', visibleScrollbar); PDFPreferences copyWith({ - PDFLayout? layout, - PDFReadingProgression? readingProgression, - double? pageSpacing, - PDFFit? fit, - bool? offsetFirstPage, - PDFSpread? spread, - bool? visibleScrollbar, + Object? layout = unset, + Object? readingProgression = unset, + Object? pageSpacing = unset, + Object? fit = unset, + Object? offsetFirstPage = unset, + Object? spread = unset, + Object? visibleScrollbar = unset, }) => PDFPreferences( - layout: layout ?? this.layout, - readingProgression: readingProgression ?? this.readingProgression, - pageSpacing: pageSpacing ?? this.pageSpacing, - fit: fit ?? this.fit, - offsetFirstPage: offsetFirstPage ?? this.offsetFirstPage, - spread: spread ?? this.spread, - visibleScrollbar: visibleScrollbar ?? this.visibleScrollbar, + layout: identical(layout, unset) ? this.layout : (layout as PDFLayout?)!, + readingProgression: identical(readingProgression, unset) + ? this.readingProgression + : (readingProgression as PDFReadingProgression?)!, + pageSpacing: identical(pageSpacing, unset) ? this.pageSpacing : (pageSpacing as double?)!, + fit: identical(fit, unset) ? this.fit : (fit as PDFFit?)!, + offsetFirstPage: identical(offsetFirstPage, unset) ? this.offsetFirstPage : (offsetFirstPage as bool?), + spread: identical(spread, unset) ? this.spread : (spread as PDFSpread?), + visibleScrollbar: identical(visibleScrollbar, unset) ? this.visibleScrollbar : (visibleScrollbar as bool?), ); @override diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_tts_voice.dart b/flutter_readium_platform_interface/lib/src/reader/reader_tts_voice.dart index 1193a4db..4ed45237 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_tts_voice.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_tts_voice.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '../enums.dart'; +import '../utils/constants.dart'; import '../utils/jsonable.dart'; import '../utils/readium_log.dart'; import 'index.dart'; @@ -119,20 +120,20 @@ class ReaderTTSVoice with Equatable implements JSONable { ]; ReaderTTSVoice copyWith({ - String? identifier, - String? name, - String? language, - bool? networkRequired, - TTSVoiceGender? gender, - TTSVoiceQuality? quality, - bool? active, + Object identifier = unset, + Object name = unset, + Object language = unset, + Object networkRequired = unset, + Object gender = unset, + Object? quality = unset, + Object? active = unset, }) => ReaderTTSVoice( - identifier: identifier ?? this.identifier, - name: name ?? this.name, - language: language ?? this.language, - networkRequired: networkRequired ?? this.networkRequired, - gender: gender ?? this.gender, - quality: quality ?? this.quality, - active: active ?? this.active, + identifier: identical(identifier, unset) ? this.identifier : (identifier as String), + name: identical(name, unset) ? this.name : (name as String), + language: identical(language, unset) ? this.language : (language as String), + networkRequired: identical(networkRequired, unset) ? this.networkRequired : (networkRequired as bool), + gender: identical(gender, unset) ? this.gender : (gender as TTSVoiceGender), + quality: identical(quality, unset) ? this.quality : (quality as TTSVoiceQuality?), + active: identical(active, unset) ? this.active : (active as bool?), ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/facet.dart b/flutter_readium_platform_interface/lib/src/shared/opds/facet.dart index 27682169..9747acd9 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/facet.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/facet.dart @@ -6,6 +6,7 @@ import 'package:dartx/dartx.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../opds.dart' show OpdsMetadata; import '../publication/link.dart' show Link; @@ -23,8 +24,13 @@ class Facet with Equatable implements JSONable { @override String toString() => 'Facet{metadata: $metadata, links: $links}'; - Facet copyWith({OpdsMetadata? metadata, List? links}) => - Facet(metadata: metadata ?? this.metadata, links: links ?? this.links); + Facet copyWith({ + Object? metadata = unset, + Object? links = unset, + }) => Facet( + metadata: identical(metadata, unset) ? this.metadata : (metadata as OpdsMetadata), + links: identical(links, unset) ? this.links : (links as List), + ); @override Map toJson() { diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/feed.dart b/flutter_readium_platform_interface/lib/src/shared/opds/feed.dart index 2875302e..1bbad3ed 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/feed.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/feed.dart @@ -50,27 +50,25 @@ class Feed extends AdditionalProperties with Equatable implements JSONable { 'context: $context}'; Feed copyWith({ - OpdsMetadata? metadata, - List? links, - List? facets, - List? groups, - List? publications, - List? navigation, - List? context, - Map? additionalProperties, + Object metadata = unset, + Object links = unset, + Object facets = unset, + Object groups = unset, + Object publications = unset, + Object navigation = unset, + Object context = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Feed( - metadata: metadata ?? this.metadata, - links: links ?? this.links, - facets: facets ?? this.facets, - groups: groups ?? this.groups, - publications: publications ?? this.publications, - navigation: navigation ?? this.navigation, - context: context ?? this.context, + metadata: identical(metadata, unset) ? this.metadata : (metadata as OpdsMetadata), + links: identical(links, unset) ? this.links : (links as List), + facets: identical(facets, unset) ? this.facets : (facets as List), + groups: identical(groups, unset) ? this.groups : (groups as List), + publications: identical(publications, unset) ? this.publications : (publications as List), + navigation: identical(navigation, unset) ? this.navigation : (navigation as List), + context: identical(context, unset) ? this.context : (context as List), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/group.dart b/flutter_readium_platform_interface/lib/src/shared/opds/group.dart index a1f1f516..4223e1a2 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/group.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/group.dart @@ -7,6 +7,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../opds.dart'; import '../publication/link.dart'; @@ -34,15 +35,15 @@ class Group with Equatable implements JSONable { 'publications: $publications, navigation: $navigation}'; Group copyWith({ - OpdsMetadata? metadata, - List? links, - List? publications, - List? navigation, + Object metadata = unset, + Object links = unset, + Object publications = unset, + Object navigation = unset, }) => Group( - metadata: metadata ?? this.metadata, - links: links ?? this.links, - publications: publications ?? this.publications, - navigation: navigation ?? this.navigation, + metadata: identical(metadata, unset) ? this.metadata : (metadata as OpdsMetadata), + links: identical(links, unset) ? this.links : (links as List), + publications: identical(publications, unset) ? this.publications : (publications as List), + navigation: identical(navigation, unset) ? this.navigation : (navigation as List), ); @override diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/opds_authentication.dart b/flutter_readium_platform_interface/lib/src/shared/opds/opds_authentication.dart index ca406782..950ee2ea 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/opds_authentication.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/opds_authentication.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '../../utils/additional_properties.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../publication/link.dart'; @@ -211,41 +212,41 @@ class OpdsAuthentication extends AdditionalProperties with Equatable implements ..putJSONableIfNotEmpty('web_color_scheme', webColorScheme); OpdsAuthentication copyWith({ - String? type, - String? id, - String? description, - List? links, - List? announcements, - List? audiences, - Map? collectionSize, - String? colorScheme, - FeatureFlags? featureFlags, - InputData? inputs, - Map? labels, - PublicKeyData? publicKey, - String? serviceDescription, - WebColor? webColorScheme, - Map? additionalProperties, + Object type = unset, + Object id = unset, + Object? description = unset, + Object links = unset, + Object announcements = unset, + Object audiences = unset, + Object collectionSize = unset, + Object? colorScheme = unset, + Object? featureFlags = unset, + Object? inputs = unset, + Object? labels = unset, + Object? publicKey = unset, + Object? serviceDescription = unset, + Object? webColorScheme = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return OpdsAuthentication( - type: type ?? this.type, - id: id ?? this.id, - links: links ?? this.links, - description: description ?? this.description, - announcements: announcements ?? this.announcements, - audiences: audiences ?? this.audiences, - collectionSize: collectionSize ?? this.collectionSize, - colorScheme: colorScheme ?? this.colorScheme, - featureFlags: featureFlags ?? this.featureFlags, - inputs: inputs ?? this.inputs, - labels: labels ?? this.labels, - publicKey: publicKey ?? this.publicKey, - serviceDescription: serviceDescription ?? this.serviceDescription, - webColorScheme: webColorScheme ?? this.webColorScheme, + type: identical(type, unset) ? this.type : (type as String), + id: identical(id, unset) ? this.id : (id as String), + links: identical(links, unset) ? this.links : (links as List), + description: identical(description, unset) ? this.description : description as String?, + announcements: identical(announcements, unset) ? this.announcements : (announcements as List), + audiences: identical(audiences, unset) ? this.audiences : (audiences as List), + collectionSize: identical(collectionSize, unset) ? this.collectionSize : (collectionSize as Map), + colorScheme: identical(colorScheme, unset) ? this.colorScheme : colorScheme as String?, + featureFlags: identical(featureFlags, unset) ? this.featureFlags : (featureFlags as FeatureFlags?), + inputs: identical(inputs, unset) ? this.inputs : (inputs as InputData?), + labels: identical(labels, unset) ? this.labels : (labels as Map?), + publicKey: identical(publicKey, unset) ? this.publicKey : (publicKey as PublicKeyData?), + serviceDescription: identical(serviceDescription, unset) + ? this.serviceDescription + : serviceDescription as String?, + webColorScheme: identical(webColorScheme, unset) ? this.webColorScheme : (webColorScheme as WebColor?), additionalProperties: mergeProperties, ); } @@ -299,9 +300,9 @@ class OpdsAuthenticationFlow with Equatable implements JSONable { ..put('type', type) ..putIterableIfNotEmpty('links', links); - OpdsAuthenticationFlow copyWith({String? type, List? links}) => OpdsAuthenticationFlow( - type: type ?? this.type, - links: links ?? this.links, + OpdsAuthenticationFlow copyWith({Object type = unset, Object links = unset}) => OpdsAuthenticationFlow( + type: identical(type, unset) ? this.type : (type as String), + links: identical(links, unset) ? this.links : (links as List), ); @override @@ -328,9 +329,9 @@ class OpdsAuthenticationLabels with Equatable implements JSONable { ..putOpt('login', login) ..putOpt('password', password); - OpdsAuthenticationLabels copyWith({String? login, String? password}) => OpdsAuthenticationLabels( - login: login ?? this.login, - password: password ?? this.password, + OpdsAuthenticationLabels copyWith({Object? login = unset, Object? password = unset}) => OpdsAuthenticationLabels( + login: identical(login, unset) ? this.login : (login as String?)!, + password: identical(password, unset) ? this.password : (password as String?)!, ); @override @@ -368,17 +369,15 @@ class Announcement extends AdditionalProperties with Equatable implements JSONab ..put('content', content); Announcement copyWith({ - String? id, - String? content, - Map? additionalProperties, + Object id = unset, + Object content = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Announcement( - id: id ?? this.id, - content: content ?? this.content, + id: identical(id, unset) ? this.id : (id as String), + content: identical(content, unset) ? this.content : (content as String), additionalProperties: mergeProperties, ); } @@ -457,9 +456,9 @@ class FeatureFlags with Equatable implements JSONable { ..putIterableIfNotEmpty('enabled', enabled) ..putIterableIfNotEmpty('disabled', disabled); - FeatureFlags copyWith({List? enabled, List? disabled}) => FeatureFlags( - enabled: enabled ?? this.enabled, - disabled: disabled ?? this.disabled, + FeatureFlags copyWith({Object enabled = unset, Object disabled = unset}) => FeatureFlags( + enabled: identical(enabled, unset) ? this.enabled : (enabled as List), + disabled: identical(disabled, unset) ? this.disabled : (disabled as List), ); @override @@ -492,9 +491,9 @@ class InputField with Equatable implements JSONable { ..putOpt('keyboard', keyboard?.name) ..putOpt('maximum_length', maximumLength); - InputField copyWith({KeyboardType? keyboard, int? maximumLength}) => InputField( - keyboard: keyboard ?? this.keyboard, - maximumLength: maximumLength ?? this.maximumLength, + InputField copyWith({Object? keyboard = unset, Object? maximumLength = unset}) => InputField( + keyboard: identical(keyboard, unset) ? this.keyboard : keyboard as KeyboardType?, + maximumLength: identical(maximumLength, unset) ? this.maximumLength : maximumLength as int?, ); @override @@ -556,13 +555,13 @@ class LoginInputField extends InputField with Equatable { @override LoginInputField copyWith({ - String? barcodeFormat, - KeyboardType? keyboard, - int? maximumLength, + Object? barcodeFormat = unset, + Object? keyboard = unset, + Object? maximumLength = unset, }) => LoginInputField( - barcodeFormat: barcodeFormat ?? this.barcodeFormat, - keyboard: keyboard ?? this.keyboard, - maximumLength: maximumLength ?? this.maximumLength, + barcodeFormat: identical(barcodeFormat, unset) ? this.barcodeFormat : barcodeFormat as String?, + keyboard: identical(keyboard, unset) ? this.keyboard : keyboard as KeyboardType?, + maximumLength: identical(maximumLength, unset) ? this.maximumLength : maximumLength as int?, ); @override @@ -598,9 +597,9 @@ class InputData with Equatable implements JSONable { ..put('login', login) ..put('password', password); - InputData copyWith({LoginInputField? login, InputField? password}) => InputData( - login: login ?? this.login, - password: password ?? this.password, + InputData copyWith({Object login = unset, Object password = unset}) => InputData( + login: identical(login, unset) ? this.login : (login as LoginInputField), + password: identical(password, unset) ? this.password : (password as InputField), ); @override @@ -631,8 +630,10 @@ class PublicKeyData with Equatable implements JSONable { ..put('type', type) ..put('value', value); - PublicKeyData copyWith({String? type, String? value}) => - PublicKeyData(type: type ?? this.type, value: value ?? this.value); + PublicKeyData copyWith({Object type = unset, Object value = unset}) => PublicKeyData( + type: identical(type, unset) ? this.type : (type as String), + value: identical(value, unset) ? this.value : (value as String), + ); @override List get props => [type, value]; @@ -671,9 +672,9 @@ class WebColor with Equatable implements JSONable { ..putOpt('primary', primary) ..putOpt('secondary', secondary); - WebColor copyWith({String? primary, String? secondary}) => WebColor( - primary: primary ?? this.primary, - secondary: secondary ?? this.secondary, + WebColor copyWith({Object primary = unset, Object secondary = unset}) => WebColor( + primary: identical(primary, unset) ? this.primary : (primary as String), + secondary: identical(secondary, unset) ? this.secondary : (secondary as String), ); @override diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/opds_metadata.dart b/flutter_readium_platform_interface/lib/src/shared/opds/opds_metadata.dart index a189e4cb..ff9e0cf7 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/opds_metadata.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/opds_metadata.dart @@ -55,33 +55,33 @@ class OpdsMetadata extends AdditionalProperties with Equatable implements JSONab ]; OpdsMetadata copyWith({ - LocalizedString? localizedTitle, - LocalizedString? localizedSubtitle, - String? identifier, - String? description, - int? numberOfItems, - int? itemsPerPage, - int? currentPage, - DateTime? modified, - double? position, - String? rdfType, - Map? additionalProperties, + Object localizedTitle = unset, + Object? localizedSubtitle = unset, + Object? identifier = unset, + Object? description = unset, + Object? numberOfItems = unset, + Object? itemsPerPage = unset, + Object? currentPage = unset, + Object? modified = unset, + Object? position = unset, + Object? rdfType = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return OpdsMetadata( - localizedTitle: localizedTitle ?? this.localizedTitle, - localizedSubtitle: localizedSubtitle ?? this.localizedSubtitle, - identifier: identifier ?? this.identifier, - description: description ?? this.description, - numberOfItems: numberOfItems ?? this.numberOfItems, - itemsPerPage: itemsPerPage ?? this.itemsPerPage, - currentPage: currentPage ?? this.currentPage, - modified: modified ?? this.modified, - position: position ?? this.position, - rdfType: rdfType ?? this.rdfType, + localizedTitle: identical(localizedTitle, unset) ? this.localizedTitle : (localizedTitle as LocalizedString), + localizedSubtitle: identical(localizedSubtitle, unset) + ? this.localizedSubtitle + : localizedSubtitle as LocalizedString?, + identifier: identical(identifier, unset) ? this.identifier : identifier as String?, + description: identical(description, unset) ? this.description : description as String?, + numberOfItems: identical(numberOfItems, unset) ? this.numberOfItems : numberOfItems as int?, + itemsPerPage: identical(itemsPerPage, unset) ? this.itemsPerPage : itemsPerPage as int?, + currentPage: identical(currentPage, unset) ? this.currentPage : currentPage as int?, + modified: identical(modified, unset) ? this.modified : modified as DateTime?, + position: identical(position, unset) ? this.position : position as double?, + rdfType: identical(rdfType, unset) ? this.rdfType : rdfType as String?, additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/opds/opds_publication.dart b/flutter_readium_platform_interface/lib/src/shared/opds/opds_publication.dart index 34d863d1..5f41a91e 100644 --- a/flutter_readium_platform_interface/lib/src/shared/opds/opds_publication.dart +++ b/flutter_readium_platform_interface/lib/src/shared/opds/opds_publication.dart @@ -12,13 +12,13 @@ class OpdsPublication implements JSONable { final List images; OpdsPublication copyWith({ - OpdsMetadata? metadata, - List? links, - List? images, + Object? metadata = unset, + Object? links = unset, + Object? images = unset, }) => OpdsPublication( - metadata ?? this.metadata, - links ?? this.links, - images: images ?? this.images, + identical(metadata, unset) ? this.metadata : (metadata as OpdsMetadata?)!, + identical(links, unset) ? this.links : (links as List?)!, + images: identical(images, unset) ? this.images : (images as List?)!, ); @override diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/localized_string.dart b/flutter_readium_platform_interface/lib/src/shared/publication/localized_string.dart index 615b63a1..d393e0d3 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/localized_string.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/localized_string.dart @@ -8,6 +8,7 @@ import 'package:dfunc/dfunc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../../utils/readium_log.dart'; @@ -143,8 +144,9 @@ class LocalizedString with Equatable implements JSONable { ), ); - LocalizedString copyWith({Map? translations}) => - LocalizedString(translations: translations ?? {}); + LocalizedString copyWith({Object? translations = unset}) => LocalizedString( + translations: identical(translations, unset) ? const {} : (translations ?? const {}) as Map, + ); /// Serializes a [LocalizedString] to its RWPM JSON representation. @override diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart b/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart index c214a45a..be1cd6c4 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import '../../extensions/readium_string_extensions.dart'; import '../../extensions/strings.dart'; import '../../utils/additional_properties.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../../utils/readium_log.dart'; import '../../utils/take.dart'; @@ -152,41 +153,43 @@ class Locator extends AdditionalProperties with Equatable implements JSONable { ..putJSONableIfNotEmpty('text', text); Locator copyWith({ - String? href, - String? type, - String? title, - Locations? locations, - LocatorText? text, - Map? additionalProperties, + Object href = unset, + Object type = unset, + Object? title = unset, + Object? locations = unset, + Object? text = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Locator( - href: href ?? this.href, - type: type ?? this.type, - title: title ?? this.title, - locations: locations ?? this.locations, - text: text ?? this.text, + href: identical(href, unset) ? this.href : (href as String), + type: identical(type, unset) ? this.type : (type as String), + title: identical(title, unset) ? this.title : title as String?, + locations: identical(locations, unset) ? this.locations : (locations as Locations), + text: identical(text, unset) ? this.text : (text as LocatorText), additionalProperties: mergeProperties, ); } /// Shortcut to get a copy of the [Locator] with different [Locations] sub-properties. Locator copyWithLocations({ - List? fragments, - double? progression = _emptyDoubleValue, - int? position = _emptyIntValue, - double? totalProgression = _emptyDoubleValue, - Map? otherLocations, + Object fragments = unset, + Object? progression = unset, + Object? position = unset, + Object? totalProgression = unset, + Object? additionalProperties = unset, }) => copyWith( locations: (locations ?? Locations()).copyWith( - fragments: fragments ?? locations?.fragments, - progression: progression.check(locations?.progression), - position: position.check(locations?.position), - totalProgression: totalProgression.check(locations?.totalProgression), - additionalProperties: otherLocations ?? locations?.additionalProperties, + fragments: identical(fragments, unset) ? (locations?.fragments ?? const []) : (fragments as List), + progression: identical(progression, unset) ? locations?.progression : (progression as double?), + position: identical(position, unset) ? locations?.position : (position as int?), + totalProgression: identical(totalProgression, unset) + ? locations?.totalProgression + : (totalProgression as double?), + additionalProperties: identical(additionalProperties, unset) || additionalProperties == null + ? locations?.additionalProperties + : (additionalProperties as Map?), ), ); @@ -221,9 +224,9 @@ class Locator extends AdditionalProperties with Equatable implements JSONable { return copyWith( // Makes sure href only contains /path. href: hrefPath, - type: MediaType.html.name, + type: MediaType.html.toString(), locations: locations?.copyWith( - fragments: idFragment == null ? null : [idFragment], + fragments: idFragment == null ? [] : [idFragment], ), ); } @@ -312,27 +315,25 @@ class Locations extends AdditionalProperties with Equatable implements JSONable final String? partialCfi; Locations copyWith({ - int? position = _emptyIntValue, - double? progression = _emptyDoubleValue, - double? totalProgression = _emptyDoubleValue, - List? fragments, - Map? additionalProperties, - String? cssSelector, - DomRange? domRange, - String? partialCfi, + Object? position = unset, + Object? progression = unset, + Object? totalProgression = unset, + Object fragments = unset, + Object? additionalProperties = unset, + Object? cssSelector = unset, + Object? domRange = unset, + Object? partialCfi = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Locations( - progression: progression.check(this.progression), - position: position.check(this.position), - totalProgression: totalProgression.check(this.totalProgression), - fragments: fragments ?? this.fragments, - cssSelector: cssSelector ?? this.cssSelector, - domRange: domRange ?? this.domRange, - partialCfi: partialCfi ?? this.partialCfi, + progression: identical(progression, unset) ? this.progression : (progression as double?), + position: identical(position, unset) ? this.position : (position as int?), + totalProgression: identical(totalProgression, unset) ? this.totalProgression : (totalProgression as double?), + fragments: identical(fragments, unset) ? this.fragments : (fragments as List), + cssSelector: identical(cssSelector, unset) ? this.cssSelector : (cssSelector as String?), + domRange: identical(domRange, unset) ? this.domRange : (domRange as DomRange?), + partialCfi: identical(partialCfi, unset) ? this.partialCfi : (partialCfi as String?), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/locator_collection.dart b/flutter_readium_platform_interface/lib/src/shared/publication/locator_collection.dart index 0a571ec6..11dbc738 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/locator_collection.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/locator_collection.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '../../utils/additional_properties.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import 'link.dart'; import 'locator.dart'; @@ -65,13 +66,13 @@ class LocatorCollection with Equatable implements JSONable { } LocatorCollection copyWith({ - LocatorCollectionMetadata? metadata, - List? links, - List? locators, + Object metadata = unset, + Object links = unset, + Object locators = unset, }) => LocatorCollection( - metadata: metadata ?? this.metadata, - links: links ?? this.links, - locators: locators ?? this.locators, + metadata: identical(metadata, unset) ? this.metadata : (metadata as LocatorCollectionMetadata), + links: identical(links, unset) ? this.links : (links as List), + locators: identical(locators, unset) ? this.locators : (locators as List), ); @override @@ -154,13 +155,13 @@ class LocatorCollectionMetadata extends AdditionalProperties with Equatable impl } LocatorCollectionMetadata copyWith({ - dynamic localizedTitle, - int? numberOfItems, - Map? additionalProperties, + Object? localizedTitle = unset, + Object? numberOfItems = unset, + Object? additionalProperties = unset, }) => LocatorCollectionMetadata( - localizedTitle: localizedTitle ?? this.localizedTitle, - numberOfItems: numberOfItems ?? this.numberOfItems, - additionalProperties: additionalProperties ?? this.additionalProperties, + localizedTitle: identical(localizedTitle, unset) ? this.localizedTitle : localizedTitle, + numberOfItems: identical(numberOfItems, unset) ? this.numberOfItems : (numberOfItems as int?), + additionalProperties: copyAdditionalProperties(additionalProperties: additionalProperties), ); @override diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/chapter.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/chapter.dart index e7eaea00..6559aa0a 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/chapter.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/chapter.dart @@ -71,27 +71,25 @@ class Chapter extends BaseCollection { final List series; Chapter copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - List? series, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object series = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Chapter( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, - series: series ?? this.series, + position: identical(position, unset) ? this.position : (position as double?), + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?), + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?), + altIdentifiers: identical(altIdentifiers, unset) ? this.altIdentifiers : (altIdentifiers as List?), + localizedSortAs: identical(localizedSortAs, unset) ? this.localizedSortAs : (localizedSortAs as LocalizedString?), + links: identical(links, unset) ? this.links : (links as List?), + series: identical(series, unset) ? this.series : (series as List), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/collection.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/collection.dart index ba20b54b..997e736f 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/collection.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/collection.dart @@ -81,25 +81,27 @@ class Collection extends BaseCollection { } Collection copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Collection( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, + position: identical(position, unset) ? this.position : (position as double?)!, + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?)!, + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?)!, + altIdentifiers: identical(altIdentifiers, unset) + ? this.altIdentifiers + : (altIdentifiers as List?)!, + localizedSortAs: identical(localizedSortAs, unset) + ? this.localizedSortAs + : (localizedSortAs as LocalizedString?)!, + links: identical(links, unset) ? this.links : (links as List?)!, additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/contributor.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/contributor.dart index 27afc84d..d1b8f208 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/contributor.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/contributor.dart @@ -86,27 +86,29 @@ class Contributor extends BaseCollection { } Contributor copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - List? roles, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object? roles = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Contributor( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, - roles: roles ?? this.roles, + position: identical(position, unset) ? this.position : (position as double?)!, + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?)!, + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?)!, + altIdentifiers: identical(altIdentifiers, unset) + ? this.altIdentifiers + : (altIdentifiers as List?)!, + localizedSortAs: identical(localizedSortAs, unset) + ? this.localizedSortAs + : (localizedSortAs as LocalizedString?)!, + links: identical(links, unset) ? this.links : (links as List?)!, + roles: identical(roles, unset) ? this.roles : (roles as List?)!, additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/episode.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/episode.dart index 1a58c865..699bebda 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/episode.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/episode.dart @@ -87,25 +87,23 @@ class Episode extends BaseCollection { } Episode copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Episode( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, + position: identical(position, unset) ? this.position : (position as double?), + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?), + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?), + altIdentifiers: identical(altIdentifiers, unset) ? this.altIdentifiers : (altIdentifiers as List?), + localizedSortAs: identical(localizedSortAs, unset) ? this.localizedSortAs : (localizedSortAs as LocalizedString?), + links: identical(links, unset) ? this.links : (links as List?), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/issue.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/issue.dart index 29c1c234..067691bc 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/issue.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/issue.dart @@ -80,29 +80,27 @@ class Issue extends BaseCollection { final List chapters; Issue copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - List
? articles, - List? chapters, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object articles = unset, + Object chapters = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Issue( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, - articles: articles ?? this.articles, - chapters: chapters ?? this.chapters, + position: identical(position, unset) ? this.position : (position as double?), + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?), + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?), + altIdentifiers: identical(altIdentifiers, unset) ? this.altIdentifiers : (altIdentifiers as List?), + localizedSortAs: identical(localizedSortAs, unset) ? this.localizedSortAs : (localizedSortAs as LocalizedString?), + links: identical(links, unset) ? this.links : (links as List?), + articles: identical(articles, unset) ? this.articles : (articles as List
), + chapters: identical(chapters, unset) ? this.chapters : (chapters as List), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/periodical.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/periodical.dart index cf5de74d..5bb8dc1d 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/periodical.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/periodical.dart @@ -91,29 +91,31 @@ class Periodical extends BaseCollection { } Periodical copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - List? issues, - List? volumes, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object? issues = unset, + Object? volumes = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Periodical( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, - issues: issues ?? this.issues, - volumes: volumes ?? this.volumes, + position: identical(position, unset) ? this.position : (position as double?)!, + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?)!, + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?)!, + altIdentifiers: identical(altIdentifiers, unset) + ? this.altIdentifiers + : (altIdentifiers as List?)!, + localizedSortAs: identical(localizedSortAs, unset) + ? this.localizedSortAs + : (localizedSortAs as LocalizedString?)!, + links: identical(links, unset) ? this.links : (links as List?)!, + issues: identical(issues, unset) ? this.issues : (issues as List?)!, + volumes: identical(volumes, unset) ? this.volumes : (volumes as List?)!, additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/season.dart b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/season.dart index d751aa22..97e64e4c 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/metadata/season.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/metadata/season.dart @@ -96,27 +96,25 @@ class Season extends BaseCollection { } Season copyWith({ - double? position, - LocalizedString? localizedName, - String? identifier, - List? altIdentifiers, - LocalizedString? localizedSortAs, - List? links, - List? episodes, - Map? additionalProperties, + Object? position = unset, + Object? localizedName = unset, + Object? identifier = unset, + Object? altIdentifiers = unset, + Object? localizedSortAs = unset, + Object? links = unset, + Object episodes = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Season( - position: position ?? this.position, - localizedName: localizedName ?? this.localizedName, - identifier: identifier ?? this.identifier, - altIdentifiers: altIdentifiers ?? this.altIdentifiers, - localizedSortAs: localizedSortAs ?? this.localizedSortAs, - links: links ?? this.links, - episodes: episodes ?? this.episodes, + position: identical(position, unset) ? this.position : (position as double?), + localizedName: identical(localizedName, unset) ? this.localizedName : (localizedName as LocalizedString?), + identifier: identical(identifier, unset) ? this.identifier : (identifier as String?), + altIdentifiers: identical(altIdentifiers, unset) ? this.altIdentifiers : (altIdentifiers as List?), + localizedSortAs: identical(localizedSortAs, unset) ? this.localizedSortAs : (localizedSortAs as LocalizedString?), + links: identical(links, unset) ? this.links : (links as List?), + episodes: identical(episodes, unset) ? this.episodes : (episodes as List), additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/properties.dart b/flutter_readium_platform_interface/lib/src/shared/publication/properties.dart index 2f48099e..e5d9b02a 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/properties.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/properties.dart @@ -8,6 +8,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '../../utils/additional_properties.dart'; +import '../../utils/constants.dart'; import '../../utils/jsonable.dart'; import '../publication.dart'; @@ -78,27 +79,25 @@ class Properties extends AdditionalProperties with Equatable implements JSONable ..putOpt('encryption', encryption); Properties copyWith({ - PresentationPage? page, - List? contains, - PresentationOrientation? orientation, - EpubLayout? layout, - PresentationOverflow? overflow, - PresentationSpread? spread, - Encryption? encryption, - Map? additionalProperties, + Object? page = unset, + Object? contains = unset, + Object? orientation = unset, + Object? layout = unset, + Object? overflow = unset, + Object? spread = unset, + Object? encryption = unset, + Object? additionalProperties = unset, }) { - final mergeProperties = Map.of(this.additionalProperties) - ..addAll(additionalProperties ?? {}) - ..removeWhere((key, value) => value == null); + final mergeProperties = copyAdditionalProperties(additionalProperties: additionalProperties); return Properties( - page: page ?? this.page, - contains: contains?.toSet().toList() ?? this.contains, - orientation: orientation ?? this.orientation, - layout: layout ?? this.layout, - overflow: overflow ?? this.overflow, - spread: spread ?? this.spread, - encryption: encryption ?? this.encryption, + page: identical(page, unset) ? this.page : (page as PresentationPage?)!, + contains: identical(contains, unset) ? this.contains : (contains as List?)?.toSet().toList(), + orientation: identical(orientation, unset) ? this.orientation : (orientation as PresentationOrientation?)!, + layout: identical(layout, unset) ? this.layout : (layout as EpubLayout?)!, + overflow: identical(overflow, unset) ? this.overflow : (overflow as PresentationOverflow?)!, + spread: identical(spread, unset) ? this.spread : (spread as PresentationSpread?)!, + encryption: identical(encryption, unset) ? this.encryption : (encryption as Encryption?)!, additionalProperties: mergeProperties, ); } diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart b/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart index 1d06bff9..b948a6ad 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart @@ -61,21 +61,23 @@ class Publication with Equatable implements JSONable { /// Returns a copy of this publication with the given fields replaced. Publication copyWith({ - List? context, - Metadata? metadata, - List? links, - List? readingOrder, - List? resources, - List? tableOfContents, - Map>? subCollections, + Object context = unset, + Object metadata = unset, + Object links = unset, + Object readingOrder = unset, + Object resources = unset, + Object tableOfContents = unset, + Object subCollections = unset, }) => Publication( - context: context ?? this.context, - metadata: metadata ?? this.metadata, - links: links ?? this.links, - readingOrder: readingOrder ?? this.readingOrder, - resources: resources ?? this.resources, - tableOfContents: tableOfContents ?? this.tableOfContents, - subCollections: subCollections ?? this.subCollections, + context: identical(context, unset) ? this.context : (context as List), + metadata: identical(metadata, unset) ? this.metadata : (metadata as Metadata), + links: identical(links, unset) ? this.links : (links as List), + readingOrder: identical(readingOrder, unset) ? this.readingOrder : (readingOrder as List), + resources: identical(resources, unset) ? this.resources : (resources as List), + tableOfContents: identical(tableOfContents, unset) ? this.tableOfContents : (tableOfContents as List), + subCollections: identical(subCollections, unset) + ? this.subCollections + : (subCollections as Map>), ); @override diff --git a/flutter_readium_platform_interface/lib/src/timebased_state.dart b/flutter_readium_platform_interface/lib/src/timebased_state.dart index 11baed45..6e2702bd 100644 --- a/flutter_readium_platform_interface/lib/src/timebased_state.dart +++ b/flutter_readium_platform_interface/lib/src/timebased_state.dart @@ -2,6 +2,7 @@ import 'package:meta/meta.dart'; import 'enums.dart'; import 'shared/publication/locator.dart'; +import 'utils/constants.dart'; import 'utils/jsonable.dart'; @immutable @@ -116,20 +117,22 @@ class ReadiumTimebasedState implements JSONable { ..putOpt('currentLocator', currentLocator?.toJson()); ReadiumTimebasedState copyWith({ - TimebasedState? state, - Duration? currentOffset, - Duration? currentBuffered, - Duration? currentDuration, - Duration? totalProgressDuration, - Duration? totalDuration, - Locator? currentLocator, + Object state = unset, + Object? currentOffset = unset, + Object? currentBuffered = unset, + Object? currentDuration = unset, + Object? totalProgressDuration = unset, + Object? totalDuration = unset, + Object? currentLocator = unset, }) => ReadiumTimebasedState( - state: state ?? this.state, - currentOffset: currentOffset ?? this.currentOffset, - currentBuffered: currentBuffered ?? this.currentBuffered, - currentDuration: currentDuration ?? this.currentDuration, - totalProgressDuration: totalProgressDuration ?? this.totalProgressDuration, - totalDuration: totalDuration ?? this.totalDuration, - currentLocator: currentLocator ?? this.currentLocator, + state: identical(state, unset) ? this.state : (state as TimebasedState), + currentOffset: identical(currentOffset, unset) ? this.currentOffset : (currentOffset as Duration?), + currentBuffered: identical(currentBuffered, unset) ? this.currentBuffered : (currentBuffered as Duration?), + currentDuration: identical(currentDuration, unset) ? this.currentDuration : (currentDuration as Duration?), + totalProgressDuration: identical(totalProgressDuration, unset) + ? this.totalProgressDuration + : (totalProgressDuration as Duration?), + totalDuration: identical(totalDuration, unset) ? this.totalDuration : (totalDuration as Duration?), + currentLocator: identical(currentLocator, unset) ? this.currentLocator : (currentLocator as Locator?), ); } diff --git a/flutter_readium_platform_interface/lib/src/utils/additional_properties.dart b/flutter_readium_platform_interface/lib/src/utils/additional_properties.dart index bc23f28a..8eb0d510 100644 --- a/flutter_readium_platform_interface/lib/src/utils/additional_properties.dart +++ b/flutter_readium_platform_interface/lib/src/utils/additional_properties.dart @@ -1,5 +1,7 @@ import 'package:collection/collection.dart'; +import 'constants.dart'; + abstract class AdditionalProperties { const AdditionalProperties({this.additionalProperties = const {}}); @@ -35,4 +37,18 @@ abstract class AdditionalProperties { } return null; } + + /// Returns a new [Map] with the merged additional properties. + Map copyAdditionalProperties({ + Object? additionalProperties = unset, + }) { + if (identical(additionalProperties, unset) || additionalProperties == null) { + return this.additionalProperties; + } + final result = {} + ..addAll(this.additionalProperties) + ..addAll(additionalProperties as Map) + ..removeWhere((key, value) => value == null); + return result; + } } diff --git a/flutter_readium_platform_interface/lib/src/utils/constants.dart b/flutter_readium_platform_interface/lib/src/utils/constants.dart new file mode 100644 index 00000000..005f5c0d --- /dev/null +++ b/flutter_readium_platform_interface/lib/src/utils/constants.dart @@ -0,0 +1,2 @@ +/// Sentinel value used to distinguish "not set" from null in copyWith methods. +const unset = Object(); diff --git a/flutter_readium_platform_interface/lib/src/utils/index.dart b/flutter_readium_platform_interface/lib/src/utils/index.dart index a43c6300..6cde4245 100644 --- a/flutter_readium_platform_interface/lib/src/utils/index.dart +++ b/flutter_readium_platform_interface/lib/src/utils/index.dart @@ -1,4 +1,5 @@ export 'additional_properties.dart'; +export 'constants.dart'; export 'href.dart'; export 'jsonable.dart'; export 'readium_log.dart'; diff --git a/flutter_readium_platform_interface/test/models_test.dart b/flutter_readium_platform_interface/test/models_test.dart index ac44010a..69a65456 100644 --- a/flutter_readium_platform_interface/test/models_test.dart +++ b/flutter_readium_platform_interface/test/models_test.dart @@ -56,6 +56,33 @@ void main() { expect(restored?.href, '/ch.xhtml'); expect(restored?.type, 'application/xhtml+xml'); }); + + test('copyWith() with no args preserves all fields', () { + final locator = Locator( + href: '/ch.xhtml', + type: 'application/xhtml+xml', + title: 'Chapter 1', + locations: Locations(progression: 0.5), + text: LocatorText(before: 'a', highlight: 'b', after: 'c'), + ); + final copied = locator.copyWith(); + expect(copied.href, '/ch.xhtml'); + expect(copied.type, 'application/xhtml+xml'); + expect(copied.title, 'Chapter 1'); + expect(copied.locations?.progression, closeTo(0.5, 1e-6)); + expect(copied.text?.before, 'a'); + }); + + test('copyWith() overrides only specified fields', () { + final locator = Locator( + href: '/ch.xhtml', + type: 'application/xhtml+xml', + title: 'Chapter 1', + ); + final updated = locator.copyWith(title: 'Updated'); + expect(updated.href, '/ch.xhtml'); + expect(updated.title, 'Updated'); + }); }); // --------------------------------------------------------------------------- @@ -98,6 +125,26 @@ void main() { expect(copied.preventMOColumnBreaks, isTrue); }); + test('copyWith() with no args preserves all fields', () { + const prefs = EPUBPreferences( + preventMOColumnBreaks: true, + spread: 'both', + ); + final copied = prefs.copyWith(); + expect(copied.preventMOColumnBreaks, isTrue); + expect(copied.spread, 'both'); + }); + + test('copyWith() overrides only specified fields', () { + const prefs = EPUBPreferences( + preventMOColumnBreaks: false, + spread: 'both', + ); + final updated = prefs.copyWith(preventMOColumnBreaks: true); + expect(updated.preventMOColumnBreaks, isTrue); + expect(updated.spread, 'both'); + }); + test('equality distinguishes preventMOColumnBreaks values', () { const a = EPUBPreferences(preventMOColumnBreaks: true); const b = EPUBPreferences(preventMOColumnBreaks: false); @@ -130,6 +177,30 @@ void main() { expect(restored.pageSpacing, 8.0); expect(restored.fit, PDFFit.auto); }); + + test('copyWith() with no args preserves all fields', () { + const prefs = PDFPreferences( + layout: PDFLayout.scrollVertical, + readingProgression: PDFReadingProgression.rtl, + pageSpacing: 12.5, + fit: PDFFit.page, + ); + final copied = prefs.copyWith(); + expect(copied.layout, PDFLayout.scrollVertical); + expect(copied.readingProgression, PDFReadingProgression.rtl); + expect(copied.pageSpacing, 12.5); + expect(copied.fit, PDFFit.page); + }); + + test('copyWith() overrides only specified fields', () { + const prefs = PDFPreferences( + layout: PDFLayout.scrollVertical, + pageSpacing: 12.5, + ); + final updated = prefs.copyWith(pageSpacing: 20.0); + expect(updated.layout, PDFLayout.scrollVertical); + expect(updated.pageSpacing, 20.0); + }); }); // --------------------------------------------------------------------------- @@ -624,6 +695,20 @@ void main() { expect(updated.stallTimeoutSeconds, 10.0); }); + test('copyWith() with no args preserves all fields', () { + const policy = AudioRecoveryPolicy( + maxAttempts: 5, + backoffBaseSeconds: 2.0, + stallTimeoutSeconds: 30.0, + connectionTimeoutSeconds: 10.0, + ); + final copied = policy.copyWith(); + expect(copied.maxAttempts, 5); + expect(copied.backoffBaseSeconds, 2.0); + expect(copied.stallTimeoutSeconds, 30.0); + expect(copied.connectionTimeoutSeconds, 10.0); + }); + test('equality is value-based', () { expect(const AudioRecoveryPolicy(), const AudioRecoveryPolicy()); expect( @@ -633,6 +718,912 @@ void main() { }); }); + // --------------------------------------------------------------------------- + // ReaderTTSVoice copyWith + // --------------------------------------------------------------------------- + group('ReaderTTSVoice', () { + test('copyWith() with no args preserves all fields', () { + final voice = ReaderTTSVoice( + identifier: 'voice1', + name: 'Test Voice', + language: 'en-US', + networkRequired: false, + gender: TTSVoiceGender.unspecified, + quality: TTSVoiceQuality.normal, + active: true, + ); + final copied = voice.copyWith(); + expect(copied.identifier, 'voice1'); + expect(copied.name, 'Test Voice'); + expect(copied.language, 'en-US'); + expect(copied.networkRequired, isFalse); + }); + + test('copyWith() overrides only specified fields', () { + final voice = ReaderTTSVoice( + identifier: 'voice1', + name: 'Test Voice', + language: 'en-US', + networkRequired: false, + gender: TTSVoiceGender.unspecified, + quality: TTSVoiceQuality.normal, + active: true, + ); + final updated = voice.copyWith(name: 'Updated Voice'); + expect(updated.identifier, 'voice1'); + expect(updated.name, 'Updated Voice'); + }); + + test('equality is value-based', () { + final a = ReaderTTSVoice( + identifier: 'v1', + name: 'A', + language: 'en', + networkRequired: false, + gender: TTSVoiceGender.unspecified, + quality: TTSVoiceQuality.normal, + active: true, + ); + final b = ReaderTTSVoice( + identifier: 'v1', + name: 'A', + language: 'en', + networkRequired: false, + gender: TTSVoiceGender.unspecified, + quality: TTSVoiceQuality.normal, + active: true, + ); + expect(a, equals(b)); + + final c = ReaderTTSVoice( + identifier: 'v1', + name: 'B', + language: 'en', + networkRequired: false, + gender: TTSVoiceGender.unspecified, + quality: TTSVoiceQuality.normal, + active: true, + ); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // ReaderDecoration copyWith + // --------------------------------------------------------------------------- + group('ReaderDecoration', () { + test('copyWith() with no args preserves all fields', () { + final decoration = ReaderDecoration( + id: 'deco1', + locator: const Locator(href: '/ch.xhtml', type: 'application/xhtml+xml'), + style: const ReaderDecorationStyle(style: DecorationStyle.highlight), + ); + final copied = decoration.copyWith(); + expect(copied.id, 'deco1'); + expect(copied.locator.href, '/ch.xhtml'); + }); + + test('copyWith() overrides only specified fields', () { + final decoration = ReaderDecoration( + id: 'deco1', + locator: const Locator(href: '/ch.xhtml', type: 'application/xhtml+xml'), + style: const ReaderDecorationStyle(style: DecorationStyle.highlight), + ); + final updated = decoration.copyWith(id: 'deco2'); + expect(updated.id, 'deco2'); + expect(updated.locator.href, '/ch.xhtml'); + }); + }); + + // --------------------------------------------------------------------------- + // ReaderDecorationStyle copyWith + // --------------------------------------------------------------------------- + group('ReaderDecorationStyle', () { + test('copyWith() with no args preserves all fields', () { + final style = ReaderDecorationStyle( + style: DecorationStyle.highlight, + tint: const Color(0xFFFF0000), + ); + final copied = style.copyWith(); + expect(copied.style, DecorationStyle.highlight); + expect(copied.tint, const Color(0xFFFF0000)); + }); + + test('copyWith() overrides only specified fields', () { + final style = ReaderDecorationStyle( + style: DecorationStyle.highlight, + tint: const Color(0xFFFF0000), + ); + final updated = style.copyWith(tint: const Color(0xFF00FF00)); + expect(updated.style, DecorationStyle.highlight); + expect(updated.tint, const Color(0xFF00FF00)); + }); + }); + + // --------------------------------------------------------------------------- + // ReadiumTimebasedState copyWith + // --------------------------------------------------------------------------- + group('ReadiumTimebasedState', () { + test('copyWith() with no args preserves all fields', () { + final state = ReadiumTimebasedState( + state: TimebasedState.playing, + currentLocator: const Locator(href: '/ch.xhtml', type: 'application/xhtml+xml'), + ); + final copied = state.copyWith(); + expect(copied.state, TimebasedState.playing); + expect(copied.currentLocator?.href, '/ch.xhtml'); + }); + + test('copyWith() overrides only specified fields', () { + final state = ReadiumTimebasedState( + state: TimebasedState.playing, + ); + final updated = state.copyWith(state: TimebasedState.paused); + expect(updated.state, TimebasedState.paused); + }); + }); + + // --------------------------------------------------------------------------- + // Facet copyWith + // --------------------------------------------------------------------------- + group('Facet', () { + test('copyWith() with no args preserves all fields', () { + final facet = Facet( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/link1.xhtml')], + ); + final copied = facet.copyWith(); + expect(copied.metadata, facet.metadata); + expect(copied.links, facet.links); + }); + + test('copyWith() overrides only specified fields', () { + final facet = Facet( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/link1.xhtml')], + ); + final updated = facet.copyWith(links: [const Link(href: '/link2.xhtml')]); + expect(updated.links.first.href, '/link2.xhtml'); + }); + + test('equality is value-based', () { + final a = Facet( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l1.xhtml')], + ); + final b = Facet( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l1.xhtml')], + ); + expect(a, equals(b)); + + final c = Facet( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l2.xhtml')], + ); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // OpdsPublication copyWith + // --------------------------------------------------------------------------- + group('OpdsPublication', () { + test('copyWith() with no args preserves all fields', () { + final pub = OpdsPublication( + const OpdsMetadata(localizedTitle: LocalizedString()), + [const Link(href: '/link1.xhtml')], + ); + final copied = pub.copyWith(); + expect(copied.metadata, pub.metadata); + expect(copied.links, pub.links); + }); + + test('copyWith() overrides only specified fields', () { + final pub = OpdsPublication( + const OpdsMetadata(localizedTitle: LocalizedString()), + [const Link(href: '/link1.xhtml')], + ); + final updated = pub.copyWith(links: [const Link(href: '/link2.xhtml')]); + expect(updated.links.first.href, '/link2.xhtml'); + }); + }); + + // --------------------------------------------------------------------------- + // Properties copyWith + // --------------------------------------------------------------------------- + group('Properties', () { + test('copyWith() with no args preserves all fields', () { + final props = Properties( + orientation: PresentationOrientation.landscape, + layout: EpubLayout.fixed, + ); + final copied = props.copyWith(); + expect(copied.orientation, PresentationOrientation.landscape); + expect(copied.layout, EpubLayout.fixed); + }); + + test('copyWith() overrides only specified fields', () { + final props = Properties(orientation: PresentationOrientation.landscape); + final updated = props.copyWith(layout: EpubLayout.reflowable); + expect(updated.orientation, PresentationOrientation.landscape); + expect(updated.layout, EpubLayout.reflowable); + }); + }); + + // --------------------------------------------------------------------------- + // Chapter copyWith + // --------------------------------------------------------------------------- + group('Chapter', () { + test('copyWith() with no args preserves all fields', () { + final chapter = Chapter(position: 1.0); + final copied = chapter.copyWith(); + expect(copied.position, 1.0); + }); + + test('copyWith() overrides only specified fields', () { + final chapter = Chapter(position: 1.0); + final updated = chapter.copyWith(position: 2.0); + expect(updated.position, 2.0); + }); + + test('equality is value-based', () { + final a = Chapter(position: 1.0); + final b = Chapter(position: 1.0); + expect(a, equals(b)); + + final c = Chapter(position: 2.0); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Season copyWith + // --------------------------------------------------------------------------- + group('Season', () { + test('copyWith() with no args preserves all fields', () { + final season = Season(position: 1.0); + final copied = season.copyWith(); + expect(copied.position, 1.0); + }); + + test('copyWith() overrides only specified fields', () { + final season = Season(position: 1.0); + final updated = season.copyWith(position: 2.0); + expect(updated.position, 2.0); + }); + + test('equality is value-based', () { + final a = Season(position: 1.0); + final b = Season(position: 1.0); + expect(a, equals(b)); + + final c = Season(position: 2.0); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Episode copyWith + // --------------------------------------------------------------------------- + group('Episode', () { + test('copyWith() with no args preserves all fields', () { + final episode = Episode(position: 1.0); + final copied = episode.copyWith(); + expect(copied.position, 1.0); + }); + + test('copyWith() overrides only specified fields', () { + final episode = Episode(position: 1.0); + final updated = episode.copyWith(position: 2.0); + expect(updated.position, 2.0); + }); + + test('equality is value-based', () { + final a = Episode(position: 1.0); + final b = Episode(position: 1.0); + expect(a, equals(b)); + + final c = Episode(position: 2.0); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Issue copyWith + // --------------------------------------------------------------------------- + group('Issue', () { + test('copyWith() with no args preserves all fields', () { + final issue = Issue(position: 1.0); + final copied = issue.copyWith(); + expect(copied.position, 1.0); + }); + + test('copyWith() overrides only specified fields', () { + final issue = Issue(position: 1.0); + final updated = issue.copyWith(position: 2.0); + expect(updated.position, 2.0); + }); + + test('equality is value-based', () { + final a = Issue(position: 1.0); + final b = Issue(position: 1.0); + expect(a, equals(b)); + + final c = Issue(position: 2.0); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Periodical copyWith + // --------------------------------------------------------------------------- + group('Periodical', () { + test('copyWith() with no args preserves all fields', () { + final periodical = Periodical(localizedName: LocalizedString()); + final copied = periodical.copyWith(); + expect(copied.localizedName, isNotNull); + }); + + test('copyWith() overrides only specified fields', () { + final periodical = Periodical(localizedName: LocalizedString()); + final updated = periodical.copyWith(identifier: 'id1'); + expect(updated.identifier, 'id1'); + }); + + test('equality is value-based', () { + final a = Periodical(localizedName: LocalizedString()); + final b = Periodical(localizedName: LocalizedString()); + expect(a, equals(b)); + + final c = Periodical(localizedName: LocalizedString(), identifier: 'id1'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Collection copyWith + // --------------------------------------------------------------------------- + group('Collection', () { + test('copyWith() with no args preserves all fields', () { + final collection = Collection(localizedName: LocalizedString()); + final copied = collection.copyWith(); + expect(copied.localizedName, isNotNull); + }); + + test('copyWith() overrides only specified fields', () { + final collection = Collection(localizedName: LocalizedString()); + final updated = collection.copyWith(identifier: 'id1'); + expect(updated.identifier, 'id1'); + }); + + test('equality is value-based', () { + final a = Collection(localizedName: LocalizedString()); + final b = Collection(localizedName: LocalizedString()); + expect(a, equals(b)); + + final c = Collection(localizedName: LocalizedString(), identifier: 'id1'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Contributor copyWith + // --------------------------------------------------------------------------- + group('Contributor', () { + test('copyWith() with no args preserves all fields', () { + final contributor = Contributor(localizedName: LocalizedString()); + final copied = contributor.copyWith(); + expect(copied.localizedName, isNotNull); + }); + + test('copyWith() overrides only specified fields', () { + final contributor = Contributor(localizedName: LocalizedString()); + final updated = contributor.copyWith(identifier: 'id1'); + expect(updated.identifier, 'id1'); + }); + + test('equality is value-based', () { + final a = Contributor(localizedName: LocalizedString()); + final b = Contributor(localizedName: LocalizedString()); + expect(a, equals(b)); + + final c = Contributor(localizedName: LocalizedString(), identifier: 'id1'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // OpdsMetadata copyWith + // --------------------------------------------------------------------------- + group('OpdsMetadata', () { + test('copyWith() with no args preserves all fields', () { + final metadata = OpdsMetadata( + localizedTitle: LocalizedString(), + ); + final copied = metadata.copyWith(); + expect(copied.localizedTitle, metadata.localizedTitle); + }); + + test('copyWith() overrides only specified fields', () { + final metadata = OpdsMetadata( + localizedTitle: LocalizedString(), + ); + final updated = metadata.copyWith(identifier: 'id1'); + expect(updated.identifier, 'id1'); + }); + + test('equality is value-based', () { + final a = OpdsMetadata(localizedTitle: LocalizedString()); + final b = OpdsMetadata(localizedTitle: LocalizedString()); + expect(a, equals(b)); + + final c = OpdsMetadata(localizedTitle: LocalizedString(), identifier: 'id1'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Feed copyWith + // --------------------------------------------------------------------------- + group('Feed', () { + test('copyWith() with no args preserves all fields', () { + final feed = Feed( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + ); + final copied = feed.copyWith(); + expect(copied.metadata, feed.metadata); + }); + + test('copyWith() overrides only specified fields', () { + final feed = Feed( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + ); + final updated = feed.copyWith(links: [const Link(href: '/link1.xhtml')]); + expect(updated.links, isNotEmpty); + }); + + test('equality is value-based', () { + final a = Feed(metadata: const OpdsMetadata(localizedTitle: LocalizedString())); + final b = Feed(metadata: const OpdsMetadata(localizedTitle: LocalizedString())); + expect(a, equals(b)); + + final c = Feed( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l1.xhtml')], + ); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Group copyWith + // --------------------------------------------------------------------------- + group('Group', () { + test('copyWith() with no args preserves all fields', () { + final group = Group( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/link1.xhtml')], + ); + final copied = group.copyWith(); + expect(copied.metadata, group.metadata); + expect(copied.links, group.links); + }); + + test('copyWith() overrides only specified fields', () { + final group = Group( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/link1.xhtml')], + ); + final updated = group.copyWith(links: [const Link(href: '/link2.xhtml')]); + expect(updated.links.first.href, '/link2.xhtml'); + }); + + test('equality is value-based', () { + final a = Group( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l1.xhtml')], + ); + final b = Group( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l1.xhtml')], + ); + expect(a, equals(b)); + + final c = Group( + metadata: const OpdsMetadata(localizedTitle: LocalizedString()), + links: [const Link(href: '/l2.xhtml')], + ); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // OpdsAuthentication copyWith + // --------------------------------------------------------------------------- + group('OpdsAuthentication', () { + test('copyWith() with no args preserves all fields', () { + final auth = OpdsAuthentication( + type: 'http://opds-spec.org/auth/schemes/open', + id: 'auth1', + ); + final copied = auth.copyWith(); + expect(copied.type, 'http://opds-spec.org/auth/schemes/open'); + expect(copied.id, 'auth1'); + }); + + test('copyWith() overrides only specified fields', () { + final auth = OpdsAuthentication( + type: 'http://opds-spec.org/auth/schemes/open', + id: 'auth1', + ); + final updated = auth.copyWith(id: 'auth2'); + expect(updated.type, 'http://opds-spec.org/auth/schemes/open'); + expect(updated.id, 'auth2'); + }); + + test('equality is value-based', () { + final a = OpdsAuthentication(type: 't1', id: 'i1'); + final b = OpdsAuthentication(type: 't1', id: 'i1'); + expect(a, equals(b)); + + final c = OpdsAuthentication(type: 't1', id: 'i2'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // OpdsAuthenticationFlow copyWith + // --------------------------------------------------------------------------- + group('OpdsAuthenticationFlow', () { + test('copyWith() with no args preserves all fields', () { + final flow = OpdsAuthenticationFlow( + type: 'http://opds-spec.org/auth/schemes/open', + ); + final copied = flow.copyWith(); + expect(copied.type, 'http://opds-spec.org/auth/schemes/open'); + }); + + test('copyWith() overrides only specified fields', () { + final flow = OpdsAuthenticationFlow( + type: 'http://opds-spec.org/auth/schemes/open', + ); + final updated = flow.copyWith(links: [const Link(href: '/link1.xhtml')]); + expect(updated.links, isNotEmpty); + }); + + test('equality is value-based', () { + final a = OpdsAuthenticationFlow(type: 't1'); + final b = OpdsAuthenticationFlow(type: 't1'); + expect(a, equals(b)); + + final c = OpdsAuthenticationFlow(type: 't2'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // Announcement copyWith + // --------------------------------------------------------------------------- + group('Announcement', () { + test('copyWith() with no args preserves all fields', () { + final announcement = Announcement( + id: 'announce1', + content: 'Test Announcement', + ); + final copied = announcement.copyWith(); + expect(copied.id, 'announce1'); + expect(copied.content, 'Test Announcement'); + }); + + test('copyWith() overrides only specified fields', () { + final announcement = Announcement( + id: 'announce1', + content: 'Original', + ); + final updated = announcement.copyWith(content: 'Updated'); + expect(updated.id, 'announce1'); + expect(updated.content, 'Updated'); + }); + + test('equality is value-based', () { + final a = Announcement(id: 'a1', content: 'C'); + final b = Announcement(id: 'a1', content: 'C'); + expect(a, equals(b)); + + final c = Announcement(id: 'a1', content: 'D'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // FeatureFlags copyWith + // --------------------------------------------------------------------------- + group('FeatureFlags', () { + test('copyWith() with no args preserves all fields', () { + final flags = FeatureFlags( + enabled: ['feature1'], + disabled: ['feature2'], + ); + final copied = flags.copyWith(); + expect(copied.enabled, ['feature1']); + expect(copied.disabled, ['feature2']); + }); + + test('copyWith() overrides only specified fields', () { + final flags = FeatureFlags( + enabled: ['feature1'], + disabled: ['feature2'], + ); + final updated = flags.copyWith(enabled: ['feature3']); + expect(updated.enabled, ['feature3']); + }); + + test('equality is value-based', () { + final a = FeatureFlags(enabled: ['f1'], disabled: ['f2']); + final b = FeatureFlags(enabled: ['f1'], disabled: ['f2']); + expect(a, equals(b)); + + final c = FeatureFlags(enabled: ['f3'], disabled: ['f2']); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // InputField copyWith + // --------------------------------------------------------------------------- + group('InputField', () { + test('copyWith() with no args preserves all fields', () { + final field = InputField( + keyboard: KeyboardType.defaultType, + maximumLength: 100, + ); + final copied = field.copyWith(); + expect(copied.keyboard, KeyboardType.defaultType); + expect(copied.maximumLength, 100); + }); + + test('copyWith() overrides only specified fields', () { + final field = InputField( + keyboard: KeyboardType.defaultType, + maximumLength: 100, + ); + final updated = field.copyWith(maximumLength: 200); + expect(updated.keyboard, KeyboardType.defaultType); + expect(updated.maximumLength, 200); + }); + + test('equality is value-based', () { + final a = InputField(keyboard: KeyboardType.defaultType, maximumLength: 100); + final b = InputField(keyboard: KeyboardType.defaultType, maximumLength: 100); + expect(a, equals(b)); + + final c = InputField(keyboard: KeyboardType.defaultType, maximumLength: 200); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // LoginInputField copyWith + // --------------------------------------------------------------------------- + group('LoginInputField', () { + test('copyWith() with no args preserves all fields', () { + final loginField = LoginInputField( + barcodeFormat: 'qr-code', + ); + final copied = loginField.copyWith(); + expect(copied.barcodeFormat, 'qr-code'); + }); + + test('copyWith() overrides only specified fields', () { + final loginField = LoginInputField( + barcodeFormat: 'qr-code', + ); + final updated = loginField.copyWith(barcodeFormat: 'barcode'); + expect(updated.barcodeFormat, 'barcode'); + }); + + test('equality is value-based', () { + final a = LoginInputField(barcodeFormat: 'qr-code'); + final b = LoginInputField(barcodeFormat: 'qr-code'); + expect(a, equals(b)); + + final c = LoginInputField(barcodeFormat: 'barcode'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // InputData copyWith + // --------------------------------------------------------------------------- + group('InputData', () { + test('copyWith() with no args preserves all fields', () { + final inputData = InputData( + login: const LoginInputField(), + password: const InputField(), + ); + final copied = inputData.copyWith(); + expect(copied.login, const LoginInputField()); + expect(copied.password, const InputField()); + }); + + test('copyWith() overrides only specified fields', () { + final inputData = InputData( + login: const LoginInputField(), + password: const InputField(), + ); + final updated = inputData.copyWith(password: const InputField(keyboard: KeyboardType.numPad)); + expect(updated.password.keyboard, KeyboardType.numPad); + }); + + test('equality is value-based', () { + final a = InputData(); + final b = InputData(); + expect(a, equals(b)); + + final c = InputData(password: const InputField(keyboard: KeyboardType.numPad)); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // PublicKeyData copyWith + // --------------------------------------------------------------------------- + group('PublicKeyData', () { + test('copyWith() with no args preserves all fields', () { + final publicKey = PublicKeyData( + type: 'RSA', + value: 'base64-encoded-key', + ); + final copied = publicKey.copyWith(); + expect(copied.type, 'RSA'); + expect(copied.value, 'base64-encoded-key'); + }); + + test('copyWith() overrides only specified fields', () { + final publicKey = PublicKeyData( + type: 'RSA', + value: 'base64-encoded-key', + ); + final updated = publicKey.copyWith(value: 'new-key'); + expect(updated.type, 'RSA'); + expect(updated.value, 'new-key'); + }); + + test('equality is value-based', () { + final a = PublicKeyData(type: 'RSA', value: 'key1'); + final b = PublicKeyData(type: 'RSA', value: 'key1'); + expect(a, equals(b)); + + final c = PublicKeyData(type: 'RSA', value: 'key2'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // WebColor copyWith + // --------------------------------------------------------------------------- + group('WebColor', () { + test('copyWith() with no args preserves all fields', () { + final color = WebColor( + primary: '#FF0000', + secondary: '#00FF00', + ); + final copied = color.copyWith(); + expect(copied.primary, '#FF0000'); + expect(copied.secondary, '#00FF00'); + }); + + test('copyWith() overrides only specified fields', () { + final color = WebColor( + primary: '#FF0000', + secondary: '#00FF00', + ); + final updated = color.copyWith(primary: '#0000FF'); + expect(updated.primary, '#0000FF'); + expect(updated.secondary, '#00FF00'); + }); + + test('equality is value-based', () { + final a = WebColor(primary: '#FF0000', secondary: '#00FF00'); + final b = WebColor(primary: '#FF0000', secondary: '#00FF00'); + expect(a, equals(b)); + + final c = WebColor(primary: '#0000FF', secondary: '#00FF00'); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // LocatorCollection copyWith + // --------------------------------------------------------------------------- + group('LocatorCollection', () { + test('copyWith() with no args preserves all fields', () { + final collection = LocatorCollection( + metadata: const LocatorCollectionMetadata(), + ); + final copied = collection.copyWith(); + expect(copied.metadata, const LocatorCollectionMetadata()); + }); + + test('copyWith() overrides only specified fields', () { + final collection = LocatorCollection( + metadata: const LocatorCollectionMetadata(), + ); + final updated = collection.copyWith(links: [const Link(href: '/link1.xhtml')]); + expect(updated.links, isNotEmpty); + }); + + test('equality is value-based', () { + final a = LocatorCollection(); + final b = LocatorCollection(); + expect(a, equals(b)); + + final c = LocatorCollection(links: [const Link(href: '/l1.xhtml')]); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // LocatorCollectionMetadata copyWith + // --------------------------------------------------------------------------- + group('LocatorCollectionMetadata', () { + test('copyWith() with no args preserves all fields', () { + final metadata = LocatorCollectionMetadata( + localizedTitle: LocalizedString(), + ); + final copied = metadata.copyWith(); + expect(copied.localizedTitle, isNotNull); + }); + + test('copyWith() overrides only specified fields', () { + final metadata = LocatorCollectionMetadata( + localizedTitle: LocalizedString(), + ); + final updated = metadata.copyWith(numberOfItems: 10); + expect(updated.numberOfItems, 10); + }); + + test('equality is value-based', () { + final a = LocatorCollectionMetadata(localizedTitle: LocalizedString()); + final b = LocatorCollectionMetadata(localizedTitle: LocalizedString()); + expect(a, equals(b)); + + final c = LocatorCollectionMetadata(localizedTitle: LocalizedString(), numberOfItems: 10); + expect(a, isNot(equals(c))); + }); + }); + + // --------------------------------------------------------------------------- + // LocalizedString copyWith + // --------------------------------------------------------------------------- + group('LocalizedString', () { + test('copyWith() with no args preserves all fields', () { + final str = LocalizedString( + translations: {'en': Translation('Hello')}, + ); + final copied = str.copyWith(); + expect(copied.translations, isNotNull); + }); + + test('copyWith() overrides only specified fields', () { + final str = LocalizedString( + translations: {'en': Translation('Original')}, + ); + final updated = str.copyWith(translations: {'en': Translation('Updated')}); + expect(updated.translations, isNotNull); + }); + + test('equality is value-based', () { + final a = LocalizedString(translations: {'en': Translation('Hello')}); + final b = LocalizedString(translations: {'en': Translation('Hello')}); + expect(a, equals(b)); + + final c = LocalizedString(translations: {'en': Translation('Hi')}); + expect(a, isNot(equals(c))); + }); + }); + // --------------------------------------------------------------------------- // Properties serialisation // ---------------------------------------------------------------------------