@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
// live editorHref) and override any endpoint.
|
|
42
42
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
43
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). */
|
|
44
46
|
const DEFAULT_INPUT = {
|
|
45
47
|
accountId: "",
|
|
46
48
|
isLoggedIn: false,
|
|
@@ -69,6 +71,8 @@ const MODE_COPY = {
|
|
|
69
71
|
};
|
|
70
72
|
// Functional (non-decorative) SVG glyphs.
|
|
71
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>`;
|
|
72
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>`;
|
|
73
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>`;
|
|
74
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>`;
|
|
@@ -77,6 +81,8 @@ const SOUND_OFF_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="no
|
|
|
77
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>`;
|
|
78
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>`;
|
|
79
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>`;
|
|
80
86
|
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
81
87
|
function jsonForScript(value) {
|
|
82
88
|
return JSON.stringify(value ?? null)
|
|
@@ -90,7 +96,7 @@ function jsonForScript(value) {
|
|
|
90
96
|
function modeToggle(active) {
|
|
91
97
|
const links = MODES.map((m) => {
|
|
92
98
|
const on = m.mode === active ? " is-active" : "";
|
|
93
|
-
return `<a class="rk-discover-mode${on}" href="/discover
|
|
99
|
+
return `<a class="rk-discover-mode${on}" href="/discover/${m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
94
100
|
}).join("");
|
|
95
101
|
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
96
102
|
}
|
|
@@ -139,9 +145,83 @@ function viewerHtml() {
|
|
|
139
145
|
</div>
|
|
140
146
|
</div>`;
|
|
141
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
|
+
}
|
|
142
214
|
export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
143
215
|
const copy = MODE_COPY[mode];
|
|
144
|
-
|
|
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
|
+
};
|
|
145
225
|
let modeBody = "";
|
|
146
226
|
// ── FEED ────────────────────────────────────────────────────────────────
|
|
147
227
|
if (mode === "feed") {
|
|
@@ -158,8 +238,7 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
158
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>
|
|
159
239
|
<input type="search" data-rk-search placeholder="Search templates" autocomplete="off" aria-label="Search templates" />
|
|
160
240
|
</label>
|
|
161
|
-
<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
|
|
162
|
-
<span class="rk-hint rk-discover-count" data-rk-count></span>`
|
|
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>`
|
|
163
242
|
: "";
|
|
164
243
|
// Categories' in-page group reveal header ("← All categories" + the group
|
|
165
244
|
// title) lives IN the toolbar row, next to the mode toggle (user feedback);
|
|
@@ -177,12 +256,13 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
177
256
|
<div class="rk-discover-stage">
|
|
178
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>
|
|
179
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>
|
|
180
260
|
<div class="rk-discover-swipe-state" data-rk-swipe-state>Loading your deck…</div>
|
|
181
261
|
</div>
|
|
182
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>
|
|
183
263
|
</div>
|
|
184
|
-
<p class="rk-discover-act-hint" data-actions-hint hidden>Swipe to customize into an ad for your brand.</p>
|
|
185
|
-
</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()}`;
|
|
186
266
|
}
|
|
187
267
|
// ── CATEGORIES ──────────────────────────────────────────────────────────
|
|
188
268
|
if (mode === "categories") {
|
|
@@ -195,11 +275,21 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
195
275
|
<div class="rk-discover-state rk-discover-cats-state" data-rk-cats-state hidden></div>
|
|
196
276
|
</div>${viewerHtml()}`;
|
|
197
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
|
+
: "";
|
|
198
285
|
const body = `
|
|
199
286
|
<main class="rk-container rk-page">
|
|
200
287
|
<div class="rk-page-head rk-discover-head">
|
|
201
|
-
<
|
|
202
|
-
|
|
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}
|
|
203
293
|
</div>
|
|
204
294
|
<div class="rk-discover-toolbar">
|
|
205
295
|
${modeToggle(mode)}
|
|
@@ -207,10 +297,58 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
207
297
|
${toolbarSearch}
|
|
208
298
|
</div>
|
|
209
299
|
${modeBody}
|
|
300
|
+
${mode === "feed" ? addModalHtml() : ""}
|
|
210
301
|
<script type="application/json" data-rk-discover-boot>${jsonForScript(boot)}</script>
|
|
211
302
|
</main>`;
|
|
212
303
|
const pageCss = `
|
|
213
|
-
|
|
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}
|
|
214
352
|
|
|
215
353
|
/* toolbar row: mode toggle + (feed) quick search share one line, wrap on mobile */
|
|
216
354
|
.rk-discover-toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:26px}
|
|
@@ -233,11 +371,10 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
233
371
|
.rk-discover-search-ic{color:var(--rk-text-faint);display:grid;place-items:center;flex:none}
|
|
234
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)}
|
|
235
373
|
.rk-discover-search input::placeholder{color:var(--rk-n-400)}
|
|
236
|
-
.rk-discover-count{white-space:nowrap}
|
|
237
374
|
|
|
238
375
|
.rk-discover-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:22px}
|
|
239
376
|
@media(max-width:520px){.rk-discover-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}}
|
|
240
|
-
.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}
|
|
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}
|
|
241
378
|
.rk-discover-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:var(--rk-n-100);color:inherit}
|
|
242
379
|
.rk-discover-poster img,.rk-discover-poster video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
243
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);
|
|
@@ -252,15 +389,28 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
252
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;
|
|
253
390
|
transition:transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
254
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}}
|
|
255
395
|
.rk-discover-bm:hover{transform:scale(1.08);background:rgba(0,0,0,.6)}
|
|
256
396
|
.rk-discover-bm.is-on{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
257
397
|
.rk-discover-bm.is-on svg{fill:currentColor}
|
|
258
|
-
.rk-discover-body{padding:15px 16px 16px;display:grid;gap:13px}
|
|
259
|
-
.rk-discover-titlewrap{display:grid;gap:6px}
|
|
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}
|
|
260
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;
|
|
261
401
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
262
402
|
.rk-discover-desc{font-size:12.5px;font-weight:500;color:var(--rk-text-muted);line-height:1.4;
|
|
263
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}
|
|
264
414
|
.rk-discover-fork{gap:7px}
|
|
265
415
|
.rk-discover-fork svg{margin-top:-1px}
|
|
266
416
|
.rk-discover-fork-disabled{opacity:.6;cursor:default;pointer-events:none}
|
|
@@ -358,12 +508,22 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
358
508
|
transition:transform .38s var(--rk-ease),opacity .38s var(--rk-ease)}
|
|
359
509
|
.rk-discover-swipe-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;pointer-events:none;background:#000}
|
|
360
510
|
.rk-discover-swipe-fallback{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;
|
|
361
|
-
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
|
|
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)}
|
|
362
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%)}
|
|
363
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}
|
|
364
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}
|
|
365
516
|
.rk-discover-swipe-caption{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.84);line-height:1.35;
|
|
366
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)}
|
|
367
527
|
|
|
368
528
|
/* deck flanked by skip/fork — buttons ride the card's vertical center, 1fr gutters absorb spare width.
|
|
369
529
|
Explicit column placement keeps the deck centered when the buttons are display:none (empty/caught-up states). */
|
|
@@ -382,6 +542,17 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
382
542
|
.rk-discover-act-skip{grid-area:skip;justify-self:center}
|
|
383
543
|
.rk-discover-act-fork{grid-area:fork;justify-self:center}
|
|
384
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
|
+
}
|
|
385
556
|
.rk-discover-act:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
386
557
|
.rk-discover-act:disabled{opacity:.5;cursor:default;transform:none}
|
|
387
558
|
.rk-discover-act-skip{color:var(--rk-n-500)}
|
|
@@ -390,6 +561,19 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
390
561
|
.rk-discover-act-fork:hover{background:var(--rk-gold-700);box-shadow:0 8px 22px -6px rgba(252,185,0,.6)}
|
|
391
562
|
.rk-discover-act-hint{font-size:12.5px;font-weight:600;color:var(--rk-text-faint);text-align:center;letter-spacing:.01em}
|
|
392
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}}
|
|
393
577
|
|
|
394
578
|
/* ── categories: browse-by-format cards ── */
|
|
395
579
|
.rk-discover-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:20px}
|
|
@@ -447,6 +631,61 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
447
631
|
var u=editorBase+'/'+encodeURIComponent(String(id));
|
|
448
632
|
return accountId? u+'?account='+encodeURIComponent(accountId) : u;
|
|
449
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
|
+
});
|
|
450
689
|
function fmtDuration(s){
|
|
451
690
|
s=Number(s); if(!s||!isFinite(s)||s<=0) return '';
|
|
452
691
|
if(s>=60){ var m=Math.floor(s/60); var r=Math.round(s%60); return m+':'+(r<10?'0':'')+r; }
|
|
@@ -455,6 +694,8 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
455
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'};
|
|
456
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); }
|
|
457
696
|
var EDIT_ICON=${JSON.stringify(EDIT_ICON)};
|
|
697
|
+
var COPY_ICON=${JSON.stringify(COPY_ICON)};
|
|
698
|
+
var IDCHECK_ICON=${JSON.stringify(IDCHECK_ICON)};
|
|
458
699
|
var BOOKMARK_ICON=${JSON.stringify(BOOKMARK_ICON)};
|
|
459
700
|
var SOUND_ON_ICON=${JSON.stringify(SOUND_ON_ICON)};
|
|
460
701
|
var SOUND_OFF_ICON=${JSON.stringify(SOUND_OFF_ICON)};
|
|
@@ -528,32 +769,41 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
528
769
|
// opens the editor via the untouched href). No platform/source pill on the
|
|
529
770
|
// poster (user feedback) — source shows only in the viewer's meta line.
|
|
530
771
|
function posterCardHtml(t, viewIdx){
|
|
531
|
-
var id=t
|
|
772
|
+
var id=domId(t);
|
|
773
|
+
var tid=realTemplateId(t);
|
|
774
|
+
var remix=needsDecompose(t);
|
|
532
775
|
var url=t.previewUrl||t.preview_url||'';
|
|
533
776
|
var hasVideo=Boolean(url)&&!isImage(url);
|
|
534
777
|
var pending=(t.status==='processing'||t.status==='failed');
|
|
535
778
|
var viewable=!pending&&Boolean(url)&&typeof viewIdx==='number'&&viewIdx>=0;
|
|
536
779
|
var dur=fmtDuration(t.durationSeconds);
|
|
537
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);
|
|
538
785
|
var posterInner=mediaHtml(t)
|
|
539
786
|
+(dur?'<span class="rk-discover-dur">'+esc(dur)+'</span>':'')
|
|
540
787
|
+'<div class="rk-discover-scrim"></div>'
|
|
541
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>':'');
|
|
542
789
|
var poster=pending
|
|
543
790
|
? '<div class="rk-discover-poster">'+posterInner+'</div>'
|
|
544
|
-
: '<a class="rk-discover-poster" href="'+attr(
|
|
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>';
|
|
545
792
|
// bookmark sits NEXT TO the anchor inside the wrap (button-in-anchor is
|
|
546
793
|
// invalid HTML); the wrap is the shared hover context for sound + bookmark
|
|
547
794
|
poster='<div class="rk-discover-posterwrap">'+poster+(pending?'':bmBtnHtml(id,'rk-discover-bm'))+'</div>';
|
|
548
795
|
var cta=pending
|
|
549
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>'
|
|
550
|
-
:
|
|
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>';
|
|
551
800
|
return '<article class="rk-discover-card rk-card-hover">'
|
|
552
801
|
+ poster
|
|
553
802
|
+ '<div class="rk-discover-body">'
|
|
554
803
|
+ '<div class="rk-discover-titlewrap">'
|
|
555
804
|
+ '<h3 class="rk-discover-title">'+esc(t.title||id)+'</h3>'
|
|
556
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>':'')
|
|
557
807
|
+ '</div>'
|
|
558
808
|
+ cta
|
|
559
809
|
+ '</div>'
|
|
@@ -610,7 +860,9 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
610
860
|
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null; var parkEl=null;
|
|
611
861
|
|
|
612
862
|
function viewerSlideHtml(t,i){
|
|
613
|
-
var id=t
|
|
863
|
+
var id=domId(t);
|
|
864
|
+
var tid=realTemplateId(t);
|
|
865
|
+
var remix=needsDecompose(t);
|
|
614
866
|
var url=t.previewUrl||t.preview_url||'';
|
|
615
867
|
var media=isImage(url)
|
|
616
868
|
? '<img class="rk-discover-viewer-media" src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'">'
|
|
@@ -629,7 +881,9 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
629
881
|
+(meta.length?'<div class="rk-discover-viewer-meta">'+meta.join(' · ')+'</div>':'')
|
|
630
882
|
+'<div class="rk-discover-viewer-title">'+esc(t.title||id)+'</div>'
|
|
631
883
|
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
632
|
-
+
|
|
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>')
|
|
633
887
|
+'</div>'
|
|
634
888
|
+'</figure>'
|
|
635
889
|
+'</section>';
|
|
@@ -764,9 +1018,18 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
764
1018
|
if(feedRoot){
|
|
765
1019
|
var gridEl=feedRoot.querySelector('[data-rk-grid]');
|
|
766
1020
|
var stateEl=feedRoot.querySelector('[data-rk-state]');
|
|
767
|
-
// search
|
|
768
|
-
|
|
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.
|
|
769
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
|
+
}
|
|
770
1033
|
var bmChip=root.querySelector('[data-rk-bmfilter]');
|
|
771
1034
|
var bmCountEl=root.querySelector('[data-rk-bmcount]');
|
|
772
1035
|
var bmFilter=false;
|
|
@@ -776,7 +1039,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
776
1039
|
function feedSetState(msg,isError){
|
|
777
1040
|
if(gridEl) gridEl.innerHTML='';
|
|
778
1041
|
if(stateEl){ stateEl.hidden=false; stateEl.innerHTML=msg; stateEl.classList.toggle('is-error',!!isError); }
|
|
779
|
-
|
|
1042
|
+
setCountHint(null);
|
|
780
1043
|
}
|
|
781
1044
|
function updateBmChip(){
|
|
782
1045
|
if(!bmChip) return;
|
|
@@ -798,7 +1061,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
798
1061
|
}
|
|
799
1062
|
if(stateEl) stateEl.hidden=true;
|
|
800
1063
|
if(gridEl) gridEl.innerHTML=vis.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
801
|
-
|
|
1064
|
+
setCountHint(vis.length);
|
|
802
1065
|
}
|
|
803
1066
|
if(bmChip) bmChip.addEventListener('click',function(){ bmFilter=!bmFilter; feedRender(); });
|
|
804
1067
|
bmChangeHandlers.push(function(){
|
|
@@ -807,17 +1070,42 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
807
1070
|
// re-render restarts muted autoplay, so re-park it if the viewer is open
|
|
808
1071
|
if(bmFilter){ feedRender(); if(viewerCtl&&viewerCtl.isOpen()) parkVideos(gridEl); }
|
|
809
1072
|
});
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
feedState.loading=
|
|
813
|
-
|
|
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){
|
|
814
1081
|
var url=feedEndpoint;
|
|
815
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; }
|
|
816
1083
|
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
817
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(); })
|
|
818
|
-
.then(function(j){ if(my!==feedReq) return;
|
|
1085
|
+
.then(function(j){ if(my!==feedReq) return; applyFeedItems((j&&Array.isArray(j.templates))?j.templates:[]); })
|
|
819
1086
|
.catch(function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); });
|
|
820
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
|
+
}
|
|
821
1109
|
// Honor a ?q= (e.g. from a category link) in the page URL.
|
|
822
1110
|
var initialQuery='';
|
|
823
1111
|
try{ initialQuery=(new URL(window.location.href)).searchParams.get('q')||''; }catch(e){ initialQuery=''; }
|
|
@@ -835,7 +1123,140 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
835
1123
|
// plain click on a card → shared viewer over the CURRENT visible feed
|
|
836
1124
|
// (search + bookmark filter applied)
|
|
837
1125
|
bindViewerOpens(gridEl, function(){ return feedState.visible; });
|
|
838
|
-
|
|
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
|
+
}
|
|
839
1260
|
}
|
|
840
1261
|
|
|
841
1262
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -849,8 +1270,35 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
849
1270
|
var hint=swipeRoot.querySelector('[data-actions-hint]');
|
|
850
1271
|
var skipBtn=swipeRoot.querySelector('[data-swipe-skip]');
|
|
851
1272
|
var forkBtn=swipeRoot.querySelector('[data-swipe-fork]');
|
|
852
|
-
var
|
|
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:{}};
|
|
853
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(); });
|
|
854
1302
|
|
|
855
1303
|
function swShowState(html,kind){
|
|
856
1304
|
if(swStateEl){ swStateEl.hidden=false; swStateEl.innerHTML=html; swStateEl.classList.toggle('is-error',kind==='error'); }
|
|
@@ -860,6 +1308,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
860
1308
|
function swHideState(){ if(swStateEl) swStateEl.hidden=true; }
|
|
861
1309
|
|
|
862
1310
|
function makeCard(card){
|
|
1311
|
+
if(card&&card.id) sw.known[String(card.id)]=true;
|
|
863
1312
|
var el=document.createElement('article');
|
|
864
1313
|
el.className='rk-discover-swipe';
|
|
865
1314
|
el._card=card; el._iframed=false;
|
|
@@ -883,6 +1332,13 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
883
1332
|
frame.setAttribute('loading','lazy');
|
|
884
1333
|
frame.setAttribute('scrolling','no');
|
|
885
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);
|
|
886
1342
|
if(fb&&fb.parentNode) fb.parentNode.insertBefore(frame, fb); else el.insertBefore(frame, el.firstChild);
|
|
887
1343
|
el._iframed=true;
|
|
888
1344
|
}
|
|
@@ -893,11 +1349,16 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
893
1349
|
el.style.zIndex=String(20-i);
|
|
894
1350
|
el.style.transform='translateY('+(i*12)+'px) scale('+(1-i*0.045).toFixed(3)+')';
|
|
895
1351
|
el.style.opacity=i===0?'1':(i===1?'0.92':'0.82');
|
|
896
|
-
|
|
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);
|
|
897
1355
|
});
|
|
898
1356
|
var has=nodes.length>0;
|
|
899
1357
|
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
900
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();
|
|
901
1362
|
if(!has) swCaughtUp();
|
|
902
1363
|
}
|
|
903
1364
|
function swCaughtUp(){
|
|
@@ -907,6 +1368,25 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
907
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>');
|
|
908
1369
|
}
|
|
909
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
|
+
}
|
|
910
1390
|
function decide(dir){
|
|
911
1391
|
if(sw.busy||!nodes.length) return;
|
|
912
1392
|
sw.busy=true;
|
|
@@ -917,11 +1397,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
917
1397
|
el.style.transform='translateX('+(dir==='fork'?460:-460)+'px) rotate('+(dir==='fork'?15:-15)+'deg)';
|
|
918
1398
|
el.style.opacity='0';
|
|
919
1399
|
window.setTimeout(function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); },380);
|
|
920
|
-
|
|
921
|
-
fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
922
|
-
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
923
|
-
.catch(function(){ /* swallowed — the card already left the deck */ })
|
|
924
|
-
.then(function(){ sw.busy=false; restack(); refill(); });
|
|
1400
|
+
postDecision(card,dir).then(function(){ sw.busy=false; restack(); refill(); });
|
|
925
1401
|
}
|
|
926
1402
|
function refill(){
|
|
927
1403
|
if(sw.refilling||!sw.productSet||!sw.loaded) return;
|
|
@@ -935,8 +1411,43 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
935
1411
|
if(j.filled&&j.record){ makeCard(j.record); restack(); }
|
|
936
1412
|
var more=j.filled&&nodes.length<sw.target&&j.reason!=='no_eligible'&&j.reason!=='at_target';
|
|
937
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();
|
|
938
1418
|
})
|
|
939
|
-
.catch(function(){
|
|
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(); });
|
|
940
1451
|
}
|
|
941
1452
|
function loadStack(){
|
|
942
1453
|
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div>Loading your deck…</div>');
|
|
@@ -953,7 +1464,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
953
1464
|
sw.productSet=Boolean(j&&j.product_description_set);
|
|
954
1465
|
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
955
1466
|
if(!sw.productSet){
|
|
956
|
-
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
|
|
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>');
|
|
957
1468
|
return;
|
|
958
1469
|
}
|
|
959
1470
|
nodes=stack.map(makeCard);
|
|
@@ -969,6 +1480,183 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
969
1480
|
swShowState('<div class="rk-discover-swipe-state-title">Couldn\\u2019t load your deck</div><div>'+esc(friendlyError(err))+'</div>','error');
|
|
970
1481
|
});
|
|
971
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
|
+
|
|
972
1660
|
if(skipBtn) skipBtn.addEventListener('click',function(){ decide('skip'); });
|
|
973
1661
|
if(forkBtn) forkBtn.addEventListener('click',function(){ decide('fork'); });
|
|
974
1662
|
loadStack();
|
|
@@ -1088,9 +1776,13 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1088
1776
|
title: `vidfarm reskin — Discover (${mode})`,
|
|
1089
1777
|
description: "Reskinned vidfarm Discover — real Feed / Swipe / Categories, live template posters, warm sunny SaaS.",
|
|
1090
1778
|
activeSlug: "discover",
|
|
1779
|
+
account: { name: input.name, email: input.email },
|
|
1091
1780
|
pageCss,
|
|
1092
1781
|
body,
|
|
1093
|
-
script
|
|
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
|
|
1094
1786
|
});
|
|
1095
1787
|
}
|
|
1096
1788
|
//# sourceMappingURL=discover-page.js.map
|