@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8
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/.agents/skills/editor-capabilities/SKILL.md +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +476 -260
- package/dist/src/cli.js +362 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21603
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -828
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3654
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,1788 +0,0 @@
|
|
|
1
|
-
// /reskin/discover — the template discovery surface, MIGRATED to the REAL
|
|
2
|
-
// production feed (part of the /reskin/* UX migration; reference:
|
|
3
|
-
// job-runs-page.ts for the "render a shell → client-fetch a real endpoint"
|
|
4
|
-
// pattern, settings-page.ts for the real-data render pattern).
|
|
5
|
-
//
|
|
6
|
-
// Three server-routed modes, each its own URL, surfaced as a Feed / Swipe /
|
|
7
|
-
// Categories segmented toggle (three real <a> links):
|
|
8
|
-
// • feed → /reskin/discover/feed — real /discover/feed poster grid
|
|
9
|
-
// • swipe → /reskin/discover/swipe — real /discover/swipe/* Tinder deck
|
|
10
|
-
// • categories → /reskin/discover/categories — real /discover/popular groups
|
|
11
|
-
//
|
|
12
|
-
// Every mode renders the farmville shell + a self-contained module script that
|
|
13
|
-
// fetches the SAME real, same-origin endpoints the live React SPA hits, with
|
|
14
|
-
// credentials, and renders real poster cards (real title, real thumbnail/video
|
|
15
|
-
// from the real url, real source + duration) that fork/open into the real
|
|
16
|
-
// editor at /editor/<template_id>. Loading / empty / error states are all
|
|
17
|
-
// guarded so an offline fetch degrades to a friendly state instead of throwing.
|
|
18
|
-
//
|
|
19
|
-
// Poster-grid extras (feed + categories reveal, which share posterCardHtml):
|
|
20
|
-
// • hover audio — mousing over a card's (muted, autoplaying) video unmutes
|
|
21
|
-
// it while the pointer stays, with an autoplay-policy fallback that
|
|
22
|
-
// reverts to muted playback if the browser blocks unmuted play.
|
|
23
|
-
// • TikTok-style viewer — clicking a card opens a fullscreen vertical
|
|
24
|
-
// scroll-snap deck (swipe / wheel / arrow keys between templates, sound
|
|
25
|
-
// on, tap video to pause, mute toggle) with an "Open Editor" CTA per
|
|
26
|
-
// slide. The card's "Open Editor" button still deep-links the editor.
|
|
27
|
-
// The viewer is shared: feed swipes the (filtered) feed, categories
|
|
28
|
-
// swipes the revealed group.
|
|
29
|
-
// • bookmarks — one-tap bookmark on each viewer slide + on cards on hover,
|
|
30
|
-
// persisted client-side in localStorage ('rk-discover-bookmarks'; there
|
|
31
|
-
// is no backend favorite endpoint), plus a "bookmarked only" toolbar
|
|
32
|
-
// filter chip on the feed so saved templates are findable.
|
|
33
|
-
// • no platform pill on posters (user feedback) — source only shows as the
|
|
34
|
-
// small meta line inside the viewer slide.
|
|
35
|
-
//
|
|
36
|
-
// The renderer keeps its `mode` signature (so the existing zero-arg
|
|
37
|
-
// `/reskin/discover/:mode` route keeps working — the client fetches the live
|
|
38
|
-
// endpoints same-origin using the browser session cookie) and adds an optional
|
|
39
|
-
// 2nd `input` arg (defaulted) so the parent route can, if it wants, pass the
|
|
40
|
-
// signed-in account id (→ ?account= on fetches + editor links, matching the
|
|
41
|
-
// live editorHref) and override any endpoint.
|
|
42
|
-
import { reskinDocument, rkEscape } from "./document.js";
|
|
43
|
-
import { POPULAR_INSPIRATION_GROUPS } from "../frontend/homepage-shared.js";
|
|
44
|
-
/** Config-only defaults (the data fields feedInitial/feedFetchedAt are seeded
|
|
45
|
-
* per-request, never defaulted). */
|
|
46
|
-
const DEFAULT_INPUT = {
|
|
47
|
-
accountId: "",
|
|
48
|
-
isLoggedIn: false,
|
|
49
|
-
feedEndpoint: "/discover/feed",
|
|
50
|
-
popularEndpoint: "/discover/popular",
|
|
51
|
-
swipeStackEndpoint: "/discover/swipe/stack",
|
|
52
|
-
swipeRefillEndpoint: "/discover/swipe/refill",
|
|
53
|
-
swipeBase: "/discover/swipe",
|
|
54
|
-
editorBase: "/editor"
|
|
55
|
-
};
|
|
56
|
-
const MODES = [
|
|
57
|
-
{ mode: "feed", label: "Feed" },
|
|
58
|
-
{ mode: "swipe", label: "Swipe" },
|
|
59
|
-
{ mode: "categories", label: "Categories" }
|
|
60
|
-
];
|
|
61
|
-
const MODE_COPY = {
|
|
62
|
-
feed: {
|
|
63
|
-
sub: "Browse proven short-form formats, then open one in the editor and make it yours in an afternoon."
|
|
64
|
-
},
|
|
65
|
-
swipe: {
|
|
66
|
-
sub: "One template at a time — keep the ones that fit and we'll recut them into ads for your brand."
|
|
67
|
-
},
|
|
68
|
-
categories: {
|
|
69
|
-
sub: "Jump straight to the style you need — every short-form format, neatly grouped."
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
// Functional (non-decorative) SVG glyphs.
|
|
73
|
-
const EDIT_ICON = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>`;
|
|
74
|
-
const COPY_ICON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>`;
|
|
75
|
-
const IDCHECK_ICON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"/></svg>`;
|
|
76
|
-
const BOOKMARK_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21l-5-3.4L7 21V5.2A2.2 2.2 0 0 1 9.2 3h5.6A2.2 2.2 0 0 1 17 5.2z"/></svg>`;
|
|
77
|
-
const CHECK_ICON = `<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"/></svg>`;
|
|
78
|
-
const CROSS_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6L6 18"/></svg>`;
|
|
79
|
-
const SOUND_ON_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M18.5 6a9 9 0 0 1 0 12"/></svg>`;
|
|
80
|
-
const SOUND_OFF_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9.5 5 5M21 9.5l-5 5"/></svg>`;
|
|
81
|
-
const CHEV_UP_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 14.5 7-7 7 7"/></svg>`;
|
|
82
|
-
const CHEV_DOWN_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 9.5 7 7 7-7"/></svg>`;
|
|
83
|
-
const PLAY_ICON = `<svg width="56" height="56" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.4v13.2L19 12z"/></svg>`;
|
|
84
|
-
const PLUS_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
85
|
-
const UPLOAD_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/></svg>`;
|
|
86
|
-
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
87
|
-
function jsonForScript(value) {
|
|
88
|
-
return JSON.stringify(value ?? null)
|
|
89
|
-
.replace(/</g, "\\u003c")
|
|
90
|
-
.replace(/>/g, "\\u003e")
|
|
91
|
-
.replace(/&/g, "\\u0026")
|
|
92
|
-
.replace(/\u2028/g, "\\u2028")
|
|
93
|
-
.replace(/\u2029/g, "\\u2029");
|
|
94
|
-
}
|
|
95
|
-
// The segmented Feed / Swipe / Categories toggle (three real links).
|
|
96
|
-
function modeToggle(active) {
|
|
97
|
-
const links = MODES.map((m) => {
|
|
98
|
-
const on = m.mode === active ? " is-active" : "";
|
|
99
|
-
return `<a class="rk-discover-mode${on}" href="/discover/${m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
100
|
-
}).join("");
|
|
101
|
-
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
102
|
-
}
|
|
103
|
-
// Server-rendered category skeleton from the real POPULAR_INSPIRATION_GROUPS
|
|
104
|
-
// manifest — always renders (real group labels + real format counts) even
|
|
105
|
-
// before/without the /discover/popular fetch, so categories mode is never blank
|
|
106
|
-
// and degrades gracefully offline. The client enriches each card with real
|
|
107
|
-
// thumbnails + wires an in-page reveal of that group's real templates.
|
|
108
|
-
function categorySkeleton() {
|
|
109
|
-
return POPULAR_INSPIRATION_GROUPS.map((g) => {
|
|
110
|
-
const count = g.items.length;
|
|
111
|
-
const blurb = g.items.slice(0, 4).map((i) => i.title).join(" · ");
|
|
112
|
-
const fallbackQuery = encodeURIComponent(g.label);
|
|
113
|
-
// Placeholder thumb tiles (no media yet) keep the card height stable; the
|
|
114
|
-
// client swaps in real poster media once /discover/popular resolves.
|
|
115
|
-
const thumbs = Array.from({ length: 4 })
|
|
116
|
-
.map(() => `<span class="rk-discover-mini rk-discover-mini-ph"></span>`)
|
|
117
|
-
.join("");
|
|
118
|
-
return `<a class="rk-discover-cat rk-card-hover" href="/discover?q=${fallbackQuery}" data-rk-cat="${rkEscape(g.label)}">
|
|
119
|
-
<div class="rk-discover-cat-head">
|
|
120
|
-
<div class="rk-discover-cat-titlewrap">
|
|
121
|
-
<h3 class="rk-discover-cat-title">${rkEscape(g.label)}</h3>
|
|
122
|
-
<p class="rk-discover-cat-count" data-rk-cat-count>${count} format${count === 1 ? "" : "s"}</p>
|
|
123
|
-
</div>
|
|
124
|
-
<span class="rk-discover-cat-arrow" aria-hidden="true">→</span>
|
|
125
|
-
</div>
|
|
126
|
-
<div class="rk-discover-cat-thumbs" data-rk-cat-thumbs>${thumbs}</div>
|
|
127
|
-
<p class="rk-discover-cat-blurb">${rkEscape(blurb)}</p>
|
|
128
|
-
</a>`;
|
|
129
|
-
}).join("");
|
|
130
|
-
}
|
|
131
|
-
// The fullscreen TikTok-style viewer shell — shared by feed and categories
|
|
132
|
-
// (the client script fills [data-viewer-track] with slides on open).
|
|
133
|
-
function viewerHtml() {
|
|
134
|
-
return `
|
|
135
|
-
<div class="rk-discover-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Template viewer">
|
|
136
|
-
<div class="rk-discover-viewer-scrim" data-viewer-close></div>
|
|
137
|
-
<div class="rk-discover-viewer-track" data-viewer-track></div>
|
|
138
|
-
<div class="rk-discover-viewer-ui">
|
|
139
|
-
<button type="button" class="rk-discover-viewer-btn" data-viewer-mute aria-label="Toggle sound"><span class="rk-dvm-on">${SOUND_ON_ICON}</span><span class="rk-dvm-off">${SOUND_OFF_ICON}</span></button>
|
|
140
|
-
<button type="button" class="rk-discover-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
141
|
-
</div>
|
|
142
|
-
<div class="rk-discover-viewer-nav">
|
|
143
|
-
<button type="button" class="rk-discover-viewer-btn" data-viewer-prev aria-label="Previous template">${CHEV_UP_ICON}</button>
|
|
144
|
-
<button type="button" class="rk-discover-viewer-btn" data-viewer-next aria-label="Next template">${CHEV_DOWN_ICON}</button>
|
|
145
|
-
</div>
|
|
146
|
-
</div>`;
|
|
147
|
-
}
|
|
148
|
-
// The fullscreen TikTok-style viewer for the SWIPE deck — a sibling of the feed
|
|
149
|
-
// viewer above but purpose-built for swiping: slides carry live composition
|
|
150
|
-
// iframes (not video/img), and a bottom action bar reject/approves the ACTIVE
|
|
151
|
-
// card, driving the same POST /discover/swipe/:id/{approve,reject} the deck uses.
|
|
152
|
-
// The client script (swipe block) fills [data-sv-track] with slides on open.
|
|
153
|
-
function swipeViewerHtml() {
|
|
154
|
-
return `
|
|
155
|
-
<div class="rk-discover-viewer rk-swipe-viewer" data-rk-swipe-viewer hidden role="dialog" aria-modal="true" aria-label="Swipe preview">
|
|
156
|
-
<div class="rk-discover-viewer-scrim" data-sv-close></div>
|
|
157
|
-
<div class="rk-discover-viewer-track rk-swipe-viewer-track" data-sv-track></div>
|
|
158
|
-
<div class="rk-discover-viewer-ui">
|
|
159
|
-
<button type="button" class="rk-discover-viewer-btn" data-sv-mute aria-label="Toggle sound"><span class="rk-dvm-on">${SOUND_ON_ICON}</span><span class="rk-dvm-off">${SOUND_OFF_ICON}</span></button>
|
|
160
|
-
<button type="button" class="rk-discover-viewer-btn" data-sv-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="rk-discover-viewer-nav">
|
|
163
|
-
<button type="button" class="rk-discover-viewer-btn" data-sv-prev aria-label="Previous">${CHEV_UP_ICON}</button>
|
|
164
|
-
<button type="button" class="rk-discover-viewer-btn" data-sv-next aria-label="Next">${CHEV_DOWN_ICON}</button>
|
|
165
|
-
</div>
|
|
166
|
-
<div class="rk-swipe-viewer-actions" data-sv-actions>
|
|
167
|
-
<button type="button" class="rk-discover-act rk-discover-act-skip" data-sv-reject aria-label="Reject">${CROSS_ICON}</button>
|
|
168
|
-
<button type="button" class="rk-discover-act rk-discover-act-fork" data-sv-approve aria-label="Approve">${CHECK_ICON}</button>
|
|
169
|
-
</div>
|
|
170
|
-
</div>`;
|
|
171
|
-
}
|
|
172
|
-
// The "Add New" modal (feed mode only) — paste a social video URL or upload a
|
|
173
|
-
// video, adding it to the caller's private Discover feed via the same real
|
|
174
|
-
// endpoints the old React SPA used (POST /discover/templates + the upload
|
|
175
|
-
// presign pair). Mirrors the old UI's Add Template modal, farmville-styled.
|
|
176
|
-
function addModalHtml() {
|
|
177
|
-
return `
|
|
178
|
-
<div class="rk-add-modal" data-rk-add-modal hidden role="dialog" aria-modal="true" aria-labelledby="rk-add-title">
|
|
179
|
-
<div class="rk-add-scrim" data-rk-add-close></div>
|
|
180
|
-
<div class="rk-add-card">
|
|
181
|
-
<div class="rk-add-head">
|
|
182
|
-
<h2 class="rk-add-title" id="rk-add-title">Add to your feed</h2>
|
|
183
|
-
<button type="button" class="rk-add-x" data-rk-add-close aria-label="Close">${CROSS_ICON}</button>
|
|
184
|
-
</div>
|
|
185
|
-
<p class="rk-add-sub">Paste a TikTok, YouTube, Twitter/X, or Instagram video link — or upload a video from your computer. It's added to your Discover feed, visible only to your account.</p>
|
|
186
|
-
<form class="rk-add-form" data-rk-add-form>
|
|
187
|
-
<div class="rk-add-field">
|
|
188
|
-
<label class="rk-add-label" for="rk-add-url">Video URL</label>
|
|
189
|
-
<input class="rk-add-input" id="rk-add-url" type="url" placeholder="https://www.tiktok.com/@…" autocomplete="off" data-rk-add-url />
|
|
190
|
-
</div>
|
|
191
|
-
<div class="rk-add-or"><span>or</span></div>
|
|
192
|
-
<label class="rk-add-file" data-rk-add-filelabel>
|
|
193
|
-
<input type="file" accept="video/*,.mp4,.mov,.webm,.m4v" data-rk-add-file />
|
|
194
|
-
<span class="rk-add-file-ic">${UPLOAD_ICON}</span>
|
|
195
|
-
<span class="rk-add-file-text" data-rk-add-filetext>Upload a video<span class="rk-add-optional">MP4 / MOV / WebM, up to 200 MB</span></span>
|
|
196
|
-
</label>
|
|
197
|
-
<div class="rk-add-field">
|
|
198
|
-
<label class="rk-add-label" for="rk-add-tagline">Tagline <span class="rk-add-optional">optional</span></label>
|
|
199
|
-
<input class="rk-add-input" id="rk-add-tagline" type="text" placeholder="What makes it work" autocomplete="off" data-rk-add-tagline />
|
|
200
|
-
</div>
|
|
201
|
-
<div class="rk-add-field">
|
|
202
|
-
<label class="rk-add-label" for="rk-add-notes">Notes <span class="rk-add-optional">optional</span></label>
|
|
203
|
-
<textarea class="rk-add-input rk-add-textarea" id="rk-add-notes" rows="2" placeholder="Anything to remember about this one" data-rk-add-notes></textarea>
|
|
204
|
-
</div>
|
|
205
|
-
<p class="rk-add-error" data-rk-add-error hidden></p>
|
|
206
|
-
<div class="rk-add-actions">
|
|
207
|
-
<button type="button" class="rk-btn rk-btn-ghost" data-rk-add-close>Cancel</button>
|
|
208
|
-
<button type="submit" class="rk-btn rk-btn-gold" data-rk-add-submit>Add to feed</button>
|
|
209
|
-
</div>
|
|
210
|
-
</form>
|
|
211
|
-
</div>
|
|
212
|
-
</div>`;
|
|
213
|
-
}
|
|
214
|
-
export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
215
|
-
const copy = MODE_COPY[mode];
|
|
216
|
-
// feedInitial/feedFetchedAt only make sense in feed mode; drop them elsewhere
|
|
217
|
-
// so swipe/categories boots stay lean.
|
|
218
|
-
const { feedInitial, feedFetchedAt, ...cfg } = input;
|
|
219
|
-
const boot = {
|
|
220
|
-
...DEFAULT_INPUT,
|
|
221
|
-
...cfg,
|
|
222
|
-
feedInitial: mode === "feed" ? feedInitial ?? null : null,
|
|
223
|
-
feedFetchedAt: mode === "feed" ? feedFetchedAt ?? null : null
|
|
224
|
-
};
|
|
225
|
-
let modeBody = "";
|
|
226
|
-
// ── FEED ────────────────────────────────────────────────────────────────
|
|
227
|
-
if (mode === "feed") {
|
|
228
|
-
modeBody = `
|
|
229
|
-
<div class="rk-discover-feed" data-rk-feed>
|
|
230
|
-
<div class="rk-discover-grid" data-rk-grid></div>
|
|
231
|
-
<div class="rk-discover-state" data-rk-state>Loading templates…</div>
|
|
232
|
-
</div>${viewerHtml()}`;
|
|
233
|
-
}
|
|
234
|
-
// Feed's quick search + bookmarked-only chip share the toolbar row with the
|
|
235
|
-
// mode toggle.
|
|
236
|
-
const toolbarSearch = mode === "feed"
|
|
237
|
-
? `<label class="rk-discover-search">
|
|
238
|
-
<span class="rk-discover-search-ic" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8.5" cy="8.5" r="5.8"/><path d="m13 13 4.2 4.2"/></svg></span>
|
|
239
|
-
<input type="search" data-rk-search placeholder="Search templates" autocomplete="off" aria-label="Search templates" />
|
|
240
|
-
</label>
|
|
241
|
-
<button type="button" class="rk-discover-bmfilter" data-rk-bmfilter aria-pressed="false" title="Show bookmarked templates only">${BOOKMARK_ICON}<span class="rk-discover-bmfilter-count" data-rk-bmcount></span></button>`
|
|
242
|
-
: "";
|
|
243
|
-
// Categories' in-page group reveal header ("← All categories" + the group
|
|
244
|
-
// title) lives IN the toolbar row, next to the mode toggle (user feedback);
|
|
245
|
-
// the client unhides it while a group is revealed.
|
|
246
|
-
const toolbarReveal = mode === "categories"
|
|
247
|
-
? `<div class="rk-discover-reveal-head" data-rk-reveal-head hidden>
|
|
248
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-cat-back>← All categories</button>
|
|
249
|
-
<h2 class="rk-discover-reveal-title" data-rk-reveal-title></h2>
|
|
250
|
-
</div>`
|
|
251
|
-
: "";
|
|
252
|
-
// ── SWIPE ───────────────────────────────────────────────────────────────
|
|
253
|
-
if (mode === "swipe") {
|
|
254
|
-
modeBody = `
|
|
255
|
-
<div class="rk-discover-swipe-wrap" data-rk-swipe>
|
|
256
|
-
<div class="rk-discover-stage">
|
|
257
|
-
<button type="button" class="rk-discover-act rk-discover-act-skip" data-swipe-skip data-swipe-act hidden aria-label="Skip">${CROSS_ICON}</button>
|
|
258
|
-
<div class="rk-discover-deck" data-deck>
|
|
259
|
-
<button type="button" class="rk-discover-sound-toggle" data-swipe-sound hidden aria-label="Unmute" aria-pressed="false">${SOUND_OFF_ICON}</button>
|
|
260
|
-
<div class="rk-discover-swipe-state" data-rk-swipe-state>Loading your deck…</div>
|
|
261
|
-
</div>
|
|
262
|
-
<button type="button" class="rk-discover-act rk-discover-act-fork" data-swipe-fork data-swipe-act hidden aria-label="Fork">${CHECK_ICON}</button>
|
|
263
|
-
</div>
|
|
264
|
-
<p class="rk-discover-act-hint" data-actions-hint hidden>Tap the card to preview full-screen. Swipe to customize into an ad for your brand.</p>
|
|
265
|
-
</div>${swipeViewerHtml()}`;
|
|
266
|
-
}
|
|
267
|
-
// ── CATEGORIES ──────────────────────────────────────────────────────────
|
|
268
|
-
if (mode === "categories") {
|
|
269
|
-
modeBody = `
|
|
270
|
-
<div class="rk-discover-catwrap" data-rk-cats>
|
|
271
|
-
<div class="rk-discover-cats" data-rk-cats-grid>${categorySkeleton()}</div>
|
|
272
|
-
<section class="rk-discover-cat-reveal" data-rk-cat-reveal hidden>
|
|
273
|
-
<div class="rk-discover-grid" data-rk-reveal-grid></div>
|
|
274
|
-
</section>
|
|
275
|
-
<div class="rk-discover-state rk-discover-cats-state" data-rk-cats-state hidden></div>
|
|
276
|
-
</div>${viewerHtml()}`;
|
|
277
|
-
}
|
|
278
|
-
// "Add New" lives top-right of the page head (feed mode only) — opens the
|
|
279
|
-
// Add modal when signed in, else routes to /login (mirrors the old UI).
|
|
280
|
-
const addNewBtn = mode === "feed"
|
|
281
|
-
? (boot.isLoggedIn
|
|
282
|
-
? `<button type="button" class="rk-btn rk-btn-gold rk-discover-add" data-rk-add>${PLUS_ICON}<span>Add New</span></button>`
|
|
283
|
-
: `<a class="rk-btn rk-btn-gold rk-discover-add" href="/login">${PLUS_ICON}<span>Add New</span></a>`)
|
|
284
|
-
: "";
|
|
285
|
-
const body = `
|
|
286
|
-
<main class="rk-container rk-page">
|
|
287
|
-
<div class="rk-page-head rk-discover-head">
|
|
288
|
-
<div class="rk-discover-headtext">
|
|
289
|
-
<h1 class="rk-h1">Discover</h1>
|
|
290
|
-
<p class="rk-sub">${copy.sub}</p>
|
|
291
|
-
</div>
|
|
292
|
-
${addNewBtn}
|
|
293
|
-
</div>
|
|
294
|
-
<div class="rk-discover-toolbar">
|
|
295
|
-
${modeToggle(mode)}
|
|
296
|
-
${toolbarReveal}
|
|
297
|
-
${toolbarSearch}
|
|
298
|
-
</div>
|
|
299
|
-
${modeBody}
|
|
300
|
-
${mode === "feed" ? addModalHtml() : ""}
|
|
301
|
-
<script type="application/json" data-rk-discover-boot>${jsonForScript(boot)}</script>
|
|
302
|
-
</main>`;
|
|
303
|
-
const pageCss = `
|
|
304
|
-
/* head row: title block on the left, "Add New" CTA pinned top-right */
|
|
305
|
-
.rk-discover-head{margin-bottom:22px;display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
|
|
306
|
-
.rk-discover-headtext{display:grid;gap:4px;min-width:0}
|
|
307
|
-
.rk-discover-add{flex:none;align-self:flex-start}
|
|
308
|
-
.rk-discover-add svg{margin-top:-1px}
|
|
309
|
-
@media(max-width:520px){
|
|
310
|
-
.rk-discover-add span{display:none}
|
|
311
|
-
.rk-discover-add{padding:11px}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* ── "Add New" modal ── */
|
|
315
|
-
.rk-add-modal{position:fixed;inset:0;z-index:140;display:grid;place-items:center;padding:20px}
|
|
316
|
-
.rk-add-modal[hidden]{display:none}
|
|
317
|
-
.rk-add-scrim{position:absolute;inset:0;background:rgba(15,12,5,.5);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
|
|
318
|
-
.rk-add-card{position:relative;z-index:1;width:min(480px,100%);max-height:calc(100vh - 40px);overflow:auto;
|
|
319
|
-
background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-xl);padding:24px 24px 22px}
|
|
320
|
-
.rk-add-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px}
|
|
321
|
-
.rk-add-title{font-family:var(--rk-font-display);font-size:var(--rk-text-xl);font-weight:800;color:var(--rk-ink);letter-spacing:-.02em}
|
|
322
|
-
.rk-add-x{flex:none;width:34px;height:34px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);
|
|
323
|
-
display:grid;place-items:center;cursor:pointer;transition:color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
324
|
-
.rk-add-x:hover{color:var(--rk-ink);border-color:var(--rk-border-strong)}
|
|
325
|
-
.rk-add-x svg{width:16px;height:16px}
|
|
326
|
-
.rk-add-sub{font-size:13px;color:var(--rk-text-muted);line-height:1.5;margin-bottom:18px}
|
|
327
|
-
.rk-add-form{display:grid;gap:14px}
|
|
328
|
-
.rk-add-field{display:grid;gap:6px}
|
|
329
|
-
.rk-add-label{font-size:12.5px;font-weight:700;color:var(--rk-n-700)}
|
|
330
|
-
.rk-add-optional{font-weight:500;color:var(--rk-text-faint)}
|
|
331
|
-
.rk-add-input{width:100%;box-sizing:border-box;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);
|
|
332
|
-
background:#fff;padding:11px 14px;font-family:var(--rk-font-body);font-size:14px;color:var(--rk-ink);outline:none;
|
|
333
|
-
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
334
|
-
.rk-add-input::placeholder{color:var(--rk-n-400)}
|
|
335
|
-
.rk-add-input:focus{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
336
|
-
.rk-add-textarea{resize:vertical;min-height:52px;line-height:1.45}
|
|
337
|
-
.rk-add-or{display:flex;align-items:center;gap:12px;color:var(--rk-text-faint);font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
338
|
-
.rk-add-or::before,.rk-add-or::after{content:"";height:1px;flex:1;background:var(--rk-border)}
|
|
339
|
-
.rk-add-file{display:flex;align-items:center;gap:12px;border:1px dashed var(--rk-border-strong);border-radius:var(--rk-r-lg);
|
|
340
|
-
padding:13px 14px;cursor:pointer;color:var(--rk-n-700);
|
|
341
|
-
transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
342
|
-
.rk-add-file:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
343
|
-
.rk-add-file input[type=file]{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
|
|
344
|
-
.rk-add-file-ic{flex:none;color:var(--rk-gold-700);display:grid;place-items:center}
|
|
345
|
-
.rk-add-file-text{font-size:13.5px;font-weight:600;display:grid;gap:2px;min-width:0}
|
|
346
|
-
.rk-add-file-text .rk-add-optional{font-size:11.5px;font-weight:500}
|
|
347
|
-
.rk-add-file.is-picked{border-style:solid;border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
348
|
-
.rk-add-error{font-size:13px;color:#b91c1c;background:var(--rk-red-tint);border:1px solid #fecaca;border-radius:var(--rk-r-lg);padding:9px 12px;font-weight:500;line-height:1.4}
|
|
349
|
-
.rk-add-error[hidden]{display:none}
|
|
350
|
-
.rk-add-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
351
|
-
.rk-add-actions .rk-btn[disabled]{opacity:.6;cursor:default;transform:none}
|
|
352
|
-
|
|
353
|
-
/* toolbar row: mode toggle + (feed) quick search share one line, wrap on mobile */
|
|
354
|
-
.rk-discover-toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:26px}
|
|
355
|
-
|
|
356
|
-
/* segmented Feed / Swipe / Categories toggle — three real links, gold active */
|
|
357
|
-
.rk-discover-modes{display:inline-flex;flex:none;gap:4px;background:var(--rk-n-100);border:1px solid var(--rk-border);
|
|
358
|
-
border-radius:var(--rk-r-full);padding:4px;box-shadow:var(--rk-shadow-xs);max-width:100%;overflow:auto;scrollbar-width:none}
|
|
359
|
-
.rk-discover-modes::-webkit-scrollbar{display:none}
|
|
360
|
-
.rk-discover-mode{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:var(--rk-r-full);
|
|
361
|
-
font-size:14px;font-weight:600;color:var(--rk-n-600);white-space:nowrap;
|
|
362
|
-
transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
363
|
-
.rk-discover-mode:hover{color:var(--rk-ink)}
|
|
364
|
-
.rk-discover-mode.is-active{background:var(--rk-gold-500);color:var(--rk-ink);font-weight:700;box-shadow:var(--rk-shadow-sm)}
|
|
365
|
-
|
|
366
|
-
/* ── feed: quick search (lives in the toolbar row) + poster grid ── */
|
|
367
|
-
.rk-discover-search{flex:1;min-width:220px;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--rk-border-strong);
|
|
368
|
-
border-radius:var(--rk-r-full);padding:10px 16px;box-shadow:var(--rk-shadow-xs);
|
|
369
|
-
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
370
|
-
.rk-discover-search:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
371
|
-
.rk-discover-search-ic{color:var(--rk-text-faint);display:grid;place-items:center;flex:none}
|
|
372
|
-
.rk-discover-search input{flex:1;min-width:0;border:0;outline:none;background:transparent;font-family:var(--rk-font-body);font-size:14px;color:var(--rk-ink)}
|
|
373
|
-
.rk-discover-search input::placeholder{color:var(--rk-n-400)}
|
|
374
|
-
|
|
375
|
-
.rk-discover-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:22px}
|
|
376
|
-
@media(max-width:520px){.rk-discover-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}}
|
|
377
|
-
.rk-discover-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);overflow:hidden;box-shadow:var(--rk-shadow-card);display:flex;flex-direction:column;min-width:0}
|
|
378
|
-
.rk-discover-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:var(--rk-n-100);color:inherit}
|
|
379
|
-
.rk-discover-poster img,.rk-discover-poster video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
380
|
-
.rk-discover-media-empty{position:absolute;inset:0;display:grid;place-items:center;font-size:12.5px;font-weight:600;color:var(--rk-text-faint);
|
|
381
|
-
background:repeating-linear-gradient(135deg,var(--rk-n-100),var(--rk-n-100) 12px,var(--rk-n-50) 12px,var(--rk-n-50) 24px)}
|
|
382
|
-
.rk-discover-dur{position:absolute;top:12px;right:12px;z-index:3;font-family:var(--rk-font-body);font-size:11.5px;font-weight:700;color:#fff;
|
|
383
|
-
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);padding:4px 9px;border-radius:var(--rk-r-full)}
|
|
384
|
-
.rk-discover-scrim{position:absolute;inset:0;z-index:1;background:linear-gradient(to top,rgba(0,0,0,.28) 0%,transparent 42%)}
|
|
385
|
-
/* wrap = positioning context so the bookmark button can sit NEXT TO (not
|
|
386
|
-
inside) the poster anchor — a button inside an <a> is invalid HTML */
|
|
387
|
-
.rk-discover-posterwrap{position:relative}
|
|
388
|
-
.rk-discover-bm{position:absolute;right:10px;bottom:10px;z-index:4;width:34px;height:34px;border:0;border-radius:var(--rk-r-full);
|
|
389
|
-
display:none;place-items:center;color:#fff;background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);cursor:pointer;
|
|
390
|
-
transition:transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
391
|
-
.rk-discover-posterwrap:hover .rk-discover-bm{display:grid}
|
|
392
|
-
/* touch has no hover, so always show the bookmark button there (else you can
|
|
393
|
-
never save a not-yet-bookmarked card from the grid) */
|
|
394
|
-
@media(hover:none){.rk-discover-bm{display:grid}}
|
|
395
|
-
.rk-discover-bm:hover{transform:scale(1.08);background:rgba(0,0,0,.6)}
|
|
396
|
-
.rk-discover-bm.is-on{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
397
|
-
.rk-discover-bm.is-on svg{fill:currentColor}
|
|
398
|
-
.rk-discover-body{padding:15px 16px 16px;display:grid;gap:13px;min-width:0}
|
|
399
|
-
.rk-discover-titlewrap{display:grid;gap:6px;min-width:0}
|
|
400
|
-
.rk-discover-title{font-family:var(--rk-font-display);font-size:1.02rem;font-weight:700;letter-spacing:-.01em;color:var(--rk-ink);line-height:1.2;
|
|
401
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
402
|
-
.rk-discover-desc{font-size:12.5px;font-weight:500;color:var(--rk-text-muted);line-height:1.4;
|
|
403
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
404
|
-
.rk-discover-idchip{justify-self:start;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;
|
|
405
|
-
padding:3px 8px;border-radius:999px;background:var(--rk-surface-2,rgba(0,0,0,.05));
|
|
406
|
-
font-family:var(--rk-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;font-weight:600;
|
|
407
|
-
color:var(--rk-text-muted);cursor:pointer;border:0;line-height:1.4;transition:background .15s,color .15s;
|
|
408
|
-
-webkit-user-select:none;user-select:none;box-sizing:border-box}
|
|
409
|
-
.rk-discover-idchip:hover{background:var(--rk-surface-3,rgba(0,0,0,.09));color:var(--rk-ink)}
|
|
410
|
-
.rk-discover-idchip svg{flex:0 0 auto;opacity:.7}
|
|
411
|
-
.rk-discover-idchip .rk-idchip-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
412
|
-
.rk-discover-idchip.is-copied{background:rgba(52,168,110,.16);color:#1f7a4d}
|
|
413
|
-
.rk-discover-idchip.is-copied svg{opacity:1;color:#1f7a4d}
|
|
414
|
-
.rk-discover-fork{gap:7px}
|
|
415
|
-
.rk-discover-fork svg{margin-top:-1px}
|
|
416
|
-
.rk-discover-fork-disabled{opacity:.6;cursor:default;pointer-events:none}
|
|
417
|
-
.rk-discover-badges{display:flex;flex-wrap:wrap;gap:6px}
|
|
418
|
-
|
|
419
|
-
/* hover-to-unmute speaker badge (video posters only): hidden until hover,
|
|
420
|
-
flips to a gold "sound on" chip once the browser actually allows audio */
|
|
421
|
-
.rk-discover-sound{position:absolute;left:12px;bottom:12px;z-index:3;width:32px;height:32px;display:none;place-items:center;color:#fff;
|
|
422
|
-
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border-radius:var(--rk-r-full);pointer-events:none}
|
|
423
|
-
.rk-discover-sound span{display:grid;place-items:center;line-height:0}
|
|
424
|
-
.rk-discover-posterwrap:hover .rk-discover-sound{display:grid}
|
|
425
|
-
.rk-discover-sound .rk-ds-on{display:none}
|
|
426
|
-
.rk-discover-posterwrap.is-sound .rk-discover-sound{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
427
|
-
.rk-discover-posterwrap.is-sound .rk-ds-on{display:grid}
|
|
428
|
-
.rk-discover-posterwrap.is-sound .rk-ds-off{display:none}
|
|
429
|
-
|
|
430
|
-
/* bookmarked-only toolbar chip (feed) — count span collapses when empty */
|
|
431
|
-
.rk-discover-bmfilter{flex:none;display:inline-flex;align-items:center;gap:7px;padding:10px 15px;border-radius:var(--rk-r-full);
|
|
432
|
-
border:1px solid var(--rk-border-strong);background:#fff;color:var(--rk-n-600);font-family:var(--rk-font-body);font-size:13px;font-weight:700;
|
|
433
|
-
cursor:pointer;box-shadow:var(--rk-shadow-xs);
|
|
434
|
-
transition:color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
435
|
-
.rk-discover-bmfilter:hover{color:var(--rk-ink);border-color:var(--rk-gold-600)}
|
|
436
|
-
.rk-discover-bmfilter.is-active{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
437
|
-
.rk-discover-bmfilter.is-active svg{fill:currentColor}
|
|
438
|
-
.rk-discover-bmfilter-count:empty{display:none}
|
|
439
|
-
|
|
440
|
-
/* load / empty / error state (feed + categories) */
|
|
441
|
-
.rk-discover-state{padding:48px 16px;text-align:center;color:var(--rk-text-muted);font-size:14px;
|
|
442
|
-
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs)}
|
|
443
|
-
.rk-discover-state[hidden]{display:none}
|
|
444
|
-
.rk-discover-state.is-error{color:#b91c1c;background:var(--rk-red-tint);border-color:#fecaca;font-weight:500}
|
|
445
|
-
.rk-discover-state .rk-btn{margin-top:14px}
|
|
446
|
-
|
|
447
|
-
/* ── feed: TikTok-style fullscreen viewer (click a card → vertical deck) ── */
|
|
448
|
-
html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
|
|
449
|
-
.rk-discover-viewer{position:fixed;inset:0;z-index:120}
|
|
450
|
-
.rk-discover-viewer[hidden]{display:none}
|
|
451
|
-
.rk-discover-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
|
452
|
-
.rk-discover-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
|
|
453
|
-
.rk-discover-viewer-track::-webkit-scrollbar{display:none}
|
|
454
|
-
.rk-discover-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
|
|
455
|
-
.rk-discover-viewer-stage{position:relative;width:min(92vw,calc(93vh * 9 / 16));aspect-ratio:9/16;max-height:100%;margin:0;
|
|
456
|
-
border-radius:var(--rk-r-3xl);overflow:hidden;background:#000;box-shadow:var(--rk-shadow-xl)}
|
|
457
|
-
.rk-discover-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
|
458
|
-
video.rk-discover-viewer-media{cursor:pointer}
|
|
459
|
-
.rk-discover-viewer-pause{position:absolute;inset:0;z-index:2;display:none;place-items:center;color:rgba(255,255,255,.92);pointer-events:none;
|
|
460
|
-
filter:drop-shadow(0 4px 14px rgba(0,0,0,.55))}
|
|
461
|
-
.rk-discover-viewer-slide.is-paused .rk-discover-viewer-pause{display:grid}
|
|
462
|
-
.rk-discover-viewer-shade{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:1;pointer-events:none;
|
|
463
|
-
background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.22) 62%,transparent 100%)}
|
|
464
|
-
/* right padding reserves the bookmark rail so text never runs under it */
|
|
465
|
-
.rk-discover-viewer-info{position:absolute;left:0;right:0;bottom:0;z-index:3;padding:20px 78px 20px 20px;display:grid;gap:7px;justify-items:start;pointer-events:none}
|
|
466
|
-
.rk-discover-viewer-bm{position:absolute;right:14px;bottom:22px;z-index:4;width:48px;height:48px;border-radius:var(--rk-r-full);
|
|
467
|
-
border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.14);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
|
|
468
|
-
color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
469
|
-
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
470
|
-
.rk-discover-viewer-bm:hover{background:rgba(255,255,255,.26);transform:scale(1.06)}
|
|
471
|
-
.rk-discover-viewer-bm.is-on{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
472
|
-
.rk-discover-viewer-bm.is-on svg{fill:currentColor}
|
|
473
|
-
.rk-discover-viewer-meta{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:rgba(255,255,255,.72)}
|
|
474
|
-
.rk-discover-viewer-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.12rem;line-height:1.2;letter-spacing:-.01em;color:#fff}
|
|
475
|
-
.rk-discover-viewer-desc{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4;
|
|
476
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
477
|
-
.rk-discover-viewer-fork{pointer-events:auto;gap:7px;margin-top:5px}
|
|
478
|
-
.rk-discover-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
|
|
479
|
-
.rk-discover-viewer-btn{width:44px;height:44px;border-radius:var(--rk-r-full);border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.12);
|
|
480
|
-
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
481
|
-
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
482
|
-
.rk-discover-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
|
|
483
|
-
.rk-discover-viewer-btn span{display:grid;place-items:center;line-height:0}
|
|
484
|
-
.rk-discover-viewer .rk-dvm-off{display:none}
|
|
485
|
-
.rk-discover-viewer.is-muted .rk-dvm-on{display:none}
|
|
486
|
-
.rk-discover-viewer.is-muted .rk-dvm-off{display:grid}
|
|
487
|
-
.rk-discover-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
|
|
488
|
-
@media(hover:none){.rk-discover-viewer-nav{display:none}}
|
|
489
|
-
@media(max-width:640px){
|
|
490
|
-
.rk-discover-viewer-nav{display:none}
|
|
491
|
-
.rk-discover-viewer-slide{padding:0}
|
|
492
|
-
.rk-discover-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/* ── swipe: single-card deck ── */
|
|
496
|
-
.rk-discover-swipe-wrap{display:grid;justify-items:center;gap:18px;padding:4px 0 8px}
|
|
497
|
-
.rk-discover-deck{position:relative;width:min(322px,84vw);aspect-ratio:9/16;margin:6px 0}
|
|
498
|
-
.rk-discover-swipe-state{position:absolute;inset:0;z-index:1;display:grid;place-content:center;justify-items:center;gap:10px;text-align:center;padding:28px;
|
|
499
|
-
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);color:var(--rk-text-muted);font-size:14px}
|
|
500
|
-
.rk-discover-swipe-state[hidden]{display:none}
|
|
501
|
-
.rk-discover-swipe-state.is-error{color:#b91c1c}
|
|
502
|
-
.rk-discover-swipe-state-title{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:700;color:var(--rk-ink)}
|
|
503
|
-
.rk-discover-swipe-state .rk-btn{margin-top:6px}
|
|
504
|
-
.rk-discover-spinner{width:30px;height:30px;border-radius:50%;border:3px solid var(--rk-gold-tint2);border-top-color:var(--rk-gold-700);animation:rk-discover-spin .8s linear infinite}
|
|
505
|
-
@keyframes rk-discover-spin{to{transform:rotate(360deg)}}
|
|
506
|
-
.rk-discover-swipe{position:absolute;inset:0;border-radius:var(--rk-r-3xl);overflow:hidden;border:1px solid var(--rk-border);background:#0b0b0b;
|
|
507
|
-
box-shadow:var(--rk-shadow-xl);transform-origin:center bottom;will-change:transform;
|
|
508
|
-
transition:transform .38s var(--rk-ease),opacity .38s var(--rk-ease)}
|
|
509
|
-
.rk-discover-swipe-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;pointer-events:none;background:#000}
|
|
510
|
-
.rk-discover-swipe-fallback{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;
|
|
511
|
-
background:linear-gradient(155deg,#3a3a3a,#111);color:#fff;font-family:var(--rk-font-display);font-weight:700;font-size:1.1rem;line-height:1.15;
|
|
512
|
-
transition:opacity .4s var(--rk-ease)}
|
|
513
|
-
.rk-discover-swipe-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.62) 0%,rgba(0,0,0,.16) 46%,transparent 74%)}
|
|
514
|
-
.rk-discover-swipe-info{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:20px;display:grid;gap:6px;pointer-events:none}
|
|
515
|
-
.rk-discover-swipe-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.05rem;color:#fff;letter-spacing:-.01em;line-height:1.18}
|
|
516
|
-
.rk-discover-swipe-caption{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.84);line-height:1.35;
|
|
517
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
518
|
-
/* audio toggle — overlays the top-of-deck card, top-right; global mute state */
|
|
519
|
-
.rk-discover-sound-toggle{position:absolute;top:12px;right:12px;z-index:40;width:38px;height:38px;border-radius:var(--rk-r-full);
|
|
520
|
-
border:0;display:grid;place-items:center;cursor:pointer;color:#fff;background:rgba(15,15,15,.5);backdrop-filter:blur(6px);
|
|
521
|
-
-webkit-backdrop-filter:blur(6px);box-shadow:0 2px 8px -2px rgba(0,0,0,.5);
|
|
522
|
-
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
523
|
-
.rk-discover-sound-toggle[hidden]{display:none}
|
|
524
|
-
.rk-discover-sound-toggle:hover{background:rgba(15,15,15,.72);transform:scale(1.06)}
|
|
525
|
-
.rk-discover-sound-toggle.is-on{background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
526
|
-
.rk-discover-sound-toggle.is-on:hover{background:var(--rk-gold-700)}
|
|
527
|
-
|
|
528
|
-
/* deck flanked by skip/fork — buttons ride the card's vertical center, 1fr gutters absorb spare width.
|
|
529
|
-
Explicit column placement keeps the deck centered when the buttons are display:none (empty/caught-up states). */
|
|
530
|
-
.rk-discover-stage{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;width:100%}
|
|
531
|
-
.rk-discover-deck{grid-column:2}
|
|
532
|
-
.rk-discover-act{width:60px;height:60px;flex:none;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);background:#fff;
|
|
533
|
-
display:grid;place-items:center;cursor:pointer;box-shadow:var(--rk-shadow-md);
|
|
534
|
-
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
535
|
-
.rk-discover-act[hidden]{display:none}
|
|
536
|
-
.rk-discover-act-skip{grid-column:1;justify-self:end}
|
|
537
|
-
.rk-discover-act-fork{grid-column:3;justify-self:start}
|
|
538
|
-
/* no side room on small phones — card spans, buttons drop under it as a pair */
|
|
539
|
-
@media(max-width:560px){
|
|
540
|
-
.rk-discover-stage{grid-template-columns:auto auto;grid-template-areas:"deck deck" "skip fork";justify-content:center;column-gap:26px;row-gap:16px}
|
|
541
|
-
.rk-discover-deck{grid-area:deck}
|
|
542
|
-
.rk-discover-act-skip{grid-area:skip;justify-self:center}
|
|
543
|
-
.rk-discover-act-fork{grid-area:fork;justify-self:center}
|
|
544
|
-
}
|
|
545
|
-
/* short viewports (landscape phones, small laptops): the deck is sized by width
|
|
546
|
-
with a locked 9/16 ratio → ~570px tall, taller than the screen, so the card +
|
|
547
|
-
its flanking Skip/Fork controls fall off-screen. Cap it by HEIGHT instead and
|
|
548
|
-
let width follow the ratio (mirrors how the fullscreen viewer stage sizes). */
|
|
549
|
-
@media(max-height:720px){
|
|
550
|
-
.rk-discover-deck{height:min(74vh,572px);width:auto}
|
|
551
|
-
}
|
|
552
|
-
@media(max-height:520px){
|
|
553
|
-
.rk-discover-head{margin-bottom:10px}
|
|
554
|
-
.rk-discover-swipe-wrap{gap:10px;padding:0}
|
|
555
|
-
}
|
|
556
|
-
.rk-discover-act:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
557
|
-
.rk-discover-act:disabled{opacity:.5;cursor:default;transform:none}
|
|
558
|
-
.rk-discover-act-skip{color:var(--rk-n-500)}
|
|
559
|
-
.rk-discover-act-skip:hover{color:var(--rk-red);border-color:var(--rk-red-tint)}
|
|
560
|
-
.rk-discover-act-fork{background:var(--rk-gold-500);color:var(--rk-ink);border-color:transparent}
|
|
561
|
-
.rk-discover-act-fork:hover{background:var(--rk-gold-700);box-shadow:0 8px 22px -6px rgba(252,185,0,.6)}
|
|
562
|
-
.rk-discover-act-hint{font-size:12.5px;font-weight:600;color:var(--rk-text-faint);text-align:center;letter-spacing:.01em}
|
|
563
|
-
.rk-discover-act-hint[hidden]{display:none}
|
|
564
|
-
/* tapping the front card opens the fullscreen swipe viewer */
|
|
565
|
-
.rk-discover-deck .rk-discover-swipe{cursor:pointer}
|
|
566
|
-
/* ── swipe: fullscreen TikTok viewer — reuses .rk-discover-viewer* shell; slides
|
|
567
|
-
hold live composition iframes (.rk-discover-swipe-frame) and a bottom bar
|
|
568
|
-
reject/approves the active card. */
|
|
569
|
-
.rk-swipe-viewer-stage{background:#000}
|
|
570
|
-
.rk-swipe-viewer-slide .rk-discover-swipe-frame{border-radius:inherit}
|
|
571
|
-
.rk-swipe-viewer .rk-discover-viewer-info{padding-bottom:112px}
|
|
572
|
-
.rk-swipe-viewer-actions{position:absolute;left:0;right:0;bottom:max(24px,env(safe-area-inset-bottom,0px));z-index:6;
|
|
573
|
-
display:flex;gap:40px;justify-content:center;align-items:center;pointer-events:none}
|
|
574
|
-
.rk-swipe-viewer-actions .rk-discover-act{pointer-events:auto;width:64px;height:64px}
|
|
575
|
-
.rk-swipe-viewer-actions .rk-discover-act[hidden]{display:none}
|
|
576
|
-
@media(max-width:560px){.rk-swipe-viewer-actions{gap:30px}}
|
|
577
|
-
|
|
578
|
-
/* ── categories: browse-by-format cards ── */
|
|
579
|
-
.rk-discover-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:20px}
|
|
580
|
-
@media(max-width:560px){.rk-discover-cats{grid-template-columns:1fr}}
|
|
581
|
-
.rk-discover-cats[hidden]{display:none}
|
|
582
|
-
.rk-discover-cat{display:flex;flex-direction:column;gap:14px;background:var(--rk-surface);border:1px solid var(--rk-border);
|
|
583
|
-
border-radius:var(--rk-r-3xl);padding:22px;box-shadow:var(--rk-shadow-card);color:inherit}
|
|
584
|
-
.rk-discover-cat-head{display:flex;align-items:center;gap:14px}
|
|
585
|
-
.rk-discover-cat-titlewrap{flex:1;min-width:0;display:grid;gap:2px}
|
|
586
|
-
.rk-discover-cat-title{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:700;color:var(--rk-ink);letter-spacing:-.01em}
|
|
587
|
-
.rk-discover-cat-count{font-size:12.5px;font-weight:600;color:var(--rk-text-muted)}
|
|
588
|
-
.rk-discover-cat-arrow{flex:none;font-size:18px;color:var(--rk-n-400);transition:transform var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
589
|
-
.rk-discover-cat:hover .rk-discover-cat-arrow{transform:translateX(3px);color:var(--rk-gold-700)}
|
|
590
|
-
.rk-discover-cat-blurb{font-size:13px;color:var(--rk-text-muted);line-height:1.45;
|
|
591
|
-
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
592
|
-
.rk-discover-cat-thumbs{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:8px}
|
|
593
|
-
.rk-discover-mini{position:relative;aspect-ratio:9/16;border-radius:var(--rk-r-lg);overflow:hidden;background:var(--rk-n-100);box-shadow:var(--rk-shadow-xs)}
|
|
594
|
-
.rk-discover-mini img,.rk-discover-mini video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
595
|
-
.rk-discover-mini-ph{background:linear-gradient(135deg,var(--rk-n-100),var(--rk-n-200))}
|
|
596
|
-
|
|
597
|
-
.rk-discover-cat-reveal[hidden]{display:none}
|
|
598
|
-
/* reveal header shares the toolbar row with the mode toggle (user feedback) */
|
|
599
|
-
.rk-discover-reveal-head{display:flex;align-items:center;gap:12px;min-width:0}
|
|
600
|
-
.rk-discover-reveal-head[hidden]{display:none}
|
|
601
|
-
.rk-discover-reveal-title{font-family:var(--rk-font-display);font-size:var(--rk-text-xl);font-weight:800;color:var(--rk-ink);letter-spacing:-.02em;
|
|
602
|
-
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
603
|
-
.rk-discover-cats-state[hidden]{display:none}
|
|
604
|
-
`;
|
|
605
|
-
const script = `
|
|
606
|
-
(function(){
|
|
607
|
-
var root=document;
|
|
608
|
-
var bootEl=root.querySelector('[data-rk-discover-boot]');
|
|
609
|
-
var boot={};
|
|
610
|
-
try{ boot=JSON.parse(bootEl&&bootEl.textContent?bootEl.textContent:'{}')||{}; }catch(e){ boot={}; }
|
|
611
|
-
var feedEndpoint=boot.feedEndpoint||'/discover/feed';
|
|
612
|
-
var popularEndpoint=boot.popularEndpoint||'/discover/popular';
|
|
613
|
-
var swipeStackEndpoint=boot.swipeStackEndpoint||'/discover/swipe/stack';
|
|
614
|
-
var swipeRefillEndpoint=boot.swipeRefillEndpoint||'/discover/swipe/refill';
|
|
615
|
-
var swipeBase=boot.swipeBase||'/discover/swipe';
|
|
616
|
-
var editorBase=boot.editorBase||'/editor';
|
|
617
|
-
var accountId=boot.accountId||'';
|
|
618
|
-
|
|
619
|
-
function esc(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
620
|
-
function attr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
621
|
-
function isVideo(u){ return Boolean(u)&&/\\.(mp4|webm|ogg|mov)(\\?|#|$)/i.test(u); }
|
|
622
|
-
// previewUrl falls back to the template's raw videoUrl, which is often a
|
|
623
|
-
// signed/extension-less URL — so treat "not a known image" as video, never
|
|
624
|
-
// the other way round, or those templates render a broken <img> (no preview).
|
|
625
|
-
function isImage(u){ return Boolean(u)&&/\\.(png|jpe?g|gif|webp|avif|svg)(\\?|#|$)/i.test(u); }
|
|
626
|
-
function withAccount(u){
|
|
627
|
-
try{ var url=new URL(u, window.location.href); if(accountId) url.searchParams.set('account', accountId); return url.toString(); }
|
|
628
|
-
catch(e){ return u; }
|
|
629
|
-
}
|
|
630
|
-
function editorUrl(id){
|
|
631
|
-
var u=editorBase+'/'+encodeURIComponent(String(id));
|
|
632
|
-
return accountId? u+'?account='+encodeURIComponent(accountId) : u;
|
|
633
|
-
}
|
|
634
|
-
// Feed cards browse INSPIRATIONS: an entry may have no template yet. The real
|
|
635
|
-
// template id (if decomposed) opens the editor directly; otherwise the card
|
|
636
|
-
// carries an inspirationId and its CTA decomposes on demand (preview → Remix).
|
|
637
|
-
function realTemplateId(t){ var id=t.templateId||t.template_id||''; return /^template_/.test(String(id))? String(id):''; }
|
|
638
|
-
function inspirationIdOf(t){ return String(t.inspirationId||t.inspiration_id||''); }
|
|
639
|
-
function domId(t){ return realTemplateId(t)||inspirationIdOf(t)||''; }
|
|
640
|
-
function needsDecompose(t){ return !realTemplateId(t) && Boolean(inspirationIdOf(t)); }
|
|
641
|
-
// POST the lazy decompose, then hand off to the editor. Shared by card +
|
|
642
|
-
// viewer Remix buttons via a delegated [data-remix] listener.
|
|
643
|
-
function remixInspiration(btn){
|
|
644
|
-
var inspId=btn.getAttribute('data-remix'); if(!inspId||btn.getAttribute('data-busy')) return;
|
|
645
|
-
btn.setAttribute('data-busy','1');
|
|
646
|
-
var label=btn.innerHTML; btn.innerHTML='Preparing…';
|
|
647
|
-
fetch(withAccount('/discover/inspirations/'+encodeURIComponent(inspId)+'/decompose'),{method:'POST',credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
648
|
-
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); })
|
|
649
|
-
.then(function(res){
|
|
650
|
-
if(res.ok&&res.data&&res.data.templateId){ window.location.href=editorUrl(res.data.templateId); return; }
|
|
651
|
-
btn.removeAttribute('data-busy'); btn.innerHTML=label;
|
|
652
|
-
window.alert((res.data&&res.data.error)||'Could not open this inspiration — please try again.');
|
|
653
|
-
})
|
|
654
|
-
.catch(function(){ btn.removeAttribute('data-busy'); btn.innerHTML=label; window.alert('Network error — please try again.'); });
|
|
655
|
-
}
|
|
656
|
-
document.addEventListener('click',function(e){
|
|
657
|
-
var b=e.target&&e.target.closest?e.target.closest('[data-remix]'):null;
|
|
658
|
-
if(!b) return; e.preventDefault(); remixInspiration(b);
|
|
659
|
-
});
|
|
660
|
-
// Click the template-id chip on a feed card to copy the id to the clipboard,
|
|
661
|
-
// with a brief inline "Copied" swap (icon + label) as subtle confirmation.
|
|
662
|
-
function copyText(text){
|
|
663
|
-
if(navigator.clipboard&&navigator.clipboard.writeText){ return navigator.clipboard.writeText(text); }
|
|
664
|
-
return new Promise(function(resolve,reject){
|
|
665
|
-
try{
|
|
666
|
-
var ta=document.createElement('textarea'); ta.value=text;
|
|
667
|
-
ta.style.position='fixed'; ta.style.top='-9999px'; document.body.appendChild(ta);
|
|
668
|
-
ta.focus(); ta.select(); var ok=document.execCommand('copy'); document.body.removeChild(ta);
|
|
669
|
-
ok?resolve():reject(new Error('copy failed'));
|
|
670
|
-
}catch(err){ reject(err); }
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
document.addEventListener('click',function(e){
|
|
674
|
-
var chip=e.target&&e.target.closest?e.target.closest('[data-copy-id]'):null;
|
|
675
|
-
if(!chip) return;
|
|
676
|
-
e.preventDefault(); e.stopPropagation();
|
|
677
|
-
var id=chip.getAttribute('data-copy-id')||'';
|
|
678
|
-
if(!id||chip.getAttribute('data-copied')) return;
|
|
679
|
-
copyText(id).then(function(){
|
|
680
|
-
var txt=chip.querySelector('.rk-idchip-text'); var prev=txt?txt.textContent:'';
|
|
681
|
-
chip.setAttribute('data-copied','1'); chip.classList.add('is-copied');
|
|
682
|
-
chip.innerHTML=IDCHECK_ICON+'<span class="rk-idchip-text">Copied</span>';
|
|
683
|
-
window.setTimeout(function(){
|
|
684
|
-
chip.classList.remove('is-copied'); chip.removeAttribute('data-copied');
|
|
685
|
-
chip.innerHTML=COPY_ICON+'<span class="rk-idchip-text">'+esc(prev)+'</span>';
|
|
686
|
-
},1300);
|
|
687
|
-
}).catch(function(){});
|
|
688
|
-
});
|
|
689
|
-
function fmtDuration(s){
|
|
690
|
-
s=Number(s); if(!s||!isFinite(s)||s<=0) return '';
|
|
691
|
-
if(s>=60){ var m=Math.floor(s/60); var r=Math.round(s%60); return m+':'+(r<10?'0':'')+r; }
|
|
692
|
-
return (s>=10? s.toFixed(0) : s.toFixed(1))+'s';
|
|
693
|
-
}
|
|
694
|
-
var SOURCE_MAP={tiktok:'TikTok','tiktok.com':'TikTok',youtube:'YouTube','youtube.com':'YouTube','youtu.be':'YouTube',instagram:'Instagram','instagram.com':'Instagram',twitter:'Twitter/X',x:'Twitter/X','x.com':'Twitter/X'};
|
|
695
|
-
function sourceLabel(s){ s=String(s||'').toLowerCase(); if(!s) return ''; if(SOURCE_MAP[s]) return SOURCE_MAP[s]; var host=s.replace(/^www\\./,'').split('.')[0]; return host.charAt(0).toUpperCase()+host.slice(1); }
|
|
696
|
-
var EDIT_ICON=${JSON.stringify(EDIT_ICON)};
|
|
697
|
-
var COPY_ICON=${JSON.stringify(COPY_ICON)};
|
|
698
|
-
var IDCHECK_ICON=${JSON.stringify(IDCHECK_ICON)};
|
|
699
|
-
var BOOKMARK_ICON=${JSON.stringify(BOOKMARK_ICON)};
|
|
700
|
-
var SOUND_ON_ICON=${JSON.stringify(SOUND_ON_ICON)};
|
|
701
|
-
var SOUND_OFF_ICON=${JSON.stringify(SOUND_OFF_ICON)};
|
|
702
|
-
var PLAY_ICON=${JSON.stringify(PLAY_ICON)};
|
|
703
|
-
|
|
704
|
-
// ── bookmarks: client-side saves in localStorage (no backend favorite
|
|
705
|
-
// endpoint exists), shared by feed cards, category reveal cards and the
|
|
706
|
-
// viewer slides. One delegated document listener toggles any [data-bm].
|
|
707
|
-
var BM_KEY='rk-discover-bookmarks';
|
|
708
|
-
var bookmarks=(function(){
|
|
709
|
-
try{ var v=JSON.parse(window.localStorage.getItem(BM_KEY)||'[]'); return new Set(Array.isArray(v)?v.map(String):[]); }
|
|
710
|
-
catch(e){ return new Set(); }
|
|
711
|
-
})();
|
|
712
|
-
var bmChangeHandlers=[];
|
|
713
|
-
function isBookmarked(id){ return Boolean(id)&&bookmarks.has(String(id)); }
|
|
714
|
-
function syncBmButtons(){
|
|
715
|
-
root.querySelectorAll('[data-bm]').forEach(function(b){
|
|
716
|
-
var on=isBookmarked(b.getAttribute('data-bm'));
|
|
717
|
-
b.classList.toggle('is-on',on);
|
|
718
|
-
b.setAttribute('aria-pressed',on?'true':'false');
|
|
719
|
-
b.setAttribute('aria-label',on?'Remove bookmark':'Bookmark');
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
function toggleBookmark(id){
|
|
723
|
-
id=String(id||''); if(!id) return;
|
|
724
|
-
if(bookmarks.has(id)) bookmarks.delete(id); else bookmarks.add(id);
|
|
725
|
-
try{ window.localStorage.setItem(BM_KEY, JSON.stringify(Array.from(bookmarks))); }catch(e){}
|
|
726
|
-
syncBmButtons();
|
|
727
|
-
bmChangeHandlers.forEach(function(fn){ try{ fn(id); }catch(e){} });
|
|
728
|
-
}
|
|
729
|
-
function bmBtnHtml(id, cls){
|
|
730
|
-
if(!id) return '';
|
|
731
|
-
var on=isBookmarked(id);
|
|
732
|
-
return '<button type="button" class="'+cls+(on?' is-on':'')+'" data-bm="'+attr(id)+'" aria-pressed="'+(on?'true':'false')+'" aria-label="'+(on?'Remove bookmark':'Bookmark')+'">'+BOOKMARK_ICON+'</button>';
|
|
733
|
-
}
|
|
734
|
-
document.addEventListener('click',function(e){
|
|
735
|
-
var b=(e.target&&e.target.closest)?e.target.closest('[data-bm]'):null;
|
|
736
|
-
if(!b) return;
|
|
737
|
-
e.preventDefault();
|
|
738
|
-
toggleBookmark(b.getAttribute('data-bm'));
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// ── shared: pause/resume a grid's muted autoplay (while the viewer is open) ──
|
|
742
|
-
function parkVideos(container){
|
|
743
|
-
if(!container) return;
|
|
744
|
-
container.querySelectorAll('video').forEach(function(o){ o.muted=true; o.pause(); });
|
|
745
|
-
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ p.classList.remove('is-sound'); });
|
|
746
|
-
}
|
|
747
|
-
function resumeVideos(container){
|
|
748
|
-
if(!container) return;
|
|
749
|
-
container.querySelectorAll('video').forEach(function(o){ o.muted=true; var pr=o.play(); if(pr&&pr.catch) pr.catch(function(){}); });
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function friendlyError(err){
|
|
753
|
-
var msg=(err&&err.message)?String(err.message):'';
|
|
754
|
-
if(!msg||/failed to fetch|networkerror|load failed/i.test(msg)) return "Couldn't reach the server — check your connection, or sign in to keep browsing.";
|
|
755
|
-
return msg;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function mediaHtml(t){
|
|
759
|
-
var url=t.previewUrl||t.preview_url||'';
|
|
760
|
-
if(!url) return '<div class="rk-discover-media-empty">'+(t.status==='processing'?'Downloading…':'No preview')+'</div>';
|
|
761
|
-
if(isImage(url)) return '<img src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'" loading="lazy" decoding="async">';
|
|
762
|
-
// #t=0.1 paints the first frame even where autoplay is blocked.
|
|
763
|
-
return '<video src="'+attr(url+'#t=0.1')+'" muted loop autoplay playsinline preload="metadata" tabindex="-1"></video>';
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Real feed/category poster card. The "Open Editor" CTA always deep-links
|
|
767
|
-
// into the real editor; the poster itself carries data-rk-view=<index> so a
|
|
768
|
-
// plain click opens the TikTok-style viewer instead (cmd/middle-click still
|
|
769
|
-
// opens the editor via the untouched href). No platform/source pill on the
|
|
770
|
-
// poster (user feedback) — source shows only in the viewer's meta line.
|
|
771
|
-
function posterCardHtml(t, viewIdx){
|
|
772
|
-
var id=domId(t);
|
|
773
|
-
var tid=realTemplateId(t);
|
|
774
|
-
var remix=needsDecompose(t);
|
|
775
|
-
var url=t.previewUrl||t.preview_url||'';
|
|
776
|
-
var hasVideo=Boolean(url)&&!isImage(url);
|
|
777
|
-
var pending=(t.status==='processing'||t.status==='failed');
|
|
778
|
-
var viewable=!pending&&Boolean(url)&&typeof viewIdx==='number'&&viewIdx>=0;
|
|
779
|
-
var dur=fmtDuration(t.durationSeconds);
|
|
780
|
-
var desc=String(t.viralDna||t.summary||'').trim();
|
|
781
|
-
// Real templates deep-link to the editor; inspiration-only cards open the
|
|
782
|
-
// preview viewer instead (a plain anchor href would 404 the editor), and the
|
|
783
|
-
// CTA decomposes on demand.
|
|
784
|
-
var posterHref=remix? '#' : editorUrl(tid);
|
|
785
|
-
var posterInner=mediaHtml(t)
|
|
786
|
-
+(dur?'<span class="rk-discover-dur">'+esc(dur)+'</span>':'')
|
|
787
|
-
+'<div class="rk-discover-scrim"></div>'
|
|
788
|
-
+(hasVideo&&!pending?'<span class="rk-discover-sound" aria-hidden="true"><span class="rk-ds-on">'+SOUND_ON_ICON+'</span><span class="rk-ds-off">'+SOUND_OFF_ICON+'</span></span>':'');
|
|
789
|
-
var poster=pending
|
|
790
|
-
? '<div class="rk-discover-poster">'+posterInner+'</div>'
|
|
791
|
-
: '<a class="rk-discover-poster" href="'+attr(posterHref)+'"'+(viewable?' data-rk-view="'+viewIdx+'"':'')+' aria-label="'+attr((viewable?'Preview ':'Open ')+(t.title||id)+(viewable?'':' in the editor'))+'">'+posterInner+'</a>';
|
|
792
|
-
// bookmark sits NEXT TO the anchor inside the wrap (button-in-anchor is
|
|
793
|
-
// invalid HTML); the wrap is the shared hover context for sound + bookmark
|
|
794
|
-
poster='<div class="rk-discover-posterwrap">'+poster+(pending?'':bmBtnHtml(id,'rk-discover-bm'))+'</div>';
|
|
795
|
-
var cta=pending
|
|
796
|
-
? '<span class="rk-btn rk-btn-ghost rk-btn-sm rk-btn-block rk-discover-fork rk-discover-fork-disabled" aria-disabled="true">'+(t.status==='failed'?'Download failed':'Preparing…')+'</span>'
|
|
797
|
-
: remix
|
|
798
|
-
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
799
|
-
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" href="'+attr(editorUrl(tid))+'" target="_blank" rel="noopener">'+EDIT_ICON+' Open Editor</a>';
|
|
800
|
-
return '<article class="rk-discover-card rk-card-hover">'
|
|
801
|
-
+ poster
|
|
802
|
-
+ '<div class="rk-discover-body">'
|
|
803
|
-
+ '<div class="rk-discover-titlewrap">'
|
|
804
|
-
+ '<h3 class="rk-discover-title">'+esc(t.title||id)+'</h3>'
|
|
805
|
-
+ (desc?'<p class="rk-discover-desc">'+esc(desc)+'</p>':'')
|
|
806
|
-
+ (tid?'<button type="button" class="rk-discover-idchip" data-copy-id="'+attr(tid)+'" title="Copy template ID">'+COPY_ICON+'<span class="rk-idchip-text">'+esc(tid)+'</span></button>':'')
|
|
807
|
-
+ '</div>'
|
|
808
|
-
+ cta
|
|
809
|
-
+ '</div>'
|
|
810
|
-
+ '</article>';
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ── hover audio: unmute a poster's (muted, autoplaying) video while the
|
|
814
|
-
// pointer is over it; only one card is ever audible at a time. Browsers may
|
|
815
|
-
// refuse unmuted playback before the user's first click (autoplay policy) —
|
|
816
|
-
// in that case we verify shortly after and fall back to muted playback
|
|
817
|
-
// instead of leaving a frozen poster. Delegated, so grid re-renders keep it.
|
|
818
|
-
function bindHoverAudio(container){
|
|
819
|
-
if(!container) return;
|
|
820
|
-
function posterOf(e){ var t=e.target; return (t&&t.closest)?t.closest('.rk-discover-posterwrap'):null; }
|
|
821
|
-
function setSound(poster,on){
|
|
822
|
-
var v=poster.querySelector('video'); if(!v) return;
|
|
823
|
-
if(on){
|
|
824
|
-
container.querySelectorAll('video').forEach(function(o){ if(o!==v) o.muted=true; });
|
|
825
|
-
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ if(p!==poster) p.classList.remove('is-sound'); });
|
|
826
|
-
v.muted=false;
|
|
827
|
-
if(v.paused){ var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){}); }
|
|
828
|
-
window.setTimeout(function(){
|
|
829
|
-
if(v.muted) return; // already reverted (pointer left)
|
|
830
|
-
if(v.paused){ v.muted=true; poster.classList.remove('is-sound'); var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){}); }
|
|
831
|
-
else poster.classList.add('is-sound');
|
|
832
|
-
},70);
|
|
833
|
-
} else {
|
|
834
|
-
v.muted=true; poster.classList.remove('is-sound');
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
container.addEventListener('mouseover',function(e){
|
|
838
|
-
var p=posterOf(e); if(!p) return;
|
|
839
|
-
if(e.relatedTarget&&p.contains(e.relatedTarget)) return; // still inside the same poster
|
|
840
|
-
setSound(p,true);
|
|
841
|
-
});
|
|
842
|
-
container.addEventListener('mouseout',function(e){
|
|
843
|
-
var p=posterOf(e); if(!p) return;
|
|
844
|
-
if(e.relatedTarget&&p.contains(e.relatedTarget)) return;
|
|
845
|
-
setSound(p,false);
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// ── TikTok-style viewer modal — SHARED by the feed grid and the categories
|
|
850
|
-
// reveal grid. open(list, clickedIdx, park): list is the template array
|
|
851
|
-
// behind the grid the user clicked, clickedIdx the clicked card index in
|
|
852
|
-
// that list, park the grid container whose muted autoplay is paused while
|
|
853
|
-
// the modal owns audio + CPU. Slides hydrate lazily (active +/-1); the click
|
|
854
|
-
// that opens the modal is a real user gesture, so the active video plays
|
|
855
|
-
// WITH sound (with a muted-playback fallback if the browser still refuses).
|
|
856
|
-
var viewerCtl=(function(){
|
|
857
|
-
var viewer=root.querySelector('[data-rk-viewer]');
|
|
858
|
-
if(!viewer) return null;
|
|
859
|
-
var vTrack=viewer.querySelector('[data-viewer-track]');
|
|
860
|
-
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null; var parkEl=null;
|
|
861
|
-
|
|
862
|
-
function viewerSlideHtml(t,i){
|
|
863
|
-
var id=domId(t);
|
|
864
|
-
var tid=realTemplateId(t);
|
|
865
|
-
var remix=needsDecompose(t);
|
|
866
|
-
var url=t.previewUrl||t.preview_url||'';
|
|
867
|
-
var media=isImage(url)
|
|
868
|
-
? '<img class="rk-discover-viewer-media" src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'">'
|
|
869
|
-
: '<video class="rk-discover-viewer-media" data-vsrc="'+attr(url)+'" loop playsinline preload="none"></video>';
|
|
870
|
-
var meta=[];
|
|
871
|
-
var srcL=sourceLabel(t.sourceType); if(srcL) meta.push(esc(srcL));
|
|
872
|
-
var dur=fmtDuration(t.durationSeconds); if(dur) meta.push(esc(dur));
|
|
873
|
-
var desc=String(t.viralDna||t.summary||'').trim();
|
|
874
|
-
return '<section class="rk-discover-viewer-slide" data-vslide="'+i+'">'
|
|
875
|
-
+'<figure class="rk-discover-viewer-stage">'
|
|
876
|
-
+ media
|
|
877
|
-
+'<span class="rk-discover-viewer-pause">'+PLAY_ICON+'</span>'
|
|
878
|
-
+'<div class="rk-discover-viewer-shade"></div>'
|
|
879
|
-
+ bmBtnHtml(id,'rk-discover-viewer-bm')
|
|
880
|
-
+'<div class="rk-discover-viewer-info">'
|
|
881
|
-
+(meta.length?'<div class="rk-discover-viewer-meta">'+meta.join(' · ')+'</div>':'')
|
|
882
|
-
+'<div class="rk-discover-viewer-title">'+esc(t.title||id)+'</div>'
|
|
883
|
-
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
884
|
-
+(remix
|
|
885
|
-
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
886
|
-
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" href="'+attr(editorUrl(tid))+'" target="_blank" rel="noopener">'+EDIT_ICON+' Open Editor</a>')
|
|
887
|
-
+'</div>'
|
|
888
|
-
+'</figure>'
|
|
889
|
-
+'</section>';
|
|
890
|
-
}
|
|
891
|
-
function hydrateSlide(slide){
|
|
892
|
-
var v=slide.querySelector('video[data-vsrc]');
|
|
893
|
-
if(!v||v.getAttribute('src')) return;
|
|
894
|
-
v.preload='auto';
|
|
895
|
-
v.setAttribute('src', v.getAttribute('data-vsrc'));
|
|
896
|
-
}
|
|
897
|
-
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
898
|
-
function setViewerActive(idx){
|
|
899
|
-
vActive=idx;
|
|
900
|
-
[].forEach.call(vTrack.children,function(s,i){
|
|
901
|
-
if(Math.abs(i-idx)<=1) hydrateSlide(s);
|
|
902
|
-
s.classList.remove('is-paused');
|
|
903
|
-
var v=s.querySelector('video');
|
|
904
|
-
if(!v) return;
|
|
905
|
-
if(i===idx){
|
|
906
|
-
v.muted=!vSound;
|
|
907
|
-
var pr=v.play();
|
|
908
|
-
if(pr&&pr.catch) pr.catch(function(){
|
|
909
|
-
// sound-on autoplay blocked → fall back to muted playback
|
|
910
|
-
v.muted=true; vSound=false; updateMuteUi();
|
|
911
|
-
var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){});
|
|
912
|
-
});
|
|
913
|
-
} else { v.pause(); }
|
|
914
|
-
});
|
|
915
|
-
updateMuteUi();
|
|
916
|
-
}
|
|
917
|
-
function stepViewer(d){
|
|
918
|
-
if(!vOpen) return;
|
|
919
|
-
var next=Math.max(0,Math.min(vItems.length-1,vActive+d));
|
|
920
|
-
if(next===vActive) return;
|
|
921
|
-
vTrack.scrollTo({top:next*vTrack.clientHeight,behavior:'smooth'});
|
|
922
|
-
}
|
|
923
|
-
function open(list, clickedIdx, park){
|
|
924
|
-
vItems=[]; var start=0;
|
|
925
|
-
(list||[]).forEach(function(t,i){
|
|
926
|
-
var url=t.previewUrl||t.preview_url||'';
|
|
927
|
-
if(!url||t.status==='processing'||t.status==='failed') return;
|
|
928
|
-
if(i===clickedIdx) start=vItems.length;
|
|
929
|
-
vItems.push(t);
|
|
930
|
-
});
|
|
931
|
-
if(!vItems.length) return;
|
|
932
|
-
parkEl=park||null;
|
|
933
|
-
vTrack.innerHTML=vItems.map(viewerSlideHtml).join('');
|
|
934
|
-
vOpen=true; vSound=true;
|
|
935
|
-
viewer.hidden=false;
|
|
936
|
-
document.documentElement.classList.add('rk-noscroll');
|
|
937
|
-
parkVideos(parkEl);
|
|
938
|
-
if(vIO) vIO.disconnect();
|
|
939
|
-
vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){
|
|
940
|
-
entries.forEach(function(en){
|
|
941
|
-
if(!en.isIntersecting) return;
|
|
942
|
-
var i=Number(en.target.getAttribute('data-vslide'));
|
|
943
|
-
if(i!==vActive) setViewerActive(i);
|
|
944
|
-
});
|
|
945
|
-
},{root:vTrack,threshold:0.6}):null;
|
|
946
|
-
if(vIO) [].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
947
|
-
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
948
|
-
setViewerActive(start);
|
|
949
|
-
}
|
|
950
|
-
function close(){
|
|
951
|
-
if(!vOpen) return;
|
|
952
|
-
vOpen=false; vActive=-1;
|
|
953
|
-
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
954
|
-
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
955
|
-
vTrack.innerHTML='';
|
|
956
|
-
viewer.hidden=true;
|
|
957
|
-
document.documentElement.classList.remove('rk-noscroll');
|
|
958
|
-
resumeVideos(parkEl);
|
|
959
|
-
parkEl=null;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
963
|
-
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
964
|
-
if(muteBtn) muteBtn.addEventListener('click',function(){
|
|
965
|
-
vSound=!vSound; updateMuteUi();
|
|
966
|
-
var s=vTrack.children[vActive]; var v=s&&s.querySelector('video');
|
|
967
|
-
if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){}); s.classList.remove('is-paused'); } }
|
|
968
|
-
});
|
|
969
|
-
var prevBtn=viewer.querySelector('[data-viewer-prev]');
|
|
970
|
-
var nextBtn=viewer.querySelector('[data-viewer-next]');
|
|
971
|
-
if(prevBtn) prevBtn.addEventListener('click',function(){ stepViewer(-1); });
|
|
972
|
-
if(nextBtn) nextBtn.addEventListener('click',function(){ stepViewer(1); });
|
|
973
|
-
// tap the video → pause/resume (TikTok); tap the empty gutter → close;
|
|
974
|
-
// bookmarks are handled by the document-level [data-bm] listener
|
|
975
|
-
vTrack.addEventListener('click',function(e){
|
|
976
|
-
var t=e.target;
|
|
977
|
-
if(t&&t.closest&&(t.closest('a')||t.closest('[data-bm]'))) return;
|
|
978
|
-
var media=(t&&t.closest)?t.closest('.rk-discover-viewer-media'):null;
|
|
979
|
-
if(media&&media.tagName==='VIDEO'){
|
|
980
|
-
var slide=media.closest('.rk-discover-viewer-slide');
|
|
981
|
-
if(media.paused){ var pr=media.play(); if(pr&&pr.catch) pr.catch(function(){}); if(slide) slide.classList.remove('is-paused'); }
|
|
982
|
-
else { media.pause(); if(slide) slide.classList.add('is-paused'); }
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if(t&&t.classList&&(t.classList.contains('rk-discover-viewer-slide')||t===vTrack)) close();
|
|
986
|
-
});
|
|
987
|
-
document.addEventListener('keydown',function(e){
|
|
988
|
-
if(!vOpen) return;
|
|
989
|
-
if(e.key==='Escape'){ e.preventDefault(); close(); }
|
|
990
|
-
else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); stepViewer(1); }
|
|
991
|
-
else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); stepViewer(-1); }
|
|
992
|
-
});
|
|
993
|
-
window.addEventListener('resize',function(){
|
|
994
|
-
if(vOpen&&vActive>=0) vTrack.scrollTop=vActive*vTrack.clientHeight;
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
return { open: open, close: close, isOpen: function(){ return vOpen; } };
|
|
998
|
-
})();
|
|
999
|
-
|
|
1000
|
-
// Wire a poster grid's plain-clicks into the shared viewer. getList is
|
|
1001
|
-
// called at click time so it always reflects the grid's CURRENT contents
|
|
1002
|
-
// (search filter, bookmark filter, revealed group).
|
|
1003
|
-
function bindViewerOpens(container, getList){
|
|
1004
|
-
if(!container||!viewerCtl) return;
|
|
1005
|
-
container.addEventListener('click',function(e){
|
|
1006
|
-
if(e.defaultPrevented||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0) return;
|
|
1007
|
-
var a=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
|
|
1008
|
-
if(!a||!container.contains(a)) return;
|
|
1009
|
-
e.preventDefault();
|
|
1010
|
-
viewerCtl.open(getList()||[], Number(a.getAttribute('data-rk-view')), container);
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1015
|
-
// FEED
|
|
1016
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1017
|
-
var feedRoot=root.querySelector('[data-rk-feed]');
|
|
1018
|
-
if(feedRoot){
|
|
1019
|
-
var gridEl=feedRoot.querySelector('[data-rk-grid]');
|
|
1020
|
-
var stateEl=feedRoot.querySelector('[data-rk-state]');
|
|
1021
|
-
// search lives in the shared toolbar row (outside [data-rk-feed]); the
|
|
1022
|
-
// visible-count is folded into the search placeholder rather than shown as
|
|
1023
|
-
// a separate label.
|
|
1024
|
-
var searchEl=root.querySelector('[data-rk-search]');
|
|
1025
|
-
// caption the placeholder with the count, but only while the box is empty —
|
|
1026
|
-
// a typed query keeps its own placeholder-less state.
|
|
1027
|
-
function setCountHint(n){
|
|
1028
|
-
if(!searchEl) return;
|
|
1029
|
-
searchEl.placeholder = (n==null)
|
|
1030
|
-
? 'Search templates'
|
|
1031
|
-
: 'Search '+n+' template'+(n===1?'':'s');
|
|
1032
|
-
}
|
|
1033
|
-
var bmChip=root.querySelector('[data-rk-bmfilter]');
|
|
1034
|
-
var bmCountEl=root.querySelector('[data-rk-bmcount]');
|
|
1035
|
-
var bmFilter=false;
|
|
1036
|
-
var feedState={items:[],visible:[],loading:false};
|
|
1037
|
-
var feedReq=0;
|
|
1038
|
-
|
|
1039
|
-
function feedSetState(msg,isError){
|
|
1040
|
-
if(gridEl) gridEl.innerHTML='';
|
|
1041
|
-
if(stateEl){ stateEl.hidden=false; stateEl.innerHTML=msg; stateEl.classList.toggle('is-error',!!isError); }
|
|
1042
|
-
setCountHint(null);
|
|
1043
|
-
}
|
|
1044
|
-
function updateBmChip(){
|
|
1045
|
-
if(!bmChip) return;
|
|
1046
|
-
var n=0;
|
|
1047
|
-
feedState.items.forEach(function(t){ if(isBookmarked(t.templateId||t.template_id)) n++; });
|
|
1048
|
-
if(bmCountEl) bmCountEl.textContent=n?String(n):'';
|
|
1049
|
-
bmChip.classList.toggle('is-active',bmFilter);
|
|
1050
|
-
bmChip.setAttribute('aria-pressed',bmFilter?'true':'false');
|
|
1051
|
-
}
|
|
1052
|
-
function feedRender(){
|
|
1053
|
-
var vis=bmFilter
|
|
1054
|
-
? feedState.items.filter(function(t){ return isBookmarked(t.templateId||t.template_id); })
|
|
1055
|
-
: feedState.items;
|
|
1056
|
-
feedState.visible=vis;
|
|
1057
|
-
updateBmChip();
|
|
1058
|
-
if(!vis.length){
|
|
1059
|
-
feedSetState(bmFilter?'No bookmarks yet — tap the bookmark on any template to save it for later.':'No templates match — try a different search.',false);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
if(stateEl) stateEl.hidden=true;
|
|
1063
|
-
if(gridEl) gridEl.innerHTML=vis.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
1064
|
-
setCountHint(vis.length);
|
|
1065
|
-
}
|
|
1066
|
-
if(bmChip) bmChip.addEventListener('click',function(){ bmFilter=!bmFilter; feedRender(); });
|
|
1067
|
-
bmChangeHandlers.push(function(){
|
|
1068
|
-
updateBmChip();
|
|
1069
|
-
// with the filter on, un-bookmarking drops the card from the grid; the
|
|
1070
|
-
// re-render restarts muted autoplay, so re-park it if the viewer is open
|
|
1071
|
-
if(bmFilter){ feedRender(); if(viewerCtl&&viewerCtl.isOpen()) parkVideos(gridEl); }
|
|
1072
|
-
});
|
|
1073
|
-
function applyFeedItems(items){
|
|
1074
|
-
feedState.items=Array.isArray(items)?items:[];
|
|
1075
|
-
feedState.loading=false;
|
|
1076
|
-
feedRender();
|
|
1077
|
-
}
|
|
1078
|
-
// Direct same-origin fetch — the fallback for when the Zustand bridge isn't
|
|
1079
|
-
// on the page yet (its deferred module is still loading) or is disabled.
|
|
1080
|
-
function feedFetchDirect(query,my){
|
|
1081
|
-
var url=feedEndpoint;
|
|
1082
|
-
try{ var u=new URL(feedEndpoint, window.location.href); if(query) u.searchParams.set('q', query); if(accountId) u.searchParams.set('account', accountId); url=u.toString(); }catch(e){ url=feedEndpoint; }
|
|
1083
|
-
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1084
|
-
.then(function(r){ if(!r.ok){ throw new Error(r.status===401?'Sign in to browse the template feed.':("Couldn't load templates ("+r.status+").")); } return r.json(); })
|
|
1085
|
-
.then(function(j){ if(my!==feedReq) return; applyFeedItems((j&&Array.isArray(j.templates))?j.templates:[]); })
|
|
1086
|
-
.catch(function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); });
|
|
1087
|
-
}
|
|
1088
|
-
// Feed loads flow through the Zustand store bridge (SSR-seed + SWR cache);
|
|
1089
|
-
// it may call onResult twice for the default feed (cached snapshot first,
|
|
1090
|
-
// then a background-revalidated fresh copy) — both are honored under the
|
|
1091
|
-
// same request token so a newer search still wins.
|
|
1092
|
-
function feedLoad(query){
|
|
1093
|
-
var my=++feedReq;
|
|
1094
|
-
feedState.loading=true;
|
|
1095
|
-
// Only flash the loading state when nothing's painted yet — a
|
|
1096
|
-
// revalidate must never blank an already-good grid.
|
|
1097
|
-
if(!feedState.items.length) feedSetState('Loading templates…',false);
|
|
1098
|
-
var bridge=window.__vidfarmDiscover;
|
|
1099
|
-
if(bridge&&bridge.loadFeed){
|
|
1100
|
-
bridge.loadFeed(query,{
|
|
1101
|
-
onLoading:function(){ if(my===feedReq && !feedState.items.length) feedSetState('Loading templates…',false); },
|
|
1102
|
-
onResult:function(items){ if(my!==feedReq) return; applyFeedItems(items); },
|
|
1103
|
-
onError:function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); }
|
|
1104
|
-
});
|
|
1105
|
-
} else {
|
|
1106
|
-
feedFetchDirect(query,my);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
// Honor a ?q= (e.g. from a category link) in the page URL.
|
|
1110
|
-
var initialQuery='';
|
|
1111
|
-
try{ initialQuery=(new URL(window.location.href)).searchParams.get('q')||''; }catch(e){ initialQuery=''; }
|
|
1112
|
-
if(searchEl){
|
|
1113
|
-
searchEl.value=initialQuery;
|
|
1114
|
-
var debounce=null;
|
|
1115
|
-
searchEl.addEventListener('input',function(){
|
|
1116
|
-
if(debounce) window.clearTimeout(debounce);
|
|
1117
|
-
var v=searchEl.value.trim();
|
|
1118
|
-
debounce=window.setTimeout(function(){ feedLoad(v); },260);
|
|
1119
|
-
});
|
|
1120
|
-
searchEl.addEventListener('keydown',function(e){ if(e.key==='Enter'){ if(debounce) window.clearTimeout(debounce); feedLoad(searchEl.value.trim()); } });
|
|
1121
|
-
}
|
|
1122
|
-
bindHoverAudio(gridEl);
|
|
1123
|
-
// plain click on a card → shared viewer over the CURRENT visible feed
|
|
1124
|
-
// (search + bookmark filter applied)
|
|
1125
|
-
bindViewerOpens(gridEl, function(){ return feedState.visible; });
|
|
1126
|
-
// SSR-seed: if the server inlined the first page of the default feed AND
|
|
1127
|
-
// there's no ?q= search, paint it immediately with NO initial fetch — this
|
|
1128
|
-
// is what kills the redundant SSR-render → client-fetch round trip.
|
|
1129
|
-
var seeded=false;
|
|
1130
|
-
if(!initialQuery && boot.feedInitial && Array.isArray(boot.feedInitial) && boot.feedInitial.length){
|
|
1131
|
-
applyFeedItems(boot.feedInitial);
|
|
1132
|
-
seeded=true;
|
|
1133
|
-
}
|
|
1134
|
-
if(!seeded){
|
|
1135
|
-
feedLoad(initialQuery.trim());
|
|
1136
|
-
}
|
|
1137
|
-
// A background revalidation (stale snapshot / BFCache restore) from the
|
|
1138
|
-
// store bridge re-paints the grid — but only while not mid-search.
|
|
1139
|
-
window.addEventListener('vidfarm:discover-feed-updated',function(ev){
|
|
1140
|
-
var q=searchEl?searchEl.value.trim():'';
|
|
1141
|
-
if(q) return;
|
|
1142
|
-
var items=ev&&ev.detail&&ev.detail.items;
|
|
1143
|
-
if(Array.isArray(items)) applyFeedItems(items);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
// ── "Add New" modal — paste a social URL or upload a video, add it to the
|
|
1147
|
-
// caller's private feed (real POST /discover/templates + upload presign
|
|
1148
|
-
// pair), then reload the feed so the new pending card appears. In feed
|
|
1149
|
-
// scope so it can reach feedLoad + the current search value.
|
|
1150
|
-
var addModal=root.querySelector('[data-rk-add-modal]');
|
|
1151
|
-
var addBtn=root.querySelector('[data-rk-add]');
|
|
1152
|
-
if(addModal&&addBtn){
|
|
1153
|
-
var addForm=addModal.querySelector('[data-rk-add-form]');
|
|
1154
|
-
var addUrl=addModal.querySelector('[data-rk-add-url]');
|
|
1155
|
-
var addFile=addModal.querySelector('[data-rk-add-file]');
|
|
1156
|
-
var addFileLabel=addModal.querySelector('[data-rk-add-filelabel]');
|
|
1157
|
-
var addFileText=addModal.querySelector('[data-rk-add-filetext]');
|
|
1158
|
-
var addTagline=addModal.querySelector('[data-rk-add-tagline]');
|
|
1159
|
-
var addNotes=addModal.querySelector('[data-rk-add-notes]');
|
|
1160
|
-
var addErr=addModal.querySelector('[data-rk-add-error]');
|
|
1161
|
-
var addSubmit=addModal.querySelector('[data-rk-add-submit]');
|
|
1162
|
-
var MAX_UPLOAD=200*1024*1024;
|
|
1163
|
-
var defaultFileText=addFileText?addFileText.innerHTML:'';
|
|
1164
|
-
var addBusy=false;
|
|
1165
|
-
|
|
1166
|
-
function showAddErr(msg){ if(addErr){ addErr.textContent=msg||''; addErr.hidden=!msg; } }
|
|
1167
|
-
function openAdd(){
|
|
1168
|
-
addModal.hidden=false;
|
|
1169
|
-
document.documentElement.classList.add('rk-noscroll');
|
|
1170
|
-
if(addUrl){ try{ addUrl.focus(); }catch(e){} }
|
|
1171
|
-
}
|
|
1172
|
-
function closeAdd(){
|
|
1173
|
-
if(addBusy) return;
|
|
1174
|
-
addModal.hidden=true;
|
|
1175
|
-
document.documentElement.classList.remove('rk-noscroll');
|
|
1176
|
-
}
|
|
1177
|
-
function resetAdd(){
|
|
1178
|
-
if(addForm) addForm.reset();
|
|
1179
|
-
if(addFileLabel) addFileLabel.classList.remove('is-picked');
|
|
1180
|
-
if(addFileText) addFileText.innerHTML=defaultFileText;
|
|
1181
|
-
showAddErr('');
|
|
1182
|
-
}
|
|
1183
|
-
function setAddBusy(on,label){
|
|
1184
|
-
addBusy=on;
|
|
1185
|
-
if(addSubmit){ addSubmit.disabled=on; addSubmit.textContent=on?(label||'Adding\\u2026'):'Add to feed'; }
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
addBtn.addEventListener('click',openAdd);
|
|
1189
|
-
addModal.querySelectorAll('[data-rk-add-close]').forEach(function(b){ b.addEventListener('click',closeAdd); });
|
|
1190
|
-
document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&!addModal.hidden) closeAdd(); });
|
|
1191
|
-
|
|
1192
|
-
if(addFile){
|
|
1193
|
-
addFile.addEventListener('change',function(){
|
|
1194
|
-
var f=addFile.files&&addFile.files[0];
|
|
1195
|
-
if(addFileLabel) addFileLabel.classList.toggle('is-picked',!!f);
|
|
1196
|
-
if(addFileText) addFileText.innerHTML=f?esc(f.name):defaultFileText;
|
|
1197
|
-
showAddErr('');
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
function ingest(uploadRef){
|
|
1202
|
-
var tagline=addTagline?addTagline.value.trim():'';
|
|
1203
|
-
var notes=addNotes?addNotes.value.trim():'';
|
|
1204
|
-
var payload=uploadRef
|
|
1205
|
-
? { upload:uploadRef, tagline:tagline||null, notes:notes||null }
|
|
1206
|
-
: { source_url:(addUrl?addUrl.value.trim():''), tagline:tagline||null, notes:notes||null };
|
|
1207
|
-
return fetch(withAccount('/discover/templates'),{
|
|
1208
|
-
method:'POST',credentials:'same-origin',
|
|
1209
|
-
headers:{'content-type':'application/json',Accept:'application/json'},
|
|
1210
|
-
body:JSON.stringify(payload)
|
|
1211
|
-
}).then(function(r){ return r.json().catch(function(){ return {}; }).then(function(d){ return {ok:r.ok,status:r.status,data:d}; }); });
|
|
1212
|
-
}
|
|
1213
|
-
function uploadFile(file){
|
|
1214
|
-
return fetch(withAccount('/discover/templates/upload/presign'),{
|
|
1215
|
-
method:'POST',credentials:'same-origin',
|
|
1216
|
-
headers:{'content-type':'application/json',Accept:'application/json'},
|
|
1217
|
-
body:JSON.stringify({ file_name:file.name, content_type:file.type||null, size_bytes:file.size })
|
|
1218
|
-
}).then(function(r){ return r.json().catch(function(){ return {}; }).then(function(d){ return {ok:r.ok,data:d}; }); })
|
|
1219
|
-
.then(function(res){
|
|
1220
|
-
if(!res.ok) throw new Error((res.data&&res.data.error)||'Unable to start the video upload.');
|
|
1221
|
-
var p=res.data||{};
|
|
1222
|
-
if(p.transport==='presigned'&&p.upload&&p.upload.url&&p.storage_key){
|
|
1223
|
-
return fetch(p.upload.url,{ method:(p.upload.method||'PUT'), headers:(p.upload.headers||{}), body:file })
|
|
1224
|
-
.then(function(put){ if(!put.ok) throw new Error('Video upload failed ('+put.status+'). Try again.');
|
|
1225
|
-
return { storage_key:p.storage_key, file_name:p.file_name||file.name }; });
|
|
1226
|
-
}
|
|
1227
|
-
var form=new FormData(); form.append('file',file,p.file_name||file.name);
|
|
1228
|
-
return fetch((p.upload&&p.upload.url)||'/discover/templates/upload',{ method:'POST',credentials:'same-origin',body:form })
|
|
1229
|
-
.then(function(direct){ return direct.json().catch(function(){ return {}; }).then(function(dd){
|
|
1230
|
-
if(!direct.ok||!dd.storage_key) throw new Error(dd.error||'Video upload failed. Try again.');
|
|
1231
|
-
return { storage_key:dd.storage_key, file_name:dd.file_name||file.name }; }); });
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
if(addForm){
|
|
1236
|
-
addForm.addEventListener('submit',function(e){
|
|
1237
|
-
e.preventDefault();
|
|
1238
|
-
if(addBusy) return;
|
|
1239
|
-
showAddErr('');
|
|
1240
|
-
var file=addFile&&addFile.files?addFile.files[0]:null;
|
|
1241
|
-
var url=addUrl?addUrl.value.trim():'';
|
|
1242
|
-
if(!file&&!url){ showAddErr('Paste a TikTok, YouTube, Twitter/X, or Instagram video URL \\u2014 or upload a video file.'); return; }
|
|
1243
|
-
if(file&&file.size>MAX_UPLOAD){ showAddErr('Video uploads can be at most 200 MB.'); return; }
|
|
1244
|
-
setAddBusy(true,file?'Uploading\\u2026':'Adding\\u2026');
|
|
1245
|
-
var chain=file?uploadFile(file):Promise.resolve(null);
|
|
1246
|
-
chain.then(function(uploadRef){ return ingest(uploadRef); })
|
|
1247
|
-
.then(function(res){
|
|
1248
|
-
setAddBusy(false);
|
|
1249
|
-
if(!res.ok){ showAddErr((res.data&&res.data.error)||'Unable to add that video.'); return; }
|
|
1250
|
-
resetAdd(); closeAdd();
|
|
1251
|
-
// A new inspiration just landed — drop the cached snapshot so the
|
|
1252
|
-
// reload fetches fresh (no stale-cache flash of the old feed).
|
|
1253
|
-
if(window.__vidfarmDiscover&&window.__vidfarmDiscover.invalidate) window.__vidfarmDiscover.invalidate();
|
|
1254
|
-
feedLoad(searchEl?searchEl.value.trim():'');
|
|
1255
|
-
})
|
|
1256
|
-
.catch(function(err){ setAddBusy(false); showAddErr((err&&err.message)?err.message:'Unable to add that video.'); });
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1263
|
-
// SWIPE
|
|
1264
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1265
|
-
var swipeRoot=root.querySelector('[data-rk-swipe]');
|
|
1266
|
-
if(swipeRoot){
|
|
1267
|
-
var deck=swipeRoot.querySelector('[data-deck]');
|
|
1268
|
-
var swStateEl=swipeRoot.querySelector('[data-rk-swipe-state]');
|
|
1269
|
-
var actionBtns=[].slice.call(swipeRoot.querySelectorAll('[data-swipe-act]'));
|
|
1270
|
-
var hint=swipeRoot.querySelector('[data-actions-hint]');
|
|
1271
|
-
var skipBtn=swipeRoot.querySelector('[data-swipe-skip]');
|
|
1272
|
-
var forkBtn=swipeRoot.querySelector('[data-swipe-fork]');
|
|
1273
|
-
var soundBtn=swipeRoot.querySelector('[data-swipe-sound]');
|
|
1274
|
-
var sw={target:5,customizing:0,productSet:false,busy:false,refilling:false,loaded:false,soundOn:false,known:{}};
|
|
1275
|
-
var nodes=[]; // live card DOM nodes, top-of-deck first
|
|
1276
|
-
var deckExpanded=false; // true while the fullscreen swipe viewer owns the previews
|
|
1277
|
-
|
|
1278
|
-
// ── audio: the same-origin preview iframes poll these window flags (see
|
|
1279
|
-
// injectSwipePreviewFit). Only the TOP card's id is named audible so the
|
|
1280
|
-
// preloaded 2nd iframe stays silent (audio never doubles). Deck previews
|
|
1281
|
-
// always start muted (the only autoplay browsers permit); the toggle is the
|
|
1282
|
-
// user gesture that unlocks sound.
|
|
1283
|
-
function syncSound(){
|
|
1284
|
-
// While the fullscreen viewer is open it owns the audio flags (it names the
|
|
1285
|
-
// active slide's card audible); the deck must not clobber them.
|
|
1286
|
-
if(deckExpanded) return;
|
|
1287
|
-
try{
|
|
1288
|
-
window.__VF_SWIPE_SOUND__ = sw.soundOn===true;
|
|
1289
|
-
window.__VF_SWIPE_SOUND_TOP__ = nodes.length? String(nodes[0]._card.id): null;
|
|
1290
|
-
}catch(e){}
|
|
1291
|
-
}
|
|
1292
|
-
function updateSoundBtn(){
|
|
1293
|
-
if(!soundBtn) return;
|
|
1294
|
-
var show=nodes.length>0;
|
|
1295
|
-
soundBtn.hidden=!show;
|
|
1296
|
-
soundBtn.classList.toggle('is-on', sw.soundOn);
|
|
1297
|
-
soundBtn.setAttribute('aria-pressed', sw.soundOn?'true':'false');
|
|
1298
|
-
soundBtn.setAttribute('aria-label', sw.soundOn?'Mute':'Unmute');
|
|
1299
|
-
soundBtn.innerHTML = sw.soundOn? SOUND_ON_ICON : SOUND_OFF_ICON;
|
|
1300
|
-
}
|
|
1301
|
-
if(soundBtn) soundBtn.addEventListener('click',function(){ sw.soundOn=!sw.soundOn; syncSound(); updateSoundBtn(); });
|
|
1302
|
-
|
|
1303
|
-
function swShowState(html,kind){
|
|
1304
|
-
if(swStateEl){ swStateEl.hidden=false; swStateEl.innerHTML=html; swStateEl.classList.toggle('is-error',kind==='error'); }
|
|
1305
|
-
actionBtns.forEach(function(b){ b.hidden=true; });
|
|
1306
|
-
if(hint) hint.hidden=true;
|
|
1307
|
-
}
|
|
1308
|
-
function swHideState(){ if(swStateEl) swStateEl.hidden=true; }
|
|
1309
|
-
|
|
1310
|
-
function makeCard(card){
|
|
1311
|
-
if(card&&card.id) sw.known[String(card.id)]=true;
|
|
1312
|
-
var el=document.createElement('article');
|
|
1313
|
-
el.className='rk-discover-swipe';
|
|
1314
|
-
el._card=card; el._iframed=false;
|
|
1315
|
-
var title=esc(card.title||card.source_title||card.template_id||'Template');
|
|
1316
|
-
var caption=card.caption?('<div class="rk-discover-swipe-caption">'+esc(card.caption)+'</div>'):'';
|
|
1317
|
-
el.innerHTML='<div class="rk-discover-swipe-fallback"><span>'+title+'</span></div>'
|
|
1318
|
-
+'<div class="rk-discover-swipe-scrim"></div>'
|
|
1319
|
-
+'<div class="rk-discover-swipe-info"><div class="rk-discover-swipe-title">'+title+'</div>'+caption+'</div>';
|
|
1320
|
-
deck.appendChild(el);
|
|
1321
|
-
return el;
|
|
1322
|
-
}
|
|
1323
|
-
function upgradeToIframe(el){
|
|
1324
|
-
if(el._iframed) return;
|
|
1325
|
-
var card=el._card; var url=card&&(card.preview_url||card.previewUrl);
|
|
1326
|
-
if(!url) return;
|
|
1327
|
-
var fb=el.querySelector('.rk-discover-swipe-fallback');
|
|
1328
|
-
var frame=document.createElement('iframe');
|
|
1329
|
-
frame.className='rk-discover-swipe-frame';
|
|
1330
|
-
frame.setAttribute('src', url);
|
|
1331
|
-
frame.setAttribute('title', String(card.title||'Preview'));
|
|
1332
|
-
frame.setAttribute('loading','lazy');
|
|
1333
|
-
frame.setAttribute('scrolling','no');
|
|
1334
|
-
frame.setAttribute('allow','autoplay');
|
|
1335
|
-
// The fallback is an OPAQUE dark placeholder that sits later in the DOM,
|
|
1336
|
-
// so it paints over the iframe. Reveal the live preview once it loads by
|
|
1337
|
-
// fading the placeholder out (kept as a load state until then). A timed
|
|
1338
|
-
// fallback covers the case where 'load' never fires.
|
|
1339
|
-
function revealFrame(){ if(fb){ fb.style.opacity='0'; fb.style.pointerEvents='none'; } }
|
|
1340
|
-
frame.addEventListener('load', function(){ window.setTimeout(revealFrame, 120); });
|
|
1341
|
-
window.setTimeout(revealFrame, 2600);
|
|
1342
|
-
if(fb&&fb.parentNode) fb.parentNode.insertBefore(frame, fb); else el.insertBefore(frame, el.firstChild);
|
|
1343
|
-
el._iframed=true;
|
|
1344
|
-
}
|
|
1345
|
-
function restack(){
|
|
1346
|
-
nodes.forEach(function(el,i){
|
|
1347
|
-
if(i>2){ el.style.display='none'; return; }
|
|
1348
|
-
el.style.display='';
|
|
1349
|
-
el.style.zIndex=String(20-i);
|
|
1350
|
-
el.style.transform='translateY('+(i*12)+'px) scale('+(1-i*0.045).toFixed(3)+')';
|
|
1351
|
-
el.style.opacity=i===0?'1':(i===1?'0.92':'0.82');
|
|
1352
|
-
// The viewer owns the previews while expanded — don't build deck iframes
|
|
1353
|
-
// (that would double the audio + CPU); resume rebuilds them on close.
|
|
1354
|
-
if(i<=1 && !deckExpanded) upgradeToIframe(el);
|
|
1355
|
-
});
|
|
1356
|
-
var has=nodes.length>0;
|
|
1357
|
-
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
1358
|
-
if(hint) hint.hidden=!has;
|
|
1359
|
-
syncSound(); updateSoundBtn();
|
|
1360
|
-
// Keep the open viewer's slide list in step with refills/removals.
|
|
1361
|
-
if(deckExpanded && swViewer) swViewer.sync();
|
|
1362
|
-
if(!has) swCaughtUp();
|
|
1363
|
-
}
|
|
1364
|
-
function swCaughtUp(){
|
|
1365
|
-
if(sw.customizing>0||sw.refilling){
|
|
1366
|
-
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div class="rk-discover-swipe-state-title">Customizing fresh clips…</div><div>Recutting viral templates into ads for your product.</div>');
|
|
1367
|
-
} else {
|
|
1368
|
-
swShowState('<div class="rk-discover-swipe-state-title">You\\u2019re all caught up</div><div>No more templates to customize right now. Check back as new ones are added.</div>');
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
// POST approve/reject for a card (shared by the deck buttons + the viewer).
|
|
1372
|
-
function postDecision(card,dir){
|
|
1373
|
-
var path=swipeBase+'/'+encodeURIComponent(String(card.id))+'/'+(dir==='fork'?'approve':'reject');
|
|
1374
|
-
return fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1375
|
-
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1376
|
-
.catch(function(){ return {}; });
|
|
1377
|
-
}
|
|
1378
|
-
// Drop a card from the live deck (array + its DOM node) by id. The viewer can
|
|
1379
|
-
// consume a card that isn't the top one, so removal is id-based, not shift().
|
|
1380
|
-
function removeNodeByCard(card){
|
|
1381
|
-
var id=card&&card.id!=null?String(card.id):null; if(id==null) return;
|
|
1382
|
-
for(var i=0;i<nodes.length;i++){
|
|
1383
|
-
if(String(nodes[i]._card.id)===id){
|
|
1384
|
-
var el=nodes[i]; nodes.splice(i,1);
|
|
1385
|
-
if(el&&el.parentNode) el.parentNode.removeChild(el);
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
function decide(dir){
|
|
1391
|
-
if(sw.busy||!nodes.length) return;
|
|
1392
|
-
sw.busy=true;
|
|
1393
|
-
var el=nodes.shift();
|
|
1394
|
-
var card=el._card;
|
|
1395
|
-
el.classList.remove('is-front');
|
|
1396
|
-
el.style.zIndex='30';
|
|
1397
|
-
el.style.transform='translateX('+(dir==='fork'?460:-460)+'px) rotate('+(dir==='fork'?15:-15)+'deg)';
|
|
1398
|
-
el.style.opacity='0';
|
|
1399
|
-
window.setTimeout(function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); },380);
|
|
1400
|
-
postDecision(card,dir).then(function(){ sw.busy=false; restack(); refill(); });
|
|
1401
|
-
}
|
|
1402
|
-
function refill(){
|
|
1403
|
-
if(sw.refilling||!sw.productSet||!sw.loaded) return;
|
|
1404
|
-
if(nodes.length>=sw.target) return;
|
|
1405
|
-
sw.refilling=true;
|
|
1406
|
-
fetch(withAccount(swipeRefillEndpoint),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1407
|
-
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1408
|
-
.then(function(j){
|
|
1409
|
-
sw.refilling=false;
|
|
1410
|
-
if(!j||j.ok===false) return; // e.g. need_provider_key — stop quietly
|
|
1411
|
-
if(j.filled&&j.record){ makeCard(j.record); restack(); }
|
|
1412
|
-
var more=j.filled&&nodes.length<sw.target&&j.reason!=='no_eligible'&&j.reason!=='at_target';
|
|
1413
|
-
if(more) refill();
|
|
1414
|
-
// Server said it's still working (no card yet, not "done") — the
|
|
1415
|
-
// customization keeps running Lambda-side even if THIS request replies
|
|
1416
|
-
// late; poll the stack so a finished card appears without a refresh.
|
|
1417
|
-
else if(!j.filled&&j.reason!=='no_eligible'&&j.reason!=='at_target') schedulePoll();
|
|
1418
|
-
})
|
|
1419
|
-
.catch(function(){
|
|
1420
|
-
// Timeout / 504 / network drop: customizeOneMore often runs past the
|
|
1421
|
-
// gateway's reply window, so the card lands server-side after we gave
|
|
1422
|
-
// up here. Poll the stack to pick it up (this is the "504 on first
|
|
1423
|
-
// load, works after a manual refresh" case — now it self-heals).
|
|
1424
|
-
sw.refilling=false; schedulePoll();
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
// ── stack re-poll: recover cards the refill Lambda produced after its HTTP
|
|
1428
|
-
// reply timed out. Backs off after a fixed number of empty tries so we never
|
|
1429
|
-
// poll forever.
|
|
1430
|
-
var pollTimer=null, pollTries=0;
|
|
1431
|
-
function schedulePoll(){
|
|
1432
|
-
if(pollTimer||pollTries>=12) return;
|
|
1433
|
-
if(nodes.length>=sw.target) return;
|
|
1434
|
-
pollTimer=window.setTimeout(function(){ pollTimer=null; pollStack(); }, 5000);
|
|
1435
|
-
}
|
|
1436
|
-
function pollStack(){
|
|
1437
|
-
if(nodes.length>=sw.target) return;
|
|
1438
|
-
fetch(withAccount(swipeStackEndpoint),{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1439
|
-
.then(function(r){ return r.ok? r.json(): null; })
|
|
1440
|
-
.then(function(j){
|
|
1441
|
-
if(!j){ pollTries++; schedulePoll(); return; }
|
|
1442
|
-
sw.customizing=Number(j&&j.customizing)||0;
|
|
1443
|
-
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
1444
|
-
var added=0;
|
|
1445
|
-
stack.forEach(function(card){ if(card&&card.id&&!sw.known[String(card.id)]){ nodes.push(makeCard(card)); added++; } });
|
|
1446
|
-
if(added){ pollTries=0; swHideState(); restack(); }
|
|
1447
|
-
else { pollTries++; if(!nodes.length) swCaughtUp(); }
|
|
1448
|
-
if((sw.customizing>0||nodes.length<sw.target)&&nodes.length<sw.target) schedulePoll();
|
|
1449
|
-
})
|
|
1450
|
-
.catch(function(){ pollTries++; schedulePoll(); });
|
|
1451
|
-
}
|
|
1452
|
-
function loadStack(){
|
|
1453
|
-
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div>Loading your deck…</div>');
|
|
1454
|
-
fetch(withAccount(swipeStackEndpoint),{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1455
|
-
.then(function(r){
|
|
1456
|
-
if(r.status===401){ var e=new Error('unauth'); e.code=401; throw e; }
|
|
1457
|
-
if(!r.ok){ throw new Error("Couldn't load your deck ("+r.status+")."); }
|
|
1458
|
-
return r.json();
|
|
1459
|
-
})
|
|
1460
|
-
.then(function(j){
|
|
1461
|
-
sw.loaded=true;
|
|
1462
|
-
sw.target=Number(j&&j.target_depth)||5;
|
|
1463
|
-
sw.customizing=Number(j&&j.customizing)||0;
|
|
1464
|
-
sw.productSet=Boolean(j&&j.product_description_set);
|
|
1465
|
-
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
1466
|
-
if(!sw.productSet){
|
|
1467
|
-
swShowState('<div class="rk-discover-swipe-state-title">Tell us about your product</div><div>Swipe auto-recuts viral templates into ads for your product. Add a product / offer description in Settings to start the deck.</div><a class="rk-btn rk-btn-gold rk-btn-sm" href="/settings/profile">Open Settings</a>');
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
nodes=stack.map(makeCard);
|
|
1471
|
-
if(nodes.length){ swHideState(); restack(); }
|
|
1472
|
-
else { swCaughtUp(); }
|
|
1473
|
-
refill();
|
|
1474
|
-
})
|
|
1475
|
-
.catch(function(err){
|
|
1476
|
-
if(err&&err.code===401){
|
|
1477
|
-
swShowState('<div class="rk-discover-swipe-state-title">Swipe is for your product</div><div>Log in to auto-generate ads from viral templates and swipe to approve.</div><a class="rk-btn rk-btn-gold rk-btn-sm" href="/login">Log in</a>');
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
swShowState('<div class="rk-discover-swipe-state-title">Couldn\\u2019t load your deck</div><div>'+esc(friendlyError(err))+'</div>','error');
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
|
-
// ── fullscreen swipe viewer — a TikTok-style vertical scroll over the CURRENT
|
|
1484
|
-
// deck. Its bottom bar reject/approves the ACTIVE slide (same postDecision +
|
|
1485
|
-
// refill the deck buttons use). While open it OWNS the previews: the deck's
|
|
1486
|
-
// iframes are torn down (parkDeck) so audio/CPU never double, and it drives
|
|
1487
|
-
// __VF_SWIPE_SOUND_* for the active slide's card. Closing rebuilds the deck.
|
|
1488
|
-
var swViewer=(function(){
|
|
1489
|
-
var el=root.querySelector('[data-rk-swipe-viewer]');
|
|
1490
|
-
if(!el) return null;
|
|
1491
|
-
var track=el.querySelector('[data-sv-track]');
|
|
1492
|
-
var actionsEl=el.querySelector('[data-sv-actions]');
|
|
1493
|
-
var rejectBtn=el.querySelector('[data-sv-reject]');
|
|
1494
|
-
var approveBtn=el.querySelector('[data-sv-approve]');
|
|
1495
|
-
var open=false, active=-1, sound=false, io=null, busy=false, activeCardId=null;
|
|
1496
|
-
|
|
1497
|
-
function updateMuteUi(){ el.classList.toggle('is-muted',!sound); }
|
|
1498
|
-
function syncSVSound(){
|
|
1499
|
-
try{ window.__VF_SWIPE_SOUND__ = sound===true; window.__VF_SWIPE_SOUND_TOP__ = activeCardId || null; }catch(e){}
|
|
1500
|
-
updateMuteUi();
|
|
1501
|
-
}
|
|
1502
|
-
function updateActionState(){ if(actionsEl) actionsEl.style.display=track.children.length?'':'none'; }
|
|
1503
|
-
// Detach every deck iframe so only the viewer plays audio/video.
|
|
1504
|
-
function parkDeck(){
|
|
1505
|
-
nodes.forEach(function(n){
|
|
1506
|
-
var f=n.querySelector('.rk-discover-swipe-frame');
|
|
1507
|
-
if(f&&f.parentNode) f.parentNode.removeChild(f);
|
|
1508
|
-
n._iframed=false;
|
|
1509
|
-
var fb=n.querySelector('.rk-discover-swipe-fallback');
|
|
1510
|
-
if(fb){ fb.style.opacity=''; fb.style.pointerEvents=''; }
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
function hydrate(slide){
|
|
1514
|
-
if(!slide||slide._iframed) return;
|
|
1515
|
-
var url=slide.getAttribute('data-preview-url'); if(!url) return;
|
|
1516
|
-
var fb=slide.querySelector('.rk-discover-swipe-fallback');
|
|
1517
|
-
var frame=document.createElement('iframe');
|
|
1518
|
-
frame.className='rk-discover-swipe-frame';
|
|
1519
|
-
frame.setAttribute('src',url);
|
|
1520
|
-
frame.setAttribute('title', slide.getAttribute('data-title')||'Preview');
|
|
1521
|
-
frame.setAttribute('loading','lazy');
|
|
1522
|
-
frame.setAttribute('scrolling','no');
|
|
1523
|
-
frame.setAttribute('allow','autoplay');
|
|
1524
|
-
function reveal(){ if(fb){ fb.style.opacity='0'; fb.style.pointerEvents='none'; } }
|
|
1525
|
-
frame.addEventListener('load',function(){ window.setTimeout(reveal,120); });
|
|
1526
|
-
window.setTimeout(reveal,2600);
|
|
1527
|
-
var stage=slide.querySelector('.rk-swipe-viewer-stage');
|
|
1528
|
-
if(stage) stage.insertBefore(frame, stage.firstChild); else slide.insertBefore(frame, slide.firstChild);
|
|
1529
|
-
slide._iframed=true;
|
|
1530
|
-
}
|
|
1531
|
-
function setActive(idx){
|
|
1532
|
-
active=idx;
|
|
1533
|
-
var kids=track.children;
|
|
1534
|
-
for(var i=0;i<kids.length;i++){ if(Math.abs(i-idx)<=1) hydrate(kids[i]); }
|
|
1535
|
-
var s=kids[idx];
|
|
1536
|
-
activeCardId=s? s.getAttribute('data-card-id'): null;
|
|
1537
|
-
syncSVSound();
|
|
1538
|
-
}
|
|
1539
|
-
function appendSlide(card){
|
|
1540
|
-
var slide=document.createElement('section');
|
|
1541
|
-
slide.className='rk-discover-viewer-slide rk-swipe-viewer-slide';
|
|
1542
|
-
slide.setAttribute('data-card-id', String(card.id));
|
|
1543
|
-
var url=card.preview_url||card.previewUrl||'';
|
|
1544
|
-
if(url) slide.setAttribute('data-preview-url', url);
|
|
1545
|
-
var titleTxt=String(card.title||card.source_title||card.template_id||'Template');
|
|
1546
|
-
slide.setAttribute('data-title', titleTxt);
|
|
1547
|
-
var title=esc(titleTxt);
|
|
1548
|
-
var caption=card.caption?('<div class="rk-discover-viewer-desc">'+esc(card.caption)+'</div>'):'';
|
|
1549
|
-
slide.innerHTML='<figure class="rk-discover-viewer-stage rk-swipe-viewer-stage">'
|
|
1550
|
-
+'<div class="rk-discover-swipe-fallback"><span>'+title+'</span></div>'
|
|
1551
|
-
+'<div class="rk-discover-viewer-shade"></div>'
|
|
1552
|
-
+'<div class="rk-discover-viewer-info"><div class="rk-discover-viewer-title">'+title+'</div>'+caption+'</div>'
|
|
1553
|
-
+'</figure>';
|
|
1554
|
-
track.appendChild(slide);
|
|
1555
|
-
if(open&&io) io.observe(slide);
|
|
1556
|
-
return slide;
|
|
1557
|
-
}
|
|
1558
|
-
// Keep the open track in step with the live deck: append a slide for any
|
|
1559
|
-
// node that doesn't have one yet (refills). Removals go through decide().
|
|
1560
|
-
function sync(){
|
|
1561
|
-
if(!open) return;
|
|
1562
|
-
var have={};
|
|
1563
|
-
for(var i=0;i<track.children.length;i++){ have[track.children[i].getAttribute('data-card-id')]=true; }
|
|
1564
|
-
nodes.forEach(function(n){ if(!have[String(n._card.id)]) appendSlide(n._card); });
|
|
1565
|
-
updateActionState();
|
|
1566
|
-
}
|
|
1567
|
-
function step(d){
|
|
1568
|
-
if(!open) return;
|
|
1569
|
-
var next=Math.max(0,Math.min(track.children.length-1,active+d));
|
|
1570
|
-
if(next===active) return;
|
|
1571
|
-
track.scrollTo({top:next*track.clientHeight,behavior:'smooth'});
|
|
1572
|
-
}
|
|
1573
|
-
function decideActive(dir){
|
|
1574
|
-
if(busy||active<0) return;
|
|
1575
|
-
var slide=track.children[active]; if(!slide) return;
|
|
1576
|
-
var id=slide.getAttribute('data-card-id'); var card=null;
|
|
1577
|
-
for(var i=0;i<nodes.length;i++){ if(String(nodes[i]._card.id)===String(id)){ card=nodes[i]._card; break; } }
|
|
1578
|
-
if(!card) return;
|
|
1579
|
-
busy=true; var wasIdx=active;
|
|
1580
|
-
var stage=slide.querySelector('.rk-swipe-viewer-stage');
|
|
1581
|
-
if(stage){
|
|
1582
|
-
stage.style.transition='transform .34s ease, opacity .34s ease';
|
|
1583
|
-
stage.style.transform='translateX('+(dir==='fork'?'62%':'-62%')+') rotate('+(dir==='fork'?'9deg':'-9deg')+')';
|
|
1584
|
-
stage.style.opacity='0';
|
|
1585
|
-
}
|
|
1586
|
-
postDecision(card,dir).then(function(){ refill(); });
|
|
1587
|
-
removeNodeByCard(card);
|
|
1588
|
-
window.setTimeout(function(){
|
|
1589
|
-
if(slide&&slide.parentNode) slide.parentNode.removeChild(slide);
|
|
1590
|
-
busy=false; updateActionState();
|
|
1591
|
-
if(!track.children.length){ close(); return; }
|
|
1592
|
-
var nextIdx=Math.min(wasIdx, track.children.length-1);
|
|
1593
|
-
track.scrollTop=nextIdx*track.clientHeight;
|
|
1594
|
-
setActive(nextIdx);
|
|
1595
|
-
},340);
|
|
1596
|
-
}
|
|
1597
|
-
function openAt(startId){
|
|
1598
|
-
if(!nodes.length||open) return;
|
|
1599
|
-
deckExpanded=true; parkDeck();
|
|
1600
|
-
track.innerHTML=''; sound=sw.soundOn===true; open=true;
|
|
1601
|
-
el.hidden=false; document.documentElement.classList.add('rk-noscroll');
|
|
1602
|
-
if(io) io.disconnect();
|
|
1603
|
-
io=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){
|
|
1604
|
-
entries.forEach(function(en){
|
|
1605
|
-
if(!en.isIntersecting) return;
|
|
1606
|
-
var i=[].indexOf.call(track.children,en.target);
|
|
1607
|
-
if(i>=0&&i!==active) setActive(i);
|
|
1608
|
-
});
|
|
1609
|
-
},{root:track,threshold:0.6}):null;
|
|
1610
|
-
nodes.forEach(function(n){ appendSlide(n._card); });
|
|
1611
|
-
updateActionState();
|
|
1612
|
-
var startIdx=0;
|
|
1613
|
-
for(var i=0;i<nodes.length;i++){ if(String(nodes[i]._card.id)===String(startId)){ startIdx=i; break; } }
|
|
1614
|
-
track.scrollTop=startIdx*track.clientHeight;
|
|
1615
|
-
setActive(startIdx);
|
|
1616
|
-
}
|
|
1617
|
-
function close(){
|
|
1618
|
-
if(!open) return;
|
|
1619
|
-
open=false; active=-1; activeCardId=null; busy=false;
|
|
1620
|
-
if(io){ io.disconnect(); io=null; }
|
|
1621
|
-
track.innerHTML=''; el.hidden=true;
|
|
1622
|
-
document.documentElement.classList.remove('rk-noscroll');
|
|
1623
|
-
deckExpanded=false; sw.soundOn=sound; // carry the sound pref back to the deck
|
|
1624
|
-
restack(); updateSoundBtn();
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
el.querySelectorAll('[data-sv-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
1628
|
-
var muteBtn=el.querySelector('[data-sv-mute]');
|
|
1629
|
-
if(muteBtn) muteBtn.addEventListener('click',function(){ sound=!sound; syncSVSound(); });
|
|
1630
|
-
var prevBtn=el.querySelector('[data-sv-prev]'); if(prevBtn) prevBtn.addEventListener('click',function(){ step(-1); });
|
|
1631
|
-
var nextBtn=el.querySelector('[data-sv-next]'); if(nextBtn) nextBtn.addEventListener('click',function(){ step(1); });
|
|
1632
|
-
if(rejectBtn) rejectBtn.addEventListener('click',function(){ decideActive('skip'); });
|
|
1633
|
-
if(approveBtn) approveBtn.addEventListener('click',function(){ decideActive('fork'); });
|
|
1634
|
-
// tap the empty gutter (not the phone frame) closes the viewer
|
|
1635
|
-
track.addEventListener('click',function(e){
|
|
1636
|
-
var t=e.target;
|
|
1637
|
-
if(t&&t.classList&&(t.classList.contains('rk-discover-viewer-slide')||t===track)) close();
|
|
1638
|
-
});
|
|
1639
|
-
document.addEventListener('keydown',function(e){
|
|
1640
|
-
if(!open) return;
|
|
1641
|
-
if(e.key==='Escape'){ e.preventDefault(); close(); }
|
|
1642
|
-
else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); }
|
|
1643
|
-
else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); }
|
|
1644
|
-
else if(e.key==='ArrowRight'){ e.preventDefault(); decideActive('fork'); }
|
|
1645
|
-
else if(e.key==='ArrowLeft'){ e.preventDefault(); decideActive('skip'); }
|
|
1646
|
-
});
|
|
1647
|
-
window.addEventListener('resize',function(){ if(open&&active>=0) track.scrollTop=active*track.clientHeight; });
|
|
1648
|
-
|
|
1649
|
-
return { openAt:openAt, close:close, sync:sync, isOpen:function(){ return open; } };
|
|
1650
|
-
})();
|
|
1651
|
-
|
|
1652
|
-
// Tap the front card → open the fullscreen viewer at that card.
|
|
1653
|
-
if(deck) deck.addEventListener('click',function(e){
|
|
1654
|
-
if(sw.busy||(swViewer&&swViewer.isOpen())||!nodes.length) return;
|
|
1655
|
-
var el=(e.target&&e.target.closest)?e.target.closest('.rk-discover-swipe'):null;
|
|
1656
|
-
if(!el||!el._card||el!==nodes[0]) return; // only the front card is interactive
|
|
1657
|
-
if(swViewer) swViewer.openAt(el._card.id);
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
if(skipBtn) skipBtn.addEventListener('click',function(){ decide('skip'); });
|
|
1661
|
-
if(forkBtn) forkBtn.addEventListener('click',function(){ decide('fork'); });
|
|
1662
|
-
loadStack();
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1666
|
-
// CATEGORIES
|
|
1667
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
1668
|
-
var catsRoot=root.querySelector('[data-rk-cats]');
|
|
1669
|
-
if(catsRoot){
|
|
1670
|
-
var catsGrid=catsRoot.querySelector('[data-rk-cats-grid]');
|
|
1671
|
-
var reveal=catsRoot.querySelector('[data-rk-cat-reveal]');
|
|
1672
|
-
var revealGrid=catsRoot.querySelector('[data-rk-reveal-grid]');
|
|
1673
|
-
// reveal header ("← All categories" + group title) lives in the toolbar row
|
|
1674
|
-
var revealHead=root.querySelector('[data-rk-reveal-head]');
|
|
1675
|
-
var revealTitle=root.querySelector('[data-rk-reveal-title]');
|
|
1676
|
-
var backBtn=root.querySelector('[data-rk-cat-back]');
|
|
1677
|
-
var catsState=catsRoot.querySelector('[data-rk-cats-state]');
|
|
1678
|
-
var groups={}; // label -> [templates]
|
|
1679
|
-
var revealItems=[]; // the currently-revealed group's templates (viewer list)
|
|
1680
|
-
|
|
1681
|
-
function miniHtml(t){
|
|
1682
|
-
var url=t.previewUrl||t.preview_url||'';
|
|
1683
|
-
if(!url) return '<span class="rk-discover-mini rk-discover-mini-ph"></span>';
|
|
1684
|
-
if(isImage(url)) return '<span class="rk-discover-mini"><img src="'+attr(url)+'" alt="" loading="lazy" decoding="async"></span>';
|
|
1685
|
-
// First-frame still, NOT autoplay: 4 tiles x 16 category cards would be
|
|
1686
|
-
// ~64 playing videos. preload=metadata + #t=0.1 paints frame one only.
|
|
1687
|
-
return '<span class="rk-discover-mini"><video src="'+attr(url+'#t=0.1')+'" muted playsinline preload="metadata" tabindex="-1" aria-hidden="true"></video></span>';
|
|
1688
|
-
}
|
|
1689
|
-
function showReveal(label){
|
|
1690
|
-
var items=groups[label]||[];
|
|
1691
|
-
if(!items.length) return false;
|
|
1692
|
-
revealItems=items;
|
|
1693
|
-
if(revealTitle) revealTitle.textContent=label;
|
|
1694
|
-
// cards carry data-rk-view=<i> so a plain click opens the shared viewer
|
|
1695
|
-
if(revealGrid) revealGrid.innerHTML=items.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
1696
|
-
if(catsGrid) catsGrid.hidden=true;
|
|
1697
|
-
if(reveal) reveal.hidden=false;
|
|
1698
|
-
if(revealHead) revealHead.hidden=false;
|
|
1699
|
-
window.scrollTo(0,0);
|
|
1700
|
-
return true;
|
|
1701
|
-
}
|
|
1702
|
-
function hideReveal(){
|
|
1703
|
-
if(viewerCtl&&viewerCtl.isOpen()) viewerCtl.close();
|
|
1704
|
-
revealItems=[];
|
|
1705
|
-
if(reveal) reveal.hidden=true;
|
|
1706
|
-
if(revealHead) revealHead.hidden=true;
|
|
1707
|
-
if(catsGrid) catsGrid.hidden=false;
|
|
1708
|
-
}
|
|
1709
|
-
if(backBtn) backBtn.addEventListener('click',hideReveal);
|
|
1710
|
-
bindHoverAudio(revealGrid); // reveal cards share posterCardHtml → same hover audio
|
|
1711
|
-
bindViewerOpens(revealGrid, function(){ return revealItems; }); // click → shared TikTok viewer
|
|
1712
|
-
|
|
1713
|
-
// Intercept category-card clicks: if we have live templates for that group,
|
|
1714
|
-
// reveal them in-page; otherwise let the href fall through to a filtered
|
|
1715
|
-
// feed (graceful offline / not-yet-seeded fallback).
|
|
1716
|
-
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1717
|
-
a.addEventListener('click',function(e){
|
|
1718
|
-
var label=a.getAttribute('data-rk-cat');
|
|
1719
|
-
if(groups[label]&&groups[label].length){ e.preventDefault(); showReveal(label); }
|
|
1720
|
-
});
|
|
1721
|
-
});
|
|
1722
|
-
|
|
1723
|
-
function enrichCards(){
|
|
1724
|
-
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1725
|
-
var label=a.getAttribute('data-rk-cat');
|
|
1726
|
-
var items=groups[label]||[];
|
|
1727
|
-
if(!items.length) return;
|
|
1728
|
-
var thumbs=a.querySelector('[data-rk-cat-thumbs]');
|
|
1729
|
-
if(thumbs) thumbs.innerHTML=items.slice(0,4).map(miniHtml).join('');
|
|
1730
|
-
var count=a.querySelector('[data-rk-cat-count]');
|
|
1731
|
-
if(count) count.textContent=items.length+' template'+(items.length===1?'':'s');
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
function ingestPopular(list){
|
|
1735
|
-
groups={};
|
|
1736
|
-
(list||[]).forEach(function(t){ var g=(t.popularGroup||'Popular').trim()||'Popular'; (groups[g]=groups[g]||[]).push(t); });
|
|
1737
|
-
enrichCards();
|
|
1738
|
-
}
|
|
1739
|
-
function loadPopular(){
|
|
1740
|
-
var url=popularEndpoint;
|
|
1741
|
-
try{ var u=new URL(popularEndpoint, window.location.href); if(accountId) u.searchParams.set('account', accountId); url=u.toString(); }catch(e){ url=popularEndpoint; }
|
|
1742
|
-
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1743
|
-
.then(function(r){ if(!r.ok){ throw new Error("Couldn't load categories ("+r.status+")."); } return r.json(); })
|
|
1744
|
-
.then(function(j){
|
|
1745
|
-
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1746
|
-
if(list.length){ ingestPopular(list); return; }
|
|
1747
|
-
// Local serve without the popular catalog seeded (and no upstream
|
|
1748
|
-
// passthrough configured): fall back to the public prod endpoint —
|
|
1749
|
-
// it's CORS-open and unauthenticated — so the sandbox still shows
|
|
1750
|
-
// real first-frame previews on a fully-local box.
|
|
1751
|
-
loadPopularFallback();
|
|
1752
|
-
// The static skeleton already shows every real group; the fetch just
|
|
1753
|
-
// enriches it, so a fetch that returns nothing is not an error state.
|
|
1754
|
-
})
|
|
1755
|
-
.catch(function(){
|
|
1756
|
-
// Offline / unreachable: try the public prod catalog before settling
|
|
1757
|
-
// for the static skeleton grid (real group labels + counts, clicks
|
|
1758
|
-
// fall through to the filtered feed).
|
|
1759
|
-
loadPopularFallback();
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
function loadPopularFallback(){
|
|
1763
|
-
if(window.location.hostname==='vidfarm.cc') return; // already prod
|
|
1764
|
-
fetch('https://vidfarm.cc/discover/popular',{headers:{'accept':'application/json'}})
|
|
1765
|
-
.then(function(r){ return r.ok?r.json():null; })
|
|
1766
|
-
.then(function(j){
|
|
1767
|
-
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1768
|
-
if(list.length) ingestPopular(list);
|
|
1769
|
-
})
|
|
1770
|
-
.catch(function(){ /* keep the static skeleton */ });
|
|
1771
|
-
}
|
|
1772
|
-
loadPopular();
|
|
1773
|
-
}
|
|
1774
|
-
})();`;
|
|
1775
|
-
return reskinDocument({
|
|
1776
|
-
title: `vidfarm reskin — Discover (${mode})`,
|
|
1777
|
-
description: "Reskinned vidfarm Discover — real Feed / Swipe / Categories, live template posters, warm sunny SaaS.",
|
|
1778
|
-
activeSlug: "discover",
|
|
1779
|
-
account: { name: input.name, email: input.email },
|
|
1780
|
-
pageCss,
|
|
1781
|
-
body,
|
|
1782
|
-
script,
|
|
1783
|
-
// Feed mode hydrates a Zustand store (SSR-seed + stale-while-revalidate);
|
|
1784
|
-
// the other modes don't need it.
|
|
1785
|
-
moduleScripts: mode === "feed" ? ["/assets/discover-client-app.js"] : undefined
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
//# sourceMappingURL=discover-page.js.map
|