@panelwave/player 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/fesm2022/panelwave-player.mjs +465 -139
- package/fesm2022/panelwave-player.mjs.map +1 -1
- package/index.d.ts +181 -22
- package/package.json +1 -1
- package/src/assets/i18n/de.json +26 -1
- package/src/assets/i18n/en.json +26 -1
|
@@ -7053,12 +7053,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
7053
7053
|
* free preview.
|
|
7054
7054
|
* - Panel-scoped rules gate exactly their `targetPanelIds` — preview does NOT
|
|
7055
7055
|
* override an explicit panel gate.
|
|
7056
|
+
* - Chapter-scoped rules gate only the panels of their chapter(s) (`refId`);
|
|
7057
|
+
* their free preview counts panels WITHIN that chapter. They take
|
|
7058
|
+
* precedence over a work gate for those panels.
|
|
7059
|
+
* - Extras-scoped rules never gate panels — they gate the extras block named
|
|
7060
|
+
* by `refId` (see `isExtraLocked`).
|
|
7056
7061
|
* - A gated panel unlocks when the gating rule's requirement is satisfied by
|
|
7057
7062
|
* the reader's entitlement snapshot.
|
|
7058
7063
|
*/
|
|
7059
7064
|
const DEFAULT_MINIMUM_AGE = 18;
|
|
7060
|
-
/** Does the
|
|
7065
|
+
/** Does the rule carry an age requirement the reader has not met? */
|
|
7066
|
+
function ageMissing(rule, entitlement) {
|
|
7067
|
+
if (rule.entitlementType !== 'age_gate' && rule.minimumAge === undefined)
|
|
7068
|
+
return false;
|
|
7069
|
+
const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
|
|
7070
|
+
return !(entitlement.ageVerified && (entitlement.age ?? 0) >= min);
|
|
7071
|
+
}
|
|
7072
|
+
/**
|
|
7073
|
+
* Does the reader's snapshot satisfy this rule's requirement? A rule with a
|
|
7074
|
+
* `minimumAge` needs the age AND its entitlement.
|
|
7075
|
+
*/
|
|
7061
7076
|
function satisfiesRule(rule, entitlement) {
|
|
7077
|
+
return !ageMissing(rule, entitlement) && hasEntitlement(rule, entitlement);
|
|
7078
|
+
}
|
|
7079
|
+
/** The entitlement part of a rule (the age part is `ageMissing`). */
|
|
7080
|
+
function hasEntitlement(rule, entitlement) {
|
|
7062
7081
|
switch (rule.entitlementType) {
|
|
7063
7082
|
case 'free':
|
|
7064
7083
|
return true;
|
|
@@ -7074,13 +7093,17 @@ function satisfiesRule(rule, entitlement) {
|
|
|
7074
7093
|
return entitlement.purchasedProductIds.length > 0;
|
|
7075
7094
|
return required.some((id) => entitlement.purchasedProductIds.includes(id));
|
|
7076
7095
|
}
|
|
7077
|
-
case 'age_gate':
|
|
7078
|
-
|
|
7079
|
-
return entitlement.ageVerified && (entitlement.age ?? 0) >= min;
|
|
7080
|
-
}
|
|
7096
|
+
case 'age_gate':
|
|
7097
|
+
return true; // the age is the whole requirement (checked by ageMissing)
|
|
7081
7098
|
}
|
|
7082
7099
|
}
|
|
7083
|
-
|
|
7100
|
+
/**
|
|
7101
|
+
* Why a rule locks the reader out. The age comes first: a reader who fails
|
|
7102
|
+
* the age check is asked for it before being offered anything to buy.
|
|
7103
|
+
*/
|
|
7104
|
+
function lockReasonFor(rule, entitlement) {
|
|
7105
|
+
if (ageMissing(rule, entitlement))
|
|
7106
|
+
return 'age_verification_required';
|
|
7084
7107
|
switch (rule.entitlementType) {
|
|
7085
7108
|
case 'subscription':
|
|
7086
7109
|
return 'subscription_required';
|
|
@@ -7108,36 +7131,48 @@ function evaluatePanelAccess(rules, entitlement, panel) {
|
|
|
7108
7131
|
return {
|
|
7109
7132
|
panelId: panel.id,
|
|
7110
7133
|
locked: !ok,
|
|
7111
|
-
reason: ok ? null : lockReasonFor(panelRule),
|
|
7134
|
+
reason: ok ? null : lockReasonFor(panelRule, entitlement),
|
|
7112
7135
|
appliedRuleId: panelRule.id,
|
|
7113
7136
|
preview: false,
|
|
7114
7137
|
};
|
|
7115
7138
|
}
|
|
7116
|
-
// 2.
|
|
7139
|
+
// 2. Chapter gates: only the panels of the named chapter, with the free
|
|
7140
|
+
// preview counted within that chapter.
|
|
7141
|
+
const chapterRule = panel.chapterId === undefined
|
|
7142
|
+
? undefined
|
|
7143
|
+
: active.find((r) => r.scope === 'chapter' && (r.targetIds ?? []).includes(panel.chapterId));
|
|
7144
|
+
if (chapterRule) {
|
|
7145
|
+
return gateWithPreview(chapterRule, entitlement, panel.id, panel.chapterIndex ?? Number.MAX_SAFE_INTEGER);
|
|
7146
|
+
}
|
|
7147
|
+
// 3. Work/global gate with free preview.
|
|
7117
7148
|
const workRule = findWorkGateRule(active);
|
|
7118
7149
|
if (workRule) {
|
|
7119
|
-
|
|
7120
|
-
if (panel.index < previewCount) {
|
|
7121
|
-
return {
|
|
7122
|
-
panelId: panel.id,
|
|
7123
|
-
locked: false,
|
|
7124
|
-
reason: null,
|
|
7125
|
-
appliedRuleId: workRule.id,
|
|
7126
|
-
preview: true,
|
|
7127
|
-
};
|
|
7128
|
-
}
|
|
7129
|
-
const ok = satisfiesRule(workRule, entitlement);
|
|
7130
|
-
return {
|
|
7131
|
-
panelId: panel.id,
|
|
7132
|
-
locked: !ok,
|
|
7133
|
-
reason: ok ? null : lockReasonFor(workRule),
|
|
7134
|
-
appliedRuleId: workRule.id,
|
|
7135
|
-
preview: false,
|
|
7136
|
-
};
|
|
7150
|
+
return gateWithPreview(workRule, entitlement, panel.id, panel.index);
|
|
7137
7151
|
}
|
|
7138
|
-
//
|
|
7152
|
+
// 4. No gate at all — free content.
|
|
7139
7153
|
return { panelId: panel.id, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7140
7154
|
}
|
|
7155
|
+
/** A work/chapter gate: panels before `previewPanelCount` (by `index`) are free. */
|
|
7156
|
+
function gateWithPreview(rule, entitlement, panelId, index) {
|
|
7157
|
+
if (index < (rule.previewPanelCount ?? 0)) {
|
|
7158
|
+
return { panelId, locked: false, reason: null, appliedRuleId: rule.id, preview: true };
|
|
7159
|
+
}
|
|
7160
|
+
const ok = satisfiesRule(rule, entitlement);
|
|
7161
|
+
return {
|
|
7162
|
+
panelId,
|
|
7163
|
+
locked: !ok,
|
|
7164
|
+
reason: ok ? null : lockReasonFor(rule, entitlement),
|
|
7165
|
+
appliedRuleId: rule.id,
|
|
7166
|
+
preview: false,
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
/** Is the extras block `extraId` locked by an extras-scoped rule? */
|
|
7170
|
+
function isExtraLocked(rules, entitlement, extraId) {
|
|
7171
|
+
return rules.some((r) => r.isActive &&
|
|
7172
|
+
r.scope === 'extras' &&
|
|
7173
|
+
(r.targetIds ?? []).includes(extraId) &&
|
|
7174
|
+
!satisfiesRule(r, entitlement));
|
|
7175
|
+
}
|
|
7141
7176
|
/** Evaluate a whole work (panels in reading order). */
|
|
7142
7177
|
function evaluateWorkAccess(rules, entitlement, panels) {
|
|
7143
7178
|
return panels.map((panel) => evaluatePanelAccess(rules, entitlement, panel));
|
|
@@ -7164,8 +7199,10 @@ const KNOWN_ENTITLEMENT_TYPES = [
|
|
|
7164
7199
|
* `minimumAge` written by the CMS exporter. Prefer the CMS names and fall
|
|
7165
7200
|
* back to the originals, so a manifest from either era gates identically.
|
|
7166
7201
|
*
|
|
7167
|
-
*
|
|
7168
|
-
*
|
|
7202
|
+
* `ageGate` / `minimumAge` is an age check ON TOP OF the entitlement (schema
|
|
7203
|
+
* semantics). Only a rule that declares no entitlement at all but names an
|
|
7204
|
+
* age becomes a pure age gate; a custom marker plus an age stays a purchase
|
|
7205
|
+
* (or subscription) gate that also requires the age.
|
|
7169
7206
|
*/
|
|
7170
7207
|
function fromManifestRule(rule, index) {
|
|
7171
7208
|
const raw = rule;
|
|
@@ -7175,9 +7212,6 @@ function fromManifestRule(rule, index) {
|
|
|
7175
7212
|
if (declared && KNOWN_ENTITLEMENT_TYPES.includes(declared)) {
|
|
7176
7213
|
entitlementType = declared;
|
|
7177
7214
|
}
|
|
7178
|
-
else if (minimumAge !== undefined) {
|
|
7179
|
-
entitlementType = 'age_gate';
|
|
7180
|
-
}
|
|
7181
7215
|
else if (declared === 'premium') {
|
|
7182
7216
|
// Original format's subscription marker.
|
|
7183
7217
|
entitlementType = 'subscription';
|
|
@@ -7186,10 +7220,17 @@ function fromManifestRule(rule, index) {
|
|
|
7186
7220
|
// 'token', 'purchaseId', or any custom marker: treat as a purchase gate.
|
|
7187
7221
|
entitlementType = 'purchase';
|
|
7188
7222
|
}
|
|
7223
|
+
else if (minimumAge !== undefined) {
|
|
7224
|
+
// No entitlement declared, only an age: a pure age gate.
|
|
7225
|
+
entitlementType = 'age_gate';
|
|
7226
|
+
}
|
|
7189
7227
|
else {
|
|
7190
7228
|
entitlementType = 'free';
|
|
7191
7229
|
}
|
|
7192
|
-
|
|
7230
|
+
// Extras-scoped rules keep their scope: they gate an extras block, never
|
|
7231
|
+
// panels (mapping them onto 'work' used to lock the whole work).
|
|
7232
|
+
const scope = raw['scope'];
|
|
7233
|
+
const refId = raw['refId'];
|
|
7193
7234
|
return {
|
|
7194
7235
|
id: raw['id'] ?? `rule-${index}`,
|
|
7195
7236
|
scope: scope ?? 'work',
|
|
@@ -7203,9 +7244,12 @@ function fromManifestRule(rule, index) {
|
|
|
7203
7244
|
targetPanelIds: raw['targetPanelIds'] ??
|
|
7204
7245
|
// A panel-scoped rule from the original format names its single target
|
|
7205
7246
|
// in refId rather than a list.
|
|
7206
|
-
(
|
|
7207
|
-
|
|
7208
|
-
|
|
7247
|
+
(scope === 'panel' && refId ? [refId] : undefined),
|
|
7248
|
+
targetIds: (scope === 'chapter' || scope === 'extras') && refId ? [refId] : undefined,
|
|
7249
|
+
name: raw['name'],
|
|
7250
|
+
description: raw['description'],
|
|
7251
|
+
price: raw['price'],
|
|
7252
|
+
entitlementKey: raw['requireEntitlement'],
|
|
7209
7253
|
};
|
|
7210
7254
|
}
|
|
7211
7255
|
/** Normalise every rule in a manifest. Returns [] when the work has no paywall. */
|
|
@@ -7215,6 +7259,14 @@ function rulesFromManifest(manifest) {
|
|
|
7215
7259
|
return [];
|
|
7216
7260
|
return rules.map(fromManifestRule);
|
|
7217
7261
|
}
|
|
7262
|
+
/** Chapter id and within-chapter index per panel id (chapter-rule previews). */
|
|
7263
|
+
function chapterOrderFromManifest(manifest) {
|
|
7264
|
+
const out = new Map();
|
|
7265
|
+
for (const chapter of manifest?.chapters ?? []) {
|
|
7266
|
+
Object.keys(chapter.panels ?? {}).forEach((panelId, chapterIndex) => out.set(panelId, { chapterId: chapter.id, chapterIndex }));
|
|
7267
|
+
}
|
|
7268
|
+
return out;
|
|
7269
|
+
}
|
|
7218
7270
|
/**
|
|
7219
7271
|
* Global reading order for preview counting: chapters in array order, and
|
|
7220
7272
|
* within a chapter the panel declaration order.
|
|
@@ -7267,7 +7319,9 @@ class PaywallService {
|
|
|
7267
7319
|
constructor() {
|
|
7268
7320
|
this.rules = [];
|
|
7269
7321
|
this.readingOrder = new Map();
|
|
7322
|
+
this.chapterOrder = new Map();
|
|
7270
7323
|
this.manifest = null;
|
|
7324
|
+
this.products = new Map();
|
|
7271
7325
|
this.snapshotSubject = new BehaviorSubject(ANONYMOUS_READER);
|
|
7272
7326
|
this.snapshot$ = this.snapshotSubject.asObservable();
|
|
7273
7327
|
}
|
|
@@ -7276,6 +7330,17 @@ class PaywallService {
|
|
|
7276
7330
|
this.manifest = manifest;
|
|
7277
7331
|
this.rules = rulesFromManifest(manifest);
|
|
7278
7332
|
this.readingOrder = new Map(readingOrderFromManifest(manifest).map((panelId, index) => [panelId, index]));
|
|
7333
|
+
this.chapterOrder = chapterOrderFromManifest(manifest);
|
|
7334
|
+
this.products = PaywallService.productsFromManifest(manifest);
|
|
7335
|
+
}
|
|
7336
|
+
/** `paywall.products` by id (format 1.6); the first entry wins on duplicate ids. */
|
|
7337
|
+
static productsFromManifest(manifest) {
|
|
7338
|
+
const out = new Map();
|
|
7339
|
+
for (const product of manifest?.paywall?.products ?? []) {
|
|
7340
|
+
if (product?.id && !out.has(product.id))
|
|
7341
|
+
out.set(product.id, product);
|
|
7342
|
+
}
|
|
7343
|
+
return out;
|
|
7279
7344
|
}
|
|
7280
7345
|
/** Replace what the reader owns (after sign-in, or a completed purchase). */
|
|
7281
7346
|
setSnapshot(snapshot) {
|
|
@@ -7304,7 +7369,15 @@ class PaywallService {
|
|
|
7304
7369
|
return { panelId, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7305
7370
|
}
|
|
7306
7371
|
const index = this.readingOrder.get(panelId) ?? Number.MAX_SAFE_INTEGER;
|
|
7307
|
-
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7372
|
+
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7373
|
+
id: panelId,
|
|
7374
|
+
index,
|
|
7375
|
+
...this.chapterOrder.get(panelId),
|
|
7376
|
+
});
|
|
7377
|
+
}
|
|
7378
|
+
/** Is this extras block locked by an extras-scoped rule the reader does not satisfy? */
|
|
7379
|
+
isExtraLocked(extraId) {
|
|
7380
|
+
return isExtraLocked(this.rules, this.snapshotSubject.value, extraId);
|
|
7308
7381
|
}
|
|
7309
7382
|
/** Convenience: may the reader open this panel? */
|
|
7310
7383
|
canAccess(panelId) {
|
|
@@ -7313,9 +7386,11 @@ class PaywallService {
|
|
|
7313
7386
|
/**
|
|
7314
7387
|
* Describe the gate blocking a panel, or null when it is not gated.
|
|
7315
7388
|
* The shape is the player's existing `PaywallGate`, so the overlay needs
|
|
7316
|
-
* no changes to render it.
|
|
7389
|
+
* no changes to render it. `locale` picks the language of product names
|
|
7390
|
+
* and descriptions from `paywall.products` (fallback: the work's default
|
|
7391
|
+
* locale).
|
|
7317
7392
|
*/
|
|
7318
|
-
gateFor(panelId) {
|
|
7393
|
+
gateFor(panelId, locale) {
|
|
7319
7394
|
const decision = this.evaluate(panelId);
|
|
7320
7395
|
if (!decision.locked)
|
|
7321
7396
|
return null;
|
|
@@ -7323,14 +7398,52 @@ class PaywallService {
|
|
|
7323
7398
|
const scope = rule?.scope === 'panel' ? 'panel' : rule?.scope === 'chapter' ? 'chapter' : 'work';
|
|
7324
7399
|
return {
|
|
7325
7400
|
scope,
|
|
7326
|
-
refId: rule?.scope === 'panel' ? panelId : undefined,
|
|
7401
|
+
refId: rule?.scope === 'panel' ? panelId : scope === 'chapter' ? this.chapterOrder.get(panelId)?.chapterId : undefined,
|
|
7327
7402
|
requireEntitlement: rule?.entitlementType,
|
|
7328
7403
|
reason: PaywallService.readerMessage(decision.reason),
|
|
7329
7404
|
preview: rule?.previewPanelCount
|
|
7330
7405
|
? { previewPanels: rule.previewPanelCount }
|
|
7331
7406
|
: undefined,
|
|
7407
|
+
ruleId: rule?.id,
|
|
7408
|
+
lockReason: decision.reason ?? undefined,
|
|
7409
|
+
options: rule
|
|
7410
|
+
? PaywallService.purchaseOptions(rule, this.products, locale ?? this.defaultLocale, this.defaultLocale)
|
|
7411
|
+
: [],
|
|
7332
7412
|
};
|
|
7333
7413
|
}
|
|
7414
|
+
/**
|
|
7415
|
+
* What the reader can buy to pass a rule: one option per required product
|
|
7416
|
+
* (purchase) or per subscription tier (subscription). Without explicit
|
|
7417
|
+
* ids, the rule's `requireEntitlement` key names the product, else the
|
|
7418
|
+
* rule id. Age gates and free rules offer nothing to buy.
|
|
7419
|
+
*
|
|
7420
|
+
* Labels (format 1.6): an option whose id has an entry in
|
|
7421
|
+
* `paywall.products` takes that entry's localized name, description and
|
|
7422
|
+
* price. Otherwise it falls back to the rule's name (a single option) or
|
|
7423
|
+
* the bare id (several options), and to the rule's description and price.
|
|
7424
|
+
*/
|
|
7425
|
+
static purchaseOptions(rule, products = new Map(), locale = 'en-US', fallbackLocale = 'en-US') {
|
|
7426
|
+
const type = rule.entitlementType === 'purchase' ? 'one-time' : rule.entitlementType;
|
|
7427
|
+
if (type !== 'one-time' && type !== 'subscription')
|
|
7428
|
+
return [];
|
|
7429
|
+
const listed = type === 'one-time' ? rule.requiredProductIds : rule.subscriptionTiers;
|
|
7430
|
+
const key = rule.entitlementKey !== rule.entitlementType ? rule.entitlementKey : undefined;
|
|
7431
|
+
const ids = listed?.length ? listed : [key ?? rule.id];
|
|
7432
|
+
return ids.map((productId) => {
|
|
7433
|
+
const product = products.get(productId);
|
|
7434
|
+
const name = product?.name ? resolveLocalizedString(product.name, locale, fallbackLocale) : '';
|
|
7435
|
+
const description = product?.description
|
|
7436
|
+
? resolveLocalizedString(product.description, locale, fallbackLocale)
|
|
7437
|
+
: '';
|
|
7438
|
+
return {
|
|
7439
|
+
productId,
|
|
7440
|
+
name: name || (ids.length > 1 ? productId : rule.name ?? ''),
|
|
7441
|
+
description: description || rule.description,
|
|
7442
|
+
price: product?.price ?? rule.price,
|
|
7443
|
+
type,
|
|
7444
|
+
};
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7334
7447
|
/** The rule that gates a panel, for callers that need its price or name. */
|
|
7335
7448
|
ruleFor(panelId) {
|
|
7336
7449
|
const decision = this.evaluate(panelId);
|
|
@@ -7338,11 +7451,17 @@ class PaywallService {
|
|
|
7338
7451
|
return null;
|
|
7339
7452
|
return this.rules.find((r) => r.id === decision.appliedRuleId) ?? null;
|
|
7340
7453
|
}
|
|
7454
|
+
/** The work's default locale (product-label fallback). */
|
|
7455
|
+
get defaultLocale() {
|
|
7456
|
+
return this.manifest?.meta?.default_locale ?? 'en-US';
|
|
7457
|
+
}
|
|
7341
7458
|
/** Reset to a manifest-less, entitlement-less state. */
|
|
7342
7459
|
clear() {
|
|
7343
7460
|
this.manifest = null;
|
|
7344
7461
|
this.rules = [];
|
|
7462
|
+
this.products.clear();
|
|
7345
7463
|
this.readingOrder.clear();
|
|
7464
|
+
this.chapterOrder.clear();
|
|
7346
7465
|
this.snapshotSubject.next(ANONYMOUS_READER);
|
|
7347
7466
|
}
|
|
7348
7467
|
/**
|
|
@@ -14568,6 +14687,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14568
14687
|
args: ['window:keydown', ['$event']]
|
|
14569
14688
|
}] } });
|
|
14570
14689
|
|
|
14690
|
+
/**
|
|
14691
|
+
* Translate a built-in UI string. Falls back to the given English text (with
|
|
14692
|
+
* `{{param}}` interpolation) when no TranslateService is provided or the key
|
|
14693
|
+
* is not loaded, so a public component embedded without ngx-translate never
|
|
14694
|
+
* shows a raw key.
|
|
14695
|
+
*/
|
|
14696
|
+
function uiText(translate, key, english, params) {
|
|
14697
|
+
const value = translate?.instant(key, params);
|
|
14698
|
+
if (typeof value === 'string' && value !== key) {
|
|
14699
|
+
return value;
|
|
14700
|
+
}
|
|
14701
|
+
return (english ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14702
|
+
}
|
|
14703
|
+
|
|
14571
14704
|
/**
|
|
14572
14705
|
* Paywall Overlay Component
|
|
14573
14706
|
* Displays paywall gate UI with purchase options
|
|
@@ -14594,6 +14727,10 @@ const PAYWALL_TEXT_EN = {
|
|
|
14594
14727
|
'paywall.go_back': 'Go Back',
|
|
14595
14728
|
'paywall.maybe_later': 'Maybe Later',
|
|
14596
14729
|
'paywall.secure_payment': 'Secure payment processing',
|
|
14730
|
+
'paywall.reason_subscription_required': 'This part of the story is included with a subscription.',
|
|
14731
|
+
'paywall.reason_purchase_required': 'This part of the story is available to buy.',
|
|
14732
|
+
'paywall.reason_age_verification_required': 'This content is age-restricted. Confirm your age to continue.',
|
|
14733
|
+
'paywall.reason_entitlement_required': 'This content requires an entitlement to access.',
|
|
14597
14734
|
};
|
|
14598
14735
|
/**
|
|
14599
14736
|
* Paywall Overlay Component
|
|
@@ -14672,17 +14809,24 @@ class PaywallOverlayComponent {
|
|
|
14672
14809
|
if (this.message) {
|
|
14673
14810
|
return this.getLocalizedString(this.message);
|
|
14674
14811
|
}
|
|
14812
|
+
// A known lock reason is translated; otherwise the gate's own sentence.
|
|
14813
|
+
const key = 'paywall.reason_' + this.gate?.lockReason;
|
|
14814
|
+
const translated = this.t(key);
|
|
14815
|
+
if (translated !== key) {
|
|
14816
|
+
return translated;
|
|
14817
|
+
}
|
|
14675
14818
|
if (this.gate?.reason) {
|
|
14676
14819
|
return this.gate.reason;
|
|
14677
14820
|
}
|
|
14678
14821
|
return this.t('paywall.message_default');
|
|
14679
14822
|
}
|
|
14680
14823
|
/**
|
|
14681
|
-
* Handle purchase option selection
|
|
14824
|
+
* Handle purchase option selection: `purchase` carries the product id,
|
|
14825
|
+
* `action` is 'subscribe' for a subscription option, else 'purchase'.
|
|
14682
14826
|
*/
|
|
14683
|
-
onPurchaseOption(
|
|
14684
|
-
this.purchase.emit(productId);
|
|
14685
|
-
this.action.emit('purchase');
|
|
14827
|
+
onPurchaseOption(option) {
|
|
14828
|
+
this.purchase.emit(option.productId);
|
|
14829
|
+
this.action.emit(option.type === 'subscription' ? 'subscribe' : 'purchase');
|
|
14686
14830
|
}
|
|
14687
14831
|
/**
|
|
14688
14832
|
* Handle login action
|
|
@@ -14745,18 +14889,14 @@ class PaywallOverlayComponent {
|
|
|
14745
14889
|
* Manifest-provided texts (title/message/gate.reason) bypass this.
|
|
14746
14890
|
*/
|
|
14747
14891
|
t(key, params) {
|
|
14748
|
-
|
|
14749
|
-
if (typeof value === 'string' && value !== key) {
|
|
14750
|
-
return value;
|
|
14751
|
-
}
|
|
14752
|
-
return (PAYWALL_TEXT_EN[key] ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14892
|
+
return uiText(this.translate, key, PAYWALL_TEXT_EN[key], params);
|
|
14753
14893
|
}
|
|
14754
14894
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14755
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option
|
|
14895
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14756
14896
|
}
|
|
14757
14897
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, decorators: [{
|
|
14758
14898
|
type: Component,
|
|
14759
|
-
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option
|
|
14899
|
+
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"] }]
|
|
14760
14900
|
}], propDecorators: { visible: [{
|
|
14761
14901
|
type: Input
|
|
14762
14902
|
}], gate: [{
|
|
@@ -15765,12 +15905,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15765
15905
|
* Age Gate Component
|
|
15766
15906
|
* Displays age verification UI for restricted content
|
|
15767
15907
|
*/
|
|
15908
|
+
/**
|
|
15909
|
+
* Built-in English UI strings (keys under `age_gate.`), used when no
|
|
15910
|
+
* TranslateService is provided or a key is not loaded. Must match the
|
|
15911
|
+
* "age_gate" section of assets/i18n/en.json.
|
|
15912
|
+
*/
|
|
15913
|
+
const AGE_GATE_TEXT_EN = {
|
|
15914
|
+
title: 'Age Verification Required',
|
|
15915
|
+
close: 'Close',
|
|
15916
|
+
warning: 'This content is restricted to users {{age}} years of age or older.',
|
|
15917
|
+
prompt: 'Please enter your birth date:',
|
|
15918
|
+
month: 'Month',
|
|
15919
|
+
day: 'Day',
|
|
15920
|
+
year: 'Year',
|
|
15921
|
+
verify: 'Verify Age',
|
|
15922
|
+
privacy: 'Your information is private and will not be stored.',
|
|
15923
|
+
error_incomplete: 'Please enter your complete birth date.',
|
|
15924
|
+
error_month: 'Please enter a valid month (1-12).',
|
|
15925
|
+
error_day: 'Please enter a valid day (1-{{max}}).',
|
|
15926
|
+
error_year: 'Please enter a valid year (1900-{{max}}).',
|
|
15927
|
+
error_too_young: 'You must be at least {{age}} years old to access this content.',
|
|
15928
|
+
};
|
|
15768
15929
|
/**
|
|
15769
15930
|
* Age Gate Component
|
|
15770
15931
|
* Modal overlay for age verification
|
|
15771
15932
|
*/
|
|
15772
15933
|
class AgeGateComponent {
|
|
15773
15934
|
constructor() {
|
|
15935
|
+
/** Optional: the gate is a public component and works (in English) without ngx-translate. */
|
|
15936
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15774
15937
|
/**
|
|
15775
15938
|
* Visibility state
|
|
15776
15939
|
*/
|
|
@@ -15821,7 +15984,11 @@ class AgeGateComponent {
|
|
|
15821
15984
|
if (this.warningMessage) {
|
|
15822
15985
|
return this.warningMessage;
|
|
15823
15986
|
}
|
|
15824
|
-
return
|
|
15987
|
+
return this.t('warning', { age: this.minimumAge });
|
|
15988
|
+
}
|
|
15989
|
+
/** Translate an `age_gate.*` UI string (English fallback). */
|
|
15990
|
+
t(key, params) {
|
|
15991
|
+
return uiText(this.translate, 'age_gate.' + key, AGE_GATE_TEXT_EN[key], params);
|
|
15825
15992
|
}
|
|
15826
15993
|
/**
|
|
15827
15994
|
* Handle verification submit
|
|
@@ -15830,7 +15997,7 @@ class AgeGateComponent {
|
|
|
15830
15997
|
this.errorMessage = '';
|
|
15831
15998
|
// Validate inputs
|
|
15832
15999
|
if (!this.birthMonth || !this.birthDay || !this.birthYear) {
|
|
15833
|
-
this.errorMessage =
|
|
16000
|
+
this.errorMessage = this.t('error_incomplete');
|
|
15834
16001
|
return;
|
|
15835
16002
|
}
|
|
15836
16003
|
const month = parseInt(this.birthMonth, 10);
|
|
@@ -15838,30 +16005,30 @@ class AgeGateComponent {
|
|
|
15838
16005
|
const year = parseInt(this.birthYear, 10);
|
|
15839
16006
|
// Validate ranges
|
|
15840
16007
|
if (month < 1 || month > 12) {
|
|
15841
|
-
this.errorMessage = '
|
|
16008
|
+
this.errorMessage = this.t('error_month');
|
|
15842
16009
|
return;
|
|
15843
16010
|
}
|
|
15844
16011
|
if (day < 1 || day > 31) {
|
|
15845
|
-
this.errorMessage = '
|
|
16012
|
+
this.errorMessage = this.t('error_day', { max: 31 });
|
|
15846
16013
|
return;
|
|
15847
16014
|
}
|
|
15848
16015
|
const currentYear = new Date().getFullYear();
|
|
15849
16016
|
if (year < 1900 || year > currentYear) {
|
|
15850
|
-
this.errorMessage =
|
|
16017
|
+
this.errorMessage = this.t('error_year', { max: currentYear });
|
|
15851
16018
|
return;
|
|
15852
16019
|
}
|
|
15853
16020
|
// Reject calendar-impossible dates (31 Apr, 30 Feb, 29 Feb in non-leap
|
|
15854
16021
|
// years): new Date() would silently roll them over into the next month.
|
|
15855
16022
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
15856
16023
|
if (day > daysInMonth) {
|
|
15857
|
-
this.errorMessage =
|
|
16024
|
+
this.errorMessage = this.t('error_day', { max: daysInMonth });
|
|
15858
16025
|
return;
|
|
15859
16026
|
}
|
|
15860
16027
|
// Calculate age
|
|
15861
16028
|
const birthDate = new Date(year, month - 1, day);
|
|
15862
16029
|
const age = this.calculateAge(birthDate);
|
|
15863
16030
|
if (age < this.minimumAge) {
|
|
15864
|
-
this.errorMessage =
|
|
16031
|
+
this.errorMessage = this.t('error_too_young', { age: this.minimumAge });
|
|
15865
16032
|
this.verify.emit({
|
|
15866
16033
|
verified: false,
|
|
15867
16034
|
age,
|
|
@@ -15906,17 +16073,26 @@ class AgeGateComponent {
|
|
|
15906
16073
|
return age;
|
|
15907
16074
|
}
|
|
15908
16075
|
/**
|
|
15909
|
-
*
|
|
16076
|
+
* Month options, named in the current locale (Intl; cached per locale).
|
|
15910
16077
|
*/
|
|
15911
16078
|
getMonthOptions() {
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
16079
|
+
if (this.monthCache?.locale !== this.locale) {
|
|
16080
|
+
let format;
|
|
16081
|
+
try {
|
|
16082
|
+
format = new Intl.DateTimeFormat(this.locale, { month: 'long', timeZone: 'UTC' });
|
|
16083
|
+
}
|
|
16084
|
+
catch {
|
|
16085
|
+
format = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
|
|
16086
|
+
}
|
|
16087
|
+
this.monthCache = {
|
|
16088
|
+
locale: this.locale,
|
|
16089
|
+
options: Array.from({ length: 12 }, (_, index) => ({
|
|
16090
|
+
value: String(index + 1),
|
|
16091
|
+
label: format.format(Date.UTC(2000, index, 1)),
|
|
16092
|
+
})),
|
|
16093
|
+
};
|
|
16094
|
+
}
|
|
16095
|
+
return this.monthCache.options;
|
|
15920
16096
|
}
|
|
15921
16097
|
/**
|
|
15922
16098
|
* Get day options
|
|
@@ -15936,11 +16112,11 @@ class AgeGateComponent {
|
|
|
15936
16112
|
return years;
|
|
15937
16113
|
}
|
|
15938
16114
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15939
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"
|
|
16115
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15940
16116
|
}
|
|
15941
16117
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, decorators: [{
|
|
15942
16118
|
type: Component,
|
|
15943
|
-
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"
|
|
16119
|
+
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"] }]
|
|
15944
16120
|
}], propDecorators: { visible: [{
|
|
15945
16121
|
type: Input
|
|
15946
16122
|
}], minimumAge: [{
|
|
@@ -15967,8 +16143,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15967
16143
|
* priority. Opened from the toolbar's "Choices" button; follows the shell's
|
|
15968
16144
|
* modal contract (visible input + close output).
|
|
15969
16145
|
*/
|
|
16146
|
+
/** English fallback for the `branch_chooser.*` keys (see assets/i18n/en.json). */
|
|
16147
|
+
const BRANCH_TEXT_EN = {
|
|
16148
|
+
title: 'Choose your path',
|
|
16149
|
+
close: 'Close',
|
|
16150
|
+
option: 'Option {{n}}',
|
|
16151
|
+
};
|
|
15970
16152
|
class BranchChooserComponent {
|
|
15971
16153
|
constructor() {
|
|
16154
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15972
16155
|
/** Visible state */
|
|
15973
16156
|
this.visible = false;
|
|
15974
16157
|
/** Current locale (labels are already resolved by the shell). */
|
|
@@ -15980,6 +16163,10 @@ class BranchChooserComponent {
|
|
|
15980
16163
|
/** Close requested without choosing. */
|
|
15981
16164
|
this.close = new EventEmitter();
|
|
15982
16165
|
}
|
|
16166
|
+
/** Translate a `branch_chooser.*` UI string (English fallback). */
|
|
16167
|
+
t(key, params) {
|
|
16168
|
+
return uiText(this.translate, 'branch_chooser.' + key, BRANCH_TEXT_EN[key], params);
|
|
16169
|
+
}
|
|
15983
16170
|
onEscape() {
|
|
15984
16171
|
if (this.visible) {
|
|
15985
16172
|
this.close.emit();
|
|
@@ -15994,11 +16181,11 @@ class BranchChooserComponent {
|
|
|
15994
16181
|
return choice.index;
|
|
15995
16182
|
}
|
|
15996
16183
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15997
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16184
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15998
16185
|
}
|
|
15999
16186
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, decorators: [{
|
|
16000
16187
|
type: Component,
|
|
16001
|
-
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16188
|
+
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"] }]
|
|
16002
16189
|
}], propDecorators: { visible: [{
|
|
16003
16190
|
type: Input
|
|
16004
16191
|
}], locale: [{
|
|
@@ -16607,8 +16794,11 @@ function createTranslateLoader(http) {
|
|
|
16607
16794
|
class PlayerShellComponent {
|
|
16608
16795
|
constructor() {
|
|
16609
16796
|
/**
|
|
16610
|
-
* Emitted when a reader acts on the paywall overlay
|
|
16611
|
-
*
|
|
16797
|
+
* Emitted when a reader acts on the paywall overlay: `purchase` /
|
|
16798
|
+
* `subscribe` (with the chosen option's `productId` — a product id or a
|
|
16799
|
+
* subscription tier), `login` or `dismiss`. The host owns checkout — the
|
|
16800
|
+
* player never talks to a payment provider itself; call
|
|
16801
|
+
* `refreshEntitlements()` (or pass a new `entitlementSnapshot`) afterwards.
|
|
16612
16802
|
*/
|
|
16613
16803
|
this.paywallAction = new EventEmitter();
|
|
16614
16804
|
/**
|
|
@@ -16890,6 +17080,10 @@ class PlayerShellComponent {
|
|
|
16890
17080
|
this.translationService = inject(TranslationService);
|
|
16891
17081
|
this.canvasCamera = inject(CanvasCameraService);
|
|
16892
17082
|
this.paywallService = inject(PaywallService);
|
|
17083
|
+
/** ngOnInit ran: later input changes are live updates, not initial values. */
|
|
17084
|
+
this.initialized = false;
|
|
17085
|
+
/** The state subscriptions are set up once per instance, not per (re)load. */
|
|
17086
|
+
this.stateSubscribed = false;
|
|
16893
17087
|
}
|
|
16894
17088
|
/**
|
|
16895
17089
|
* Effective reduced-motion state for every transition the shell drives
|
|
@@ -16903,6 +17097,7 @@ class PlayerShellComponent {
|
|
|
16903
17097
|
* Initialize component
|
|
16904
17098
|
*/
|
|
16905
17099
|
ngOnInit() {
|
|
17100
|
+
this.initialized = true;
|
|
16906
17101
|
this.toolbarVisible = this.showToolbar;
|
|
16907
17102
|
this.watchReducedMotion();
|
|
16908
17103
|
this.subscribeToVideoSignals();
|
|
@@ -16913,6 +17108,100 @@ class PlayerShellComponent {
|
|
|
16913
17108
|
window.addEventListener('pagehide', this.onPageHide);
|
|
16914
17109
|
}
|
|
16915
17110
|
}
|
|
17111
|
+
/**
|
|
17112
|
+
* Inputs changed after init. The first round (before ngOnInit) is skipped:
|
|
17113
|
+
* ngOnInit reads every input itself, so nothing loads twice.
|
|
17114
|
+
*/
|
|
17115
|
+
ngOnChanges(changes) {
|
|
17116
|
+
if (!this.initialized) {
|
|
17117
|
+
return;
|
|
17118
|
+
}
|
|
17119
|
+
if (changes['manifest'] || changes['manifestUrl']) {
|
|
17120
|
+
// A new work: reload re-reads every other input as well.
|
|
17121
|
+
void this.reload();
|
|
17122
|
+
return;
|
|
17123
|
+
}
|
|
17124
|
+
if (changes['locale']) {
|
|
17125
|
+
this.applyLocale();
|
|
17126
|
+
}
|
|
17127
|
+
if (changes['entitlementSnapshot']) {
|
|
17128
|
+
if (!this.entitlementSnapshot && !this.httpEntitlement) {
|
|
17129
|
+
this.paywallService.setSnapshot(null);
|
|
17130
|
+
}
|
|
17131
|
+
this.refreshEntitlements(this.entitlementSnapshot).catch(() => undefined);
|
|
17132
|
+
}
|
|
17133
|
+
if (changes['viewModeOverride']) {
|
|
17134
|
+
this.applyViewModeOverride();
|
|
17135
|
+
}
|
|
17136
|
+
if (changes['showToolbar']) {
|
|
17137
|
+
this.toolbarVisible = this.showToolbar;
|
|
17138
|
+
}
|
|
17139
|
+
if (changes['autoplay'] && this.autoplay !== this.autoplayEnabled) {
|
|
17140
|
+
this.onToggleAutoplay();
|
|
17141
|
+
}
|
|
17142
|
+
else if ((changes['secondsPerPanel'] || changes['reducedMotion']) && this.autoplayEnabled) {
|
|
17143
|
+
// Re-arm with the new timing / video start modes.
|
|
17144
|
+
this.startAutoplay();
|
|
17145
|
+
}
|
|
17146
|
+
this.cdr.markForCheck();
|
|
17147
|
+
}
|
|
17148
|
+
/**
|
|
17149
|
+
* Reload the work from the current `manifest` / `manifestUrl` inputs
|
|
17150
|
+
* (also the error screen's Retry). Story state of the previous load —
|
|
17151
|
+
* position, variables except persistent ones, open overlays, autoplay —
|
|
17152
|
+
* is reset; the analytics session of the previous work is closed.
|
|
17153
|
+
*/
|
|
17154
|
+
async reload() {
|
|
17155
|
+
this.stopAutoplay();
|
|
17156
|
+
this.autoplayEnabled = false;
|
|
17157
|
+
this.videoSequencer.reset();
|
|
17158
|
+
this.panelAudio.stopAll();
|
|
17159
|
+
this.endAnalyticsSession();
|
|
17160
|
+
this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
|
|
17161
|
+
this.lastTrackedPanelId = undefined;
|
|
17162
|
+
this.closePaywall();
|
|
17163
|
+
this.closeAgeGate();
|
|
17164
|
+
this.tocVisible = this.settingsVisible = this.languageModalVisible = this.charactersVisible = false;
|
|
17165
|
+
this.extrasVisible = this.shareVisible = this.commentsVisible = this.thumbnailsVisible = false;
|
|
17166
|
+
this.actionModalVisible = this.branchChooserVisible = false;
|
|
17167
|
+
this.variantOverrides.clear();
|
|
17168
|
+
this.variableStore.setDefinitions([]);
|
|
17169
|
+
for (const scope of ['global', 'chapter', 'page', 'session']) {
|
|
17170
|
+
this.variableStore.resetScope(scope);
|
|
17171
|
+
}
|
|
17172
|
+
this.currentChapter = this.currentPanel = this.currentPage = this.effectivePanel = undefined;
|
|
17173
|
+
this.viewMode = 'panel';
|
|
17174
|
+
this.visitedPanelIds = [];
|
|
17175
|
+
this.httpEntitlement = undefined;
|
|
17176
|
+
this.hasError = false;
|
|
17177
|
+
this.errorMessage = '';
|
|
17178
|
+
await this.initializePlayer();
|
|
17179
|
+
}
|
|
17180
|
+
/** Content locale + GUI language follow `locale`. */
|
|
17181
|
+
applyLocale() {
|
|
17182
|
+
this.playerState.setLocale(this.locale);
|
|
17183
|
+
this.translationService.setLanguage(this.locale);
|
|
17184
|
+
}
|
|
17185
|
+
/** Apply a changed `viewModeOverride` to the running view. */
|
|
17186
|
+
applyViewModeOverride() {
|
|
17187
|
+
const before = this.viewMode;
|
|
17188
|
+
if (this.viewModeOverride === 'panel' && this.viewMode === 'page') {
|
|
17189
|
+
this.viewMode = 'panel';
|
|
17190
|
+
this.videoSequencer.reset();
|
|
17191
|
+
}
|
|
17192
|
+
else {
|
|
17193
|
+
this.updateCanvasAvailability();
|
|
17194
|
+
}
|
|
17195
|
+
if (this.viewMode === before) {
|
|
17196
|
+
return;
|
|
17197
|
+
}
|
|
17198
|
+
if (this.viewMode === 'canvas') {
|
|
17199
|
+
this.jumpCameraToCurrentPanel();
|
|
17200
|
+
}
|
|
17201
|
+
if (this.autoplayEnabled) {
|
|
17202
|
+
this.startAutoplay();
|
|
17203
|
+
}
|
|
17204
|
+
}
|
|
16916
17205
|
/**
|
|
16917
17206
|
* Cleanup on destroy
|
|
16918
17207
|
*/
|
|
@@ -17029,8 +17318,7 @@ class PlayerShellComponent {
|
|
|
17029
17318
|
this.variableStore.seed(context);
|
|
17030
17319
|
}
|
|
17031
17320
|
// Set initial locale and configure translations
|
|
17032
|
-
this.
|
|
17033
|
-
this.translationService.setLanguage(this.locale);
|
|
17321
|
+
this.applyLocale();
|
|
17034
17322
|
// Check if page view is available
|
|
17035
17323
|
const loadedManifest = this.manifestService.getManifest();
|
|
17036
17324
|
if (loadedManifest?.chapters) {
|
|
@@ -17065,12 +17353,19 @@ class PlayerShellComponent {
|
|
|
17065
17353
|
}
|
|
17066
17354
|
// Subscribe to state changes
|
|
17067
17355
|
this.subscribeToStateChanges();
|
|
17356
|
+
// The host's `autoplay` input decides at start; the reader's toolbar
|
|
17357
|
+
// toggle takes over afterwards.
|
|
17358
|
+
if (this.autoplay && !this.autoplayEnabled) {
|
|
17359
|
+
this.onToggleAutoplay();
|
|
17360
|
+
}
|
|
17068
17361
|
this.loading = false;
|
|
17069
17362
|
this.ready.emit();
|
|
17070
17363
|
}
|
|
17071
17364
|
catch (err) {
|
|
17072
17365
|
this.handleError(err);
|
|
17073
17366
|
}
|
|
17367
|
+
// Async completion (e.g. a reload from ngOnChanges): repaint the OnPush view.
|
|
17368
|
+
this.cdr.markForCheck();
|
|
17074
17369
|
}
|
|
17075
17370
|
/**
|
|
17076
17371
|
* Configure the TrackingService from the manifest's `tracking` section:
|
|
@@ -17225,6 +17520,10 @@ class PlayerShellComponent {
|
|
|
17225
17520
|
* Subscribe to state changes
|
|
17226
17521
|
*/
|
|
17227
17522
|
subscribeToStateChanges() {
|
|
17523
|
+
if (this.stateSubscribed) {
|
|
17524
|
+
return;
|
|
17525
|
+
}
|
|
17526
|
+
this.stateSubscribed = true;
|
|
17228
17527
|
// Listen to panel changes
|
|
17229
17528
|
this.playerState.currentPanel$
|
|
17230
17529
|
.pipe(takeUntil(this.destroy$))
|
|
@@ -17294,8 +17593,11 @@ class PlayerShellComponent {
|
|
|
17294
17593
|
* @param transition - Optional transition to render for this panel change
|
|
17295
17594
|
* (already resolved against the outputPresets default). Omitted for
|
|
17296
17595
|
* non-adjacent jumps (TOC, initial load), which swap instantly.
|
|
17596
|
+
* @param mutations - Variable mutations of the traversed edge. Applied only
|
|
17597
|
+
* once the navigation is allowed (after the paywall / age gate), so a
|
|
17598
|
+
* blocked attempt never changes story state.
|
|
17297
17599
|
*/
|
|
17298
|
-
async navigateToPanel(chapterId, panelId, transition, cameraMove) {
|
|
17600
|
+
async navigateToPanel(chapterId, panelId, transition, cameraMove, mutations) {
|
|
17299
17601
|
try {
|
|
17300
17602
|
this.viewportTransition = transition ?? null;
|
|
17301
17603
|
// Check entitlement. A host-supplied adapter still wins — it is the
|
|
@@ -17304,10 +17606,11 @@ class PlayerShellComponent {
|
|
|
17304
17606
|
if (this.entitlementAdapter) {
|
|
17305
17607
|
const hasAccess = await this.entitlementAdapter.hasAccess(panelId);
|
|
17306
17608
|
if (!hasAccess) {
|
|
17307
|
-
this.openPaywall(this.paywallService.gateFor(panelId) ?? {
|
|
17609
|
+
this.openPaywall(this.paywallService.gateFor(panelId, this.locale) ?? {
|
|
17308
17610
|
scope: 'panel',
|
|
17309
17611
|
refId: panelId,
|
|
17310
|
-
reason:
|
|
17612
|
+
reason: PaywallService.readerMessage(null),
|
|
17613
|
+
lockReason: 'entitlement_required',
|
|
17311
17614
|
});
|
|
17312
17615
|
return;
|
|
17313
17616
|
}
|
|
@@ -17318,10 +17621,10 @@ class PlayerShellComponent {
|
|
|
17318
17621
|
// the last panel they were entitled to see.
|
|
17319
17622
|
if (this.paywallService.evaluate(panelId).reason === 'age_verification_required') {
|
|
17320
17623
|
// An age gate is answered in-player (birth date), not by checkout.
|
|
17321
|
-
this.openAgeGate(chapterId, panelId, transition, cameraMove);
|
|
17624
|
+
this.openAgeGate(chapterId, panelId, transition, cameraMove, mutations);
|
|
17322
17625
|
return;
|
|
17323
17626
|
}
|
|
17324
|
-
const gate = this.paywallService.gateFor(panelId);
|
|
17627
|
+
const gate = this.paywallService.gateFor(panelId, this.locale);
|
|
17325
17628
|
if (gate) {
|
|
17326
17629
|
this.openPaywall(gate);
|
|
17327
17630
|
return;
|
|
@@ -17335,6 +17638,10 @@ class PlayerShellComponent {
|
|
|
17335
17638
|
if (!panelData) {
|
|
17336
17639
|
throw new Error(`Panel not found: ${panelId}`);
|
|
17337
17640
|
}
|
|
17641
|
+
// Edge `action` mutations (schema: "applied when traversing this edge").
|
|
17642
|
+
if (mutations?.length) {
|
|
17643
|
+
this.variableStore.applyMutations(mutations, { chapterId });
|
|
17644
|
+
}
|
|
17338
17645
|
const chapterChanged = this.currentChapter !== chapter;
|
|
17339
17646
|
const leavingPanelId = this.getCurrentPanelId();
|
|
17340
17647
|
this.currentChapter = chapter;
|
|
@@ -17469,7 +17776,8 @@ class PlayerShellComponent {
|
|
|
17469
17776
|
buildExtrasList(manifest) {
|
|
17470
17777
|
// Media (images / video / audio / thumbnails) resolve through the asset
|
|
17471
17778
|
// catalog; see utils/extras-utils.ts for the ExtraBlock mapping.
|
|
17472
|
-
|
|
17779
|
+
// An extras-scoped paywall rule the reader does not satisfy locks its block.
|
|
17780
|
+
this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId)).map((extra) => (this.paywallService.isExtraLocked(extra.id) ? { ...extra, gated: true } : extra));
|
|
17473
17781
|
}
|
|
17474
17782
|
/**
|
|
17475
17783
|
* Canvas view is available when the current chapter has a canvas layout
|
|
@@ -17529,12 +17837,7 @@ class PlayerShellComponent {
|
|
|
17529
17837
|
return;
|
|
17530
17838
|
const result = this.flowEngine.getNextPanel(this.currentChapter.graph, currentPanelId, context, this.flowEngine.getDefaultTransition(this.manifestService.getManifest()?.settings), this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
17531
17839
|
if (result.nextPanelId) {
|
|
17532
|
-
|
|
17533
|
-
// edge") were returned by the flow engine but never applied here.
|
|
17534
|
-
if (result.action?.length) {
|
|
17535
|
-
this.variableStore.applyMutations(result.action, { chapterId: this.currentChapter.id });
|
|
17536
|
-
}
|
|
17537
|
-
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove);
|
|
17840
|
+
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove, result.action);
|
|
17538
17841
|
}
|
|
17539
17842
|
}
|
|
17540
17843
|
catch (err) {
|
|
@@ -17663,16 +17966,7 @@ class PlayerShellComponent {
|
|
|
17663
17966
|
else if (newMode === 'canvas') {
|
|
17664
17967
|
this.viewMode = 'canvas';
|
|
17665
17968
|
this.videoSequencer.reset();
|
|
17666
|
-
|
|
17667
|
-
if (this.currentChapter) {
|
|
17668
|
-
const panelId = this.getCurrentPanelId();
|
|
17669
|
-
if (panelId) {
|
|
17670
|
-
const placement = this.currentChapter.canvas?.placements?.[panelId];
|
|
17671
|
-
if (placement) {
|
|
17672
|
-
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
17673
|
-
}
|
|
17674
|
-
}
|
|
17675
|
-
}
|
|
17969
|
+
this.jumpCameraToCurrentPanel();
|
|
17676
17970
|
}
|
|
17677
17971
|
else {
|
|
17678
17972
|
// Switch to panel view
|
|
@@ -17685,6 +17979,14 @@ class PlayerShellComponent {
|
|
|
17685
17979
|
}
|
|
17686
17980
|
console.log('View mode:', this.viewMode);
|
|
17687
17981
|
}
|
|
17982
|
+
/** Land the canvas camera on the current panel (entering canvas view). */
|
|
17983
|
+
jumpCameraToCurrentPanel() {
|
|
17984
|
+
const panelId = this.getCurrentPanelId();
|
|
17985
|
+
const placement = panelId ? this.currentChapter?.canvas?.placements?.[panelId] : undefined;
|
|
17986
|
+
if (placement) {
|
|
17987
|
+
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
17988
|
+
}
|
|
17989
|
+
}
|
|
17688
17990
|
/**
|
|
17689
17991
|
* Tap on a placed panel in canvas view. Traverses the matching out-edge of
|
|
17690
17992
|
* the current panel when its condition passes (the graph still gates
|
|
@@ -17707,7 +18009,7 @@ class PlayerShellComponent {
|
|
|
17707
18009
|
if (edge) {
|
|
17708
18010
|
this.navigationAttempt.emit({ direction: 'panel', target: panelId });
|
|
17709
18011
|
void this.navigateToPanel(this.currentChapter.id, panelId, edge.transition, edge.cameraMove ??
|
|
17710
|
-
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
18012
|
+
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings), edge.action);
|
|
17711
18013
|
return;
|
|
17712
18014
|
}
|
|
17713
18015
|
if (this.visitedPanelIds.includes(panelId)) {
|
|
@@ -17715,6 +18017,14 @@ class PlayerShellComponent {
|
|
|
17715
18017
|
void this.navigateToPanel(this.currentChapter.id, panelId);
|
|
17716
18018
|
}
|
|
17717
18019
|
}
|
|
18020
|
+
/**
|
|
18021
|
+
* The manifest actually loaded — from `manifest` or fetched via
|
|
18022
|
+
* `manifestUrl`. Everything that lists the work (ToC, thumbnails) binds to
|
|
18023
|
+
* this, never to the `manifest` input, which is unset for URL loading.
|
|
18024
|
+
*/
|
|
18025
|
+
get loadedManifest() {
|
|
18026
|
+
return this.manifestService.getManifest() ?? undefined;
|
|
18027
|
+
}
|
|
17718
18028
|
/** Balloon defaults for the canvas stage (same source as the viewport). */
|
|
17719
18029
|
get manifestBalloonConfig() {
|
|
17720
18030
|
return this.manifestService.getManifest()?.settings?.typography?.balloon_config ?? null;
|
|
@@ -17728,12 +18038,9 @@ class PlayerShellComponent {
|
|
|
17728
18038
|
return this.manifestService.getManifest()?.settings?.preload ?? null;
|
|
17729
18039
|
}
|
|
17730
18040
|
onLocaleChange(locale) {
|
|
17731
|
-
//
|
|
18041
|
+
// Content locale and GUI language stay in sync.
|
|
17732
18042
|
this.locale = locale;
|
|
17733
|
-
this.
|
|
17734
|
-
// Update GUI language (sync with content locale)
|
|
17735
|
-
this.translationService.setLanguage(locale);
|
|
17736
|
-
console.log('Locale changed to:', locale);
|
|
18043
|
+
this.applyLocale();
|
|
17737
18044
|
}
|
|
17738
18045
|
onToggleSpeech() {
|
|
17739
18046
|
this.setSpeechEnabled(!this.speechEnabled);
|
|
@@ -18055,7 +18362,15 @@ class PlayerShellComponent {
|
|
|
18055
18362
|
*/
|
|
18056
18363
|
handleKeyboard(event) {
|
|
18057
18364
|
// Don't handle if user is typing in an input
|
|
18058
|
-
if (event.target instanceof HTMLInputElement ||
|
|
18365
|
+
if (event.target instanceof HTMLInputElement ||
|
|
18366
|
+
event.target instanceof HTMLTextAreaElement ||
|
|
18367
|
+
event.target instanceof HTMLSelectElement) {
|
|
18368
|
+
return;
|
|
18369
|
+
}
|
|
18370
|
+
// The paywall and the age gate block the story: no shortcut may move
|
|
18371
|
+
// past them. Their own Escape handling applies (paywall: "Maybe later"
|
|
18372
|
+
// dismisses; age gate: dismiss keeps the reader where they were).
|
|
18373
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18059
18374
|
return;
|
|
18060
18375
|
}
|
|
18061
18376
|
// While a dialog is open the story behind it must not react: Escape
|
|
@@ -18192,6 +18507,9 @@ class PlayerShellComponent {
|
|
|
18192
18507
|
* Handle swipe gesture
|
|
18193
18508
|
*/
|
|
18194
18509
|
onSwipe(direction) {
|
|
18510
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18511
|
+
return;
|
|
18512
|
+
}
|
|
18195
18513
|
if (direction === 'left') {
|
|
18196
18514
|
// Swipe left = navigate forward
|
|
18197
18515
|
if (this.viewMode === 'page') {
|
|
@@ -18589,9 +18907,11 @@ class PlayerShellComponent {
|
|
|
18589
18907
|
*/
|
|
18590
18908
|
onPaywallAction(action) {
|
|
18591
18909
|
const gate = this.paywallGate;
|
|
18910
|
+
const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
|
|
18592
18911
|
if (gate) {
|
|
18593
|
-
this.paywallAction.emit({ action, gate });
|
|
18912
|
+
this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
|
|
18594
18913
|
}
|
|
18914
|
+
this.paywallProductId = undefined;
|
|
18595
18915
|
this.closePaywall();
|
|
18596
18916
|
}
|
|
18597
18917
|
/**
|
|
@@ -18608,6 +18928,10 @@ class PlayerShellComponent {
|
|
|
18608
18928
|
await this.httpEntitlement.resolveEntitlement({ workId: manifest?.meta?.id ?? '' });
|
|
18609
18929
|
this.paywallService.setSnapshot(this.httpEntitlement.getSnapshot());
|
|
18610
18930
|
}
|
|
18931
|
+
// What the host passed replaces the snapshot wholesale; an age check the
|
|
18932
|
+
// reader already passed on this device still counts.
|
|
18933
|
+
this.restoreAgeVerification();
|
|
18934
|
+
this.buildExtrasList(this.manifestService.getManifest() ?? null);
|
|
18611
18935
|
const gatedPanelId = this.paywallGate?.refId ?? this.getCurrentPanelId();
|
|
18612
18936
|
if (gatedPanelId && this.paywallService.canAccess(gatedPanelId)) {
|
|
18613
18937
|
this.closePaywall();
|
|
@@ -18616,9 +18940,9 @@ class PlayerShellComponent {
|
|
|
18616
18940
|
// ── Age gate ──────────────────────────────────────────────────────────────
|
|
18617
18941
|
static { this.AGE_VERIFIED_STORAGE_KEY = 'pw-age-verified'; }
|
|
18618
18942
|
/** Raise the age gate for a panel and remember where the reader was going. */
|
|
18619
|
-
openAgeGate(chapterId, panelId, transition, cameraMove) {
|
|
18943
|
+
openAgeGate(chapterId, panelId, transition, cameraMove, mutations) {
|
|
18620
18944
|
this.ageGateMinimumAge = this.paywallService.ruleFor(panelId)?.minimumAge ?? 18;
|
|
18621
|
-
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove };
|
|
18945
|
+
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove, mutations };
|
|
18622
18946
|
this.ageGateVisible = true;
|
|
18623
18947
|
this.cdr.markForCheck();
|
|
18624
18948
|
}
|
|
@@ -18648,8 +18972,11 @@ class PlayerShellComponent {
|
|
|
18648
18972
|
this.ageGateVisible = false;
|
|
18649
18973
|
this.pendingAgeGatedNavigation = null;
|
|
18650
18974
|
this.cdr.markForCheck();
|
|
18651
|
-
|
|
18652
|
-
|
|
18975
|
+
// Retry the move: navigateToPanel re-checks the gate, so a rule that
|
|
18976
|
+
// also needs a purchase / subscription now raises the paywall (with its
|
|
18977
|
+
// Buy / Subscribe options) instead of letting the reader through.
|
|
18978
|
+
if (pending) {
|
|
18979
|
+
await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove, pending.mutations);
|
|
18653
18980
|
}
|
|
18654
18981
|
}
|
|
18655
18982
|
persistAgeVerification(age) {
|
|
@@ -18725,41 +19052,43 @@ class PlayerShellComponent {
|
|
|
18725
19052
|
bookmark.panelId === this.getCurrentPanelId();
|
|
18726
19053
|
}
|
|
18727
19054
|
// ── Branch choices ────────────────────────────────────────────────────────
|
|
18728
|
-
/**
|
|
19055
|
+
/**
|
|
19056
|
+
* At least two paths out of the current panel are open right now (edge
|
|
19057
|
+
* conditions evaluated exactly like the chooser does) — the toolbar shows
|
|
19058
|
+
* "Choices". A closed conditional edge does not count.
|
|
19059
|
+
*/
|
|
18729
19060
|
get hasBranchesAhead() {
|
|
18730
|
-
|
|
18731
|
-
const edges = this.currentChapter?.graph?.edges;
|
|
18732
|
-
if (!panelId || !edges) {
|
|
18733
|
-
return false;
|
|
18734
|
-
}
|
|
18735
|
-
let count = 0;
|
|
18736
|
-
for (const edge of edges) {
|
|
18737
|
-
if (edge.from === panelId && ++count > 1) {
|
|
18738
|
-
return true;
|
|
18739
|
-
}
|
|
18740
|
-
}
|
|
18741
|
-
return false;
|
|
19061
|
+
return this.openEdges().length > 1;
|
|
18742
19062
|
}
|
|
18743
|
-
/** Outgoing edges whose conditions pass right now
|
|
18744
|
-
|
|
19063
|
+
/** Outgoing edges of the current panel whose conditions pass right now. */
|
|
19064
|
+
openEdges() {
|
|
18745
19065
|
const chapter = this.currentChapter;
|
|
18746
19066
|
const panelId = this.getCurrentPanelId();
|
|
18747
|
-
if (!chapter
|
|
19067
|
+
if (!chapter || !panelId) {
|
|
18748
19068
|
return [];
|
|
18749
19069
|
}
|
|
19070
|
+
const outgoing = chapter.graph?.edges?.filter((edge) => edge.from === panelId) ?? [];
|
|
19071
|
+
if (!outgoing.some((edge) => edge.condition)) {
|
|
19072
|
+
// Nothing to evaluate: skip building a variable context on every check.
|
|
19073
|
+
return outgoing;
|
|
19074
|
+
}
|
|
18750
19075
|
const context = this.variableStore.createContext(chapter.id);
|
|
18751
|
-
return
|
|
18752
|
-
.filter((edge) => edge.from === panelId)
|
|
18753
|
-
.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context))
|
|
18754
|
-
.map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter, index) }));
|
|
19076
|
+
return outgoing.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context));
|
|
18755
19077
|
}
|
|
18756
|
-
|
|
19078
|
+
/** Open outgoing edges with reader-facing labels. */
|
|
19079
|
+
computeBranchChoices() {
|
|
19080
|
+
const chapter = this.currentChapter;
|
|
19081
|
+
return chapter
|
|
19082
|
+
? this.openEdges().map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter) }))
|
|
19083
|
+
: [];
|
|
19084
|
+
}
|
|
19085
|
+
branchLabel(edge, chapter) {
|
|
18757
19086
|
const fromEdge = this.localizedText(edge.label);
|
|
18758
19087
|
if (fromEdge) {
|
|
18759
19088
|
return fromEdge;
|
|
18760
19089
|
}
|
|
18761
|
-
|
|
18762
|
-
return
|
|
19090
|
+
// Neither: the chooser renders a translated "Option N".
|
|
19091
|
+
return this.localizedText(chapter.panels?.[edge.to]?.title);
|
|
18763
19092
|
}
|
|
18764
19093
|
localizedText(value) {
|
|
18765
19094
|
if (!value) {
|
|
@@ -18784,13 +19113,10 @@ class PlayerShellComponent {
|
|
|
18784
19113
|
to: choice.edge.to,
|
|
18785
19114
|
index: choice.index,
|
|
18786
19115
|
});
|
|
18787
|
-
|
|
18788
|
-
this.variableStore.applyMutations(choice.edge.action, { chapterId: chapter.id });
|
|
18789
|
-
}
|
|
18790
|
-
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings));
|
|
19116
|
+
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings), choice.edge.action);
|
|
18791
19117
|
}
|
|
18792
19118
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18793
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
19119
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], usesOnChanges: true, ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
18794
19120
|
}
|
|
18795
19121
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
|
|
18796
19122
|
type: Component,
|
|
@@ -18812,7 +19138,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18812
19138
|
ShareModalComponent,
|
|
18813
19139
|
CommentsDrawerComponent,
|
|
18814
19140
|
PwIconComponent
|
|
18815
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
19141
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
18816
19142
|
}], propDecorators: { manifest: [{
|
|
18817
19143
|
type: Input
|
|
18818
19144
|
}], manifestUrl: [{
|
|
@@ -18978,5 +19304,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18978
19304
|
* Generated bundle index. Do not edit.
|
|
18979
19305
|
*/
|
|
18980
19306
|
|
|
18981
|
-
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
19307
|
+
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, chapterOrderFromManifest, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
18982
19308
|
//# sourceMappingURL=panelwave-player.mjs.map
|