@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1
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/vidfarm-media/SKILL.md +43 -6
- package/SKILL.director.md +37 -23
- package/SKILL.platform.md +6 -6
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1832 -213
- package/dist/src/cli.js +238 -17
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +116 -73
- package/dist/src/devcli/composition-edit.js +262 -0
- package/dist/src/devcli/timeline-edit.js +283 -0
- package/dist/src/editor-chat.js +29 -6
- 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 +744 -0
- package/dist/src/frontend/template-editor-chat.js +22 -19
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +25 -1
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +503 -187
- package/dist/src/reskin/chat-page.js +739 -299
- package/dist/src/reskin/discover-page.js +1450 -279
- package/dist/src/reskin/document.js +1392 -16
- package/dist/src/reskin/help-page.js +139 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +547 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +1188 -317
- package/dist/src/reskin/login-page.js +124 -87
- package/dist/src/reskin/pricing-page.js +197 -166
- package/dist/src/reskin/settings-page.js +566 -187
- package/dist/src/reskin/theme.js +434 -17
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +79 -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 +14 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +114 -0
- package/dist/src/services/storage.js +24 -1
- 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/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,68 +1,89 @@
|
|
|
1
|
-
// /reskin/settings — the flagship reskin
|
|
2
|
-
// migration: shows how to port a real vidfarm page (the 4-tab Settings screen)
|
|
3
|
-
// into the farmville design system using ONLY shared rk-* primitives + a small
|
|
4
|
-
// page-scoped CSS block + a self-contained module script.
|
|
1
|
+
// /reskin/settings — the flagship reskin, now MIGRATED to real production data.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// This is the reference for the /reskin/* UX migration: the renderer accepts the
|
|
4
|
+
// SAME input object the live settings route already builds (via
|
|
5
|
+
// `Parameters<typeof renderSettingsPage>[0]`), so the route wiring is just "fetch
|
|
6
|
+
// real data + call this instead of the legacy renderer". Every form posts to the
|
|
7
|
+
// REAL /settings/* endpoints with the correct account + ids, so profile save,
|
|
8
|
+
// AI-key add/remove, FlockPoster connect, channel resend/delete, attachment
|
|
9
|
+
// upload/delete, and wallet top-up all actually work. Design stays farmville
|
|
10
|
+
// (shared rk-* primitives, isolated CSS); only the data + actions became real.
|
|
11
11
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
12
|
-
const SAMPLE = {
|
|
13
|
-
name: "Dylan Vane",
|
|
14
|
-
email: "dylan@studio.co",
|
|
15
|
-
accountId: "acct_8f2a10",
|
|
16
|
-
isPaidPlan: true,
|
|
17
|
-
about: "Vidfarm makes short-form ads for DTC brands. Punchy hooks, UGC energy, captions that pop. Audience: founders & marketers who want to ship a week of content in an afternoon.",
|
|
18
|
-
vidfarmApiKey: "vf_live_9c41f0b7a2e84d6f8b3c9a1e",
|
|
19
|
-
wallet: { balanceUsd: 12.4, totalFundsAddedUsd: 40, totalChargeUsd: 27.6, updatedAt: "Jul 7, 2026, 4:12 PM" },
|
|
20
|
-
walletEvents: [
|
|
21
|
-
{ title: "Wallet top-up", sub: "Visa ···· 4242", amountUsd: 20, when: "Jul 6", kind: "credit" },
|
|
22
|
-
{ title: "Timeline render usage", sub: "HyperFrames Lambda · job_a91f", amountUsd: -1.8, when: "Jul 6", kind: "debit" },
|
|
23
|
-
{ title: "Clip scan usage", sub: "clip_scan_lambda · 14 clips", amountUsd: -0.42, when: "Jul 5", kind: "debit" },
|
|
24
|
-
{ title: "Timeline render usage", sub: "HyperFrames Lambda · job_77c2", amountUsd: -2.1, when: "Jul 4", kind: "debit" },
|
|
25
|
-
{ title: "Wallet top-up", sub: "Visa ···· 4242", amountUsd: 20, when: "Jul 1", kind: "credit" }
|
|
26
|
-
],
|
|
27
|
-
attachments: [
|
|
28
|
-
{ fileName: "brand-guidelines.pdf", kind: "PDF", size: "2.4 MB", when: "Jul 2" },
|
|
29
|
-
{ fileName: "logo-lockup.png", kind: "PNG", size: "184 KB", when: "Jul 2" },
|
|
30
|
-
{ fileName: "founder-vo-sample.mp3", kind: "Audio", size: "1.1 MB", when: "Jun 28" }
|
|
31
|
-
],
|
|
32
|
-
channels: [
|
|
33
|
-
{ title: "Vidfarm Daily", platform: "TikTok", handle: "@vidfarm", status: "Connected", kind: "flockposter" },
|
|
34
|
-
{ title: "Founder Reels", platform: "Instagram", handle: "@dylan.builds", status: "Connected", kind: "flockposter" },
|
|
35
|
-
{ title: "Newsletter drop", platform: "Email", handle: "dylan@studio.co", status: "Verify sent", kind: "email" }
|
|
36
|
-
],
|
|
37
|
-
providerKeys: [
|
|
38
|
-
{ provider: "OpenAI", label: "gpt-5.4 workspace", secret: "sk-proj-1a2b3c4d5e6f7g8h9i0j", status: "Active", lastUsed: "2h ago" },
|
|
39
|
-
{ provider: "Gemini", label: "clip tagging", secret: "AIzaSyD-9x0q1w2e3r4t5y6u7i8o", status: "Active", lastUsed: "yesterday" }
|
|
40
|
-
]
|
|
41
|
-
};
|
|
42
12
|
const EYE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
13
|
+
const SEARCH_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
|
|
43
14
|
function money(n) {
|
|
44
15
|
const abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
45
16
|
return `${n < 0 ? "−" : ""}$${abs}`;
|
|
46
17
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
18
|
+
function fmtBytes(bytes) {
|
|
19
|
+
if (!bytes || bytes < 1024)
|
|
20
|
+
return `${bytes || 0} B`;
|
|
21
|
+
const units = ["KB", "MB", "GB"];
|
|
22
|
+
let v = bytes / 1024;
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
25
|
+
v /= 1024;
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
|
|
49
29
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
function fmtWhen(ms) {
|
|
31
|
+
if (!ms || ms <= 0)
|
|
32
|
+
return "";
|
|
33
|
+
try {
|
|
34
|
+
return new Date(ms).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function fmtDate(iso) {
|
|
41
|
+
const t = Date.parse(iso);
|
|
42
|
+
if (Number.isNaN(t))
|
|
43
|
+
return iso;
|
|
44
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
45
|
+
}
|
|
46
|
+
function walletEventTitle(e) {
|
|
47
|
+
if (e.entity_type === "wallet_funding_event")
|
|
48
|
+
return "Wallet top-up";
|
|
49
|
+
if (e.cost_center_slug === "hyperframes_lambda" || e.cost_center_slug === "remotion_lambda")
|
|
50
|
+
return "Timeline render usage";
|
|
51
|
+
if (e.cost_center_slug === "wallet_funding")
|
|
52
|
+
return "Wallet funding";
|
|
53
|
+
if (e.cost_center_slug) {
|
|
54
|
+
return e.cost_center_slug.split(/[_-]+/g).filter(Boolean)
|
|
55
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
|
|
56
|
+
}
|
|
57
|
+
return "Usage charge";
|
|
58
|
+
}
|
|
59
|
+
export function renderReskinSettings(input) {
|
|
60
|
+
const acct = `<input type="hidden" name="account" value="${rkEscape(input.currentAccountId)}" />`;
|
|
61
|
+
const displayName = input.name ?? "";
|
|
62
|
+
const activeTab = input.activeTab || "profile";
|
|
63
|
+
const flash = input.error
|
|
64
|
+
? `<div class="rk-notice rk-notice-err rk-set-flash">${rkEscape(input.error)}</div>`
|
|
65
|
+
: input.notice
|
|
66
|
+
? `<div class="rk-notice rk-notice-ok rk-set-flash">${rkEscape(input.notice)}</div>`
|
|
67
|
+
: "";
|
|
53
68
|
// ---- Profile ----
|
|
54
|
-
const attachmentCards =
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const attachmentCards = input.attachments.length
|
|
70
|
+
? input.attachments.map((a) => {
|
|
71
|
+
const ext = (a.fileName.split(".").pop() || a.contentType.split("/").pop() || "file").toUpperCase().slice(0, 4);
|
|
72
|
+
return `
|
|
57
73
|
<div class="rk-item">
|
|
58
74
|
<span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape(ext)}</span>
|
|
59
75
|
<div class="rk-item-main">
|
|
60
76
|
<div class="rk-item-title">${rkEscape(a.fileName)}</div>
|
|
61
|
-
<div class="rk-item-sub">${rkEscape(
|
|
77
|
+
<div class="rk-item-sub">${rkEscape(fmtBytes(a.sizeBytes))} · added ${rkEscape(fmtDate(a.createdAt))}</div>
|
|
62
78
|
</div>
|
|
63
|
-
<
|
|
79
|
+
<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(a.viewUrl)}" target="_blank" rel="noreferrer">Open</a>
|
|
80
|
+
<form method="post" action="/settings/attachments/${encodeURIComponent(a.id)}/delete" data-rk-confirm="Delete ${rkEscape(a.fileName)}?" style="display:inline">
|
|
81
|
+
${acct}
|
|
82
|
+
<button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
|
|
83
|
+
</form>
|
|
64
84
|
</div>`;
|
|
65
|
-
|
|
85
|
+
}).join("")
|
|
86
|
+
: `<p class="rk-muted rk-set-empty">No brand assets yet — upload logos, fonts, VO, or footage to reuse across projects.</p>`;
|
|
66
87
|
const profile = `
|
|
67
88
|
<div class="rk-card rk-set-panel-card">
|
|
68
89
|
<div class="rk-card-head">
|
|
@@ -72,60 +93,72 @@ export function renderReskinSettings() {
|
|
|
72
93
|
</div>
|
|
73
94
|
<form class="rk-stack" method="post" action="/settings/profile">
|
|
74
95
|
${acct}
|
|
96
|
+
<input type="hidden" name="groupchat_url" value="${rkEscape(input.groupchatUrl ?? "")}" />
|
|
75
97
|
<div class="rk-field">
|
|
76
98
|
<label class="rk-label" for="rk-name">Display name</label>
|
|
77
|
-
<input class="rk-input" id="rk-name" type="text" name="display_name" value="${rkEscape(
|
|
99
|
+
<input class="rk-input" id="rk-name" type="text" name="display_name" value="${rkEscape(displayName)}" />
|
|
78
100
|
</div>
|
|
79
101
|
<div class="rk-field">
|
|
80
102
|
<label class="rk-label" for="rk-about">Product description</label>
|
|
81
|
-
<textarea class="rk-textarea" id="rk-about" name="about" placeholder="What it is, who it's for, the promise…">${rkEscape(
|
|
103
|
+
<textarea class="rk-textarea" id="rk-about" name="about" placeholder="What it is, who it's for, the promise…">${rkEscape(input.about ?? "")}</textarea>
|
|
82
104
|
<span class="rk-hint">Swipe mode reads this to auto-recut templates into ads for you.</span>
|
|
83
105
|
</div>
|
|
84
|
-
<div class="rk-field">
|
|
85
|
-
<label class="rk-label">Brand assets</label>
|
|
86
|
-
<div class="rk-set-drop">
|
|
87
|
-
<div><strong>Drop files</strong> or <span class="rk-set-link">browse</span> — logos, fonts, VO, footage.</div>
|
|
88
|
-
</div>
|
|
89
|
-
<div class="rk-list rk-set-attach">${attachmentCards}</div>
|
|
90
|
-
</div>
|
|
91
106
|
<div class="rk-row">
|
|
92
107
|
<button class="rk-btn rk-btn-ink" type="submit">Save changes</button>
|
|
93
|
-
<span class="rk-hint">Signed in as ${rkEscape(
|
|
108
|
+
<span class="rk-hint">Signed in as ${rkEscape(input.email)}</span>
|
|
94
109
|
</div>
|
|
95
110
|
</form>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="rk-card rk-set-panel-card rk-set-myfiles">
|
|
113
|
+
<div class="rk-card-head">
|
|
114
|
+
<span class="rk-eyebrow">My Files</span>
|
|
115
|
+
<h2>Your files</h2>
|
|
116
|
+
<p class="rk-muted">Your private library — logos, fonts, VO, footage, and notes. Open <b>My Files</b> or <b>Temp</b> below and browse into folders.</p>
|
|
117
|
+
</div>
|
|
118
|
+
<form method="post" action="/settings/attachments" enctype="multipart/form-data" class="rk-stack rk-set-upload" data-rk-upload>
|
|
119
|
+
${acct}
|
|
120
|
+
<label class="rk-set-drop" for="rk-files">
|
|
121
|
+
<div><strong>Choose files</strong> to upload — logos, fonts, VO, footage.</div>
|
|
122
|
+
<input class="rk-set-file" id="rk-files" type="file" name="files" multiple />
|
|
123
|
+
</label>
|
|
124
|
+
<div class="rk-row"><button class="rk-btn rk-btn-gold rk-btn-sm" type="submit" data-rk-upload-btn>Upload files</button></div>
|
|
125
|
+
</form>
|
|
126
|
+
<div class="rk-set-explorer" data-rk-fx>
|
|
127
|
+
<div class="rk-aichat-crumbs rk-set-fx-crumbs" data-rk-fx-crumbs aria-label="Current folder"></div>
|
|
128
|
+
<div class="rk-set-fx-body" data-rk-fx-body></div>
|
|
129
|
+
</div>
|
|
96
130
|
</div>`;
|
|
97
131
|
// ---- Wallet ----
|
|
98
|
-
const
|
|
132
|
+
const walletBalance = input.wallet.unavailableReason
|
|
133
|
+
? `<div class="rk-notice rk-notice-err">${rkEscape(input.wallet.unavailableReason)}</div>`
|
|
134
|
+
: `<div class="rk-stat-num rk-set-balance-num">${money(input.wallet.balanceUsd)}</div>
|
|
135
|
+
<p class="rk-muted">Available balance${input.wallet.updatedAtMs > 0 ? ` · updated ${rkEscape(fmtWhen(input.wallet.updatedAtMs))}` : ""}</p>`;
|
|
136
|
+
const historyRows = input.walletEvents.unavailableReason
|
|
137
|
+
? `<div class="rk-notice rk-notice-err">${rkEscape(input.walletEvents.unavailableReason)}</div>`
|
|
138
|
+
: input.walletEvents.events.length
|
|
139
|
+
? input.walletEvents.events.map((e) => {
|
|
140
|
+
const credit = e.entity_type === "wallet_funding_event" || e.amount_usd > 0;
|
|
141
|
+
const sub = [e.cost_center_slug || e.entity_type, e.job_id, e.tracer].filter(Boolean).join(" · ");
|
|
142
|
+
return `
|
|
99
143
|
<div class="rk-item">
|
|
100
|
-
<span class="rk-tile rk-tile-sm rk-tile-txt ${
|
|
144
|
+
<span class="rk-tile rk-tile-sm rk-tile-txt ${credit ? "rk-tile-green" : "rk-tile-ink"}">${credit ? "+" : "−"}</span>
|
|
101
145
|
<div class="rk-item-main">
|
|
102
|
-
<div class="rk-item-title">${rkEscape(e
|
|
103
|
-
<div class="rk-item-sub">${rkEscape(
|
|
146
|
+
<div class="rk-item-title">${rkEscape(walletEventTitle(e))}</div>
|
|
147
|
+
<div class="rk-item-sub">${rkEscape(sub)} · ${rkEscape(fmtWhen(e.occurred_at_ms))}</div>
|
|
104
148
|
</div>
|
|
105
|
-
<div class="rk-set-amount ${
|
|
106
|
-
</div
|
|
149
|
+
<div class="rk-set-amount ${credit ? "is-credit" : ""}">${money(e.amount_usd)}</div>
|
|
150
|
+
</div>`;
|
|
151
|
+
}).join("")
|
|
152
|
+
: `<p class="rk-muted rk-set-empty">No wallet activity yet.</p>`;
|
|
107
153
|
const wallet = `
|
|
108
154
|
<div class="rk-stack rk-set-panel">
|
|
109
|
-
<div class="rk-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
<div class="rk-row rk-wrap rk-set-balance-cta">
|
|
115
|
-
<a class="rk-btn rk-btn-gold" href="#">Top up wallet <span class="rk-arrow">→</span></a>
|
|
116
|
-
<span class="rk-pill rk-pill-green">Auto-scales to zero</span>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="rk-stack">
|
|
120
|
-
<div class="rk-card rk-card-pad-sm rk-stat">
|
|
121
|
-
<div class="rk-stat-label">Funds added</div>
|
|
122
|
-
<div class="rk-stat-num">${money(s.wallet.totalFundsAddedUsd)}</div>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="rk-card rk-card-pad-sm rk-stat">
|
|
125
|
-
<div class="rk-stat-label">Total usage</div>
|
|
126
|
-
<div class="rk-stat-num">${money(s.wallet.totalChargeUsd)}</div>
|
|
127
|
-
</div>
|
|
155
|
+
<div class="rk-card rk-set-balance">
|
|
156
|
+
<span class="rk-eyebrow">Wallet</span>
|
|
157
|
+
${walletBalance}
|
|
158
|
+
<div class="rk-row rk-wrap rk-set-balance-cta">
|
|
159
|
+
<button class="rk-btn rk-btn-gold" type="button" data-rk-topup>Top up wallet <span class="rk-arrow">→</span></button>
|
|
128
160
|
</div>
|
|
161
|
+
<div class="rk-hint rk-set-topup-status" data-rk-topup-status hidden></div>
|
|
129
162
|
</div>
|
|
130
163
|
<div class="rk-card">
|
|
131
164
|
<div class="rk-card-head">
|
|
@@ -134,76 +167,134 @@ export function renderReskinSettings() {
|
|
|
134
167
|
<p class="rk-muted">Every render and top-up, itemized. You're only charged for compute — BYO AI keys are never billed.</p>
|
|
135
168
|
</div>
|
|
136
169
|
<div class="rk-list">${historyRows}</div>
|
|
137
|
-
<div class="rk-row rk-set-ledger-foot"><button class="rk-btn rk-btn-ghost rk-btn-sm" type="button">Load more</button></div>
|
|
138
170
|
</div>
|
|
139
171
|
</div>`;
|
|
140
172
|
// ---- Channels ----
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
// Mirrors the live /settings#channels ordering: topline heading + Connect
|
|
174
|
+
// button + "…" menu (Add Email Channel), then a channel search, then the
|
|
175
|
+
// channel list. The FlockPoster key + email channel forms live in dialogs.
|
|
176
|
+
const emailCards = input.emailChannels.map((ch) => {
|
|
177
|
+
const status = ch.status || "Pending";
|
|
178
|
+
const statusPill = /connect|active|verified/i.test(status) ? "rk-pill-green" : "rk-pill-gold";
|
|
179
|
+
const hay = [ch.title, ch.platform || "email", ch.handle].filter(Boolean).join(" ").toLowerCase();
|
|
180
|
+
return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
|
|
181
|
+
<span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape((ch.platform || "E").charAt(0).toUpperCase())}</span>
|
|
147
182
|
<div class="rk-item-main">
|
|
148
|
-
<div class="rk-item-title">${rkEscape(
|
|
149
|
-
<div class="rk-item-sub">${rkEscape(
|
|
183
|
+
<div class="rk-item-title">${rkEscape(ch.title)}</div>
|
|
184
|
+
<div class="rk-item-sub">${rkEscape(ch.platform || "Email")} · ${rkEscape(ch.handle || "")}</div>
|
|
150
185
|
</div>
|
|
151
|
-
<span class="rk-pill ${statusPill}">${rkEscape(
|
|
152
|
-
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="
|
|
186
|
+
<span class="rk-pill ${statusPill}">${rkEscape(status)}</span>
|
|
187
|
+
<form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/resend" style="display:inline">${acct}<button class="rk-btn rk-btn-ghost rk-btn-sm" type="submit">Resend</button></form>
|
|
188
|
+
<form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/delete" data-rk-confirm="Remove this channel?" style="display:inline">${acct}<button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button></form>
|
|
153
189
|
</div>`;
|
|
154
190
|
}).join("");
|
|
191
|
+
const flockCards = input.flockposterChannels.map((ch) => {
|
|
192
|
+
const status = ch.status || "Connected";
|
|
193
|
+
const tint = /tiktok/i.test(ch.platform || "") ? "rk-tile-ink" : /insta/i.test(ch.platform || "") ? "rk-tile-coral" : "rk-tile-sky";
|
|
194
|
+
const hay = [ch.title, ch.platform, ch.handle].filter(Boolean).join(" ").toLowerCase();
|
|
195
|
+
return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
|
|
196
|
+
<span class="rk-tile rk-tile-txt ${tint}">${rkEscape((ch.platform || "F").charAt(0).toUpperCase())}</span>
|
|
197
|
+
<div class="rk-item-main">
|
|
198
|
+
<div class="rk-item-title">${rkEscape(ch.title)}</div>
|
|
199
|
+
<div class="rk-item-sub">${rkEscape(ch.platform || "")} · ${rkEscape(ch.handle || "")}</div>
|
|
200
|
+
</div>
|
|
201
|
+
<span class="rk-pill rk-pill-green">${rkEscape(status)}</span>
|
|
202
|
+
</div>`;
|
|
203
|
+
}).join("");
|
|
204
|
+
const channelCards = flockCards + emailCards;
|
|
205
|
+
const channelCount = input.emailChannels.length + input.flockposterChannels.length;
|
|
206
|
+
const hasFlockKey = Boolean(input.flockposterApiKey);
|
|
155
207
|
const channels = `
|
|
156
208
|
<div class="rk-stack rk-set-panel">
|
|
209
|
+
${input.flockposterChannelsError ? `<div class="rk-notice rk-notice-err">${rkEscape(input.flockposterChannelsError)}</div>` : ""}
|
|
157
210
|
<div class="rk-card">
|
|
158
|
-
<div class="rk-spread rk-
|
|
159
|
-
<div>
|
|
211
|
+
<div class="rk-spread rk-set-channels-topline">
|
|
212
|
+
<div class="rk-card-head rk-set-channels-headcopy">
|
|
160
213
|
<span class="rk-eyebrow">Channels</span>
|
|
161
|
-
<h2>${
|
|
162
|
-
<p class="rk-muted">Where approved cuts get published. Connect via FlockPoster or add an email
|
|
214
|
+
<h2>${channelCount} connected channel${channelCount === 1 ? "" : "s"}</h2>
|
|
215
|
+
<p class="rk-muted">Where approved cuts get published. Connect via FlockPoster, or add an email destination.</p>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="rk-row rk-set-channels-actions">
|
|
218
|
+
<button class="rk-btn rk-btn-gold rk-btn-sm" type="button" data-rk-open-flock>Connect</button>
|
|
219
|
+
<div class="rk-set-menu-wrap">
|
|
220
|
+
<button class="rk-btn rk-btn-ghost rk-btn-sm rk-set-menu-btn" type="button" data-rk-channel-menu aria-expanded="false" aria-label="More channel actions">…</button>
|
|
221
|
+
<div class="rk-set-menu" data-rk-channel-menu-pop hidden>
|
|
222
|
+
<button type="button" data-rk-open-email>Add Email Channel</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
163
225
|
</div>
|
|
164
|
-
<a class="rk-btn rk-btn-ink" href="#">Connect channel <span class="rk-arrow">→</span></a>
|
|
165
226
|
</div>
|
|
166
|
-
<
|
|
227
|
+
<label class="rk-set-chsearch">
|
|
228
|
+
<span class="rk-set-chsearch-ic" aria-hidden="true">${SEARCH_ICON}</span>
|
|
229
|
+
<input class="rk-input" data-rk-channel-search type="search"
|
|
230
|
+
placeholder="${channelCount ? "Search channels by name, handle, or platform" : "Add an email channel or connect FlockPoster to search channels"}"
|
|
231
|
+
autocomplete="off" spellcheck="false" aria-label="Search channels"${channelCount ? "" : " disabled"} />
|
|
232
|
+
</label>
|
|
233
|
+
<div class="rk-list" data-rk-channel-list>${channelCards || `<p class="rk-muted rk-set-empty">No channels connected yet.</p>`}</div>
|
|
234
|
+
<p class="rk-muted rk-set-empty" data-rk-channel-noresults hidden>No channels match your search.</p>
|
|
167
235
|
</div>
|
|
168
|
-
|
|
236
|
+
</div>
|
|
237
|
+
<dialog class="rk-set-dialog" data-rk-flock-dialog${input.openFlockPosterDialog ? ` data-rk-autoopen="1"` : ""}>
|
|
238
|
+
<form class="rk-set-dialog-form" method="post" action="/settings/channels/flockposter">
|
|
239
|
+
${acct}
|
|
169
240
|
<div class="rk-card-head">
|
|
170
|
-
<span class="rk-eyebrow">FlockPoster</span>
|
|
241
|
+
<span class="rk-eyebrow">FlockPoster settings</span>
|
|
171
242
|
<h3>FlockPoster API key</h3>
|
|
172
|
-
<p class="rk-muted"
|
|
243
|
+
<p class="rk-muted">${hasFlockKey ? "Update the stored key and retest the connection before saving." : "Paste a FlockPoster API key to connect this workspace and load its channels."}</p>
|
|
173
244
|
</div>
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
<div class="rk-
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
<input class="rk-input" id="rk-fp" type="password" name="api_key" placeholder="fp_live_…" />
|
|
180
|
-
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-fp" aria-label="Show API key">${EYE}</button>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
<div class="rk-row">
|
|
184
|
-
<button class="rk-btn rk-btn-gold" type="submit">Save & test connection</button>
|
|
245
|
+
<div class="rk-field">
|
|
246
|
+
<label class="rk-label" for="rk-fp">API key</label>
|
|
247
|
+
<div class="rk-input-group">
|
|
248
|
+
<input class="rk-input" id="rk-fp" type="password" name="flockposter_api_key" placeholder="${hasFlockKey ? "•••••••• (stored) — paste to replace" : "fp_live_…"}" autocomplete="off" />
|
|
249
|
+
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-fp" aria-label="Show API key">${EYE}</button>
|
|
185
250
|
</div>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
251
|
+
<span class="rk-hint">${hasFlockKey ? "The key stays hidden by default — changing it triggers a connection test on save." : "We test the key before storing it. If the connection succeeds, this tab loads your channels."}</span>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="rk-set-dialog-actions">
|
|
254
|
+
<a class="rk-btn rk-btn-ghost rk-btn-sm" href="https://flockposter.com" target="_blank" rel="noreferrer">Visit App</a>
|
|
255
|
+
<button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
|
|
256
|
+
<button type="submit" class="rk-btn rk-btn-gold">${hasFlockKey ? "Save" : "Connect"} & test</button>
|
|
257
|
+
</div>
|
|
258
|
+
</form>
|
|
259
|
+
</dialog>
|
|
260
|
+
<dialog class="rk-set-dialog" data-rk-email-dialog>
|
|
261
|
+
<form class="rk-set-dialog-form" method="post" action="/settings/channels/email">
|
|
262
|
+
${acct}
|
|
263
|
+
<div class="rk-card-head">
|
|
264
|
+
<span class="rk-eyebrow">Email channel</span>
|
|
265
|
+
<h3>Add Email Channel</h3>
|
|
266
|
+
<p class="rk-muted">We'll send a consent confirmation before this email can be used for scheduled delivery.</p>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="rk-field"><label class="rk-label" for="rk-em-title">Title</label><input class="rk-input" id="rk-em-title" name="title" required maxlength="160" placeholder="Client newsletter" /></div>
|
|
269
|
+
<div class="rk-field"><label class="rk-label" for="rk-em-addr">Email</label><input class="rk-input" id="rk-em-addr" name="email" type="email" required maxlength="320" placeholder="name@example.com" /></div>
|
|
270
|
+
<div class="rk-field"><label class="rk-label" for="rk-em-note">Note</label><textarea class="rk-textarea rk-set-email-note" id="rk-em-note" name="note" maxlength="2000" placeholder="Internal notes for team"></textarea></div>
|
|
271
|
+
<div class="rk-set-dialog-actions">
|
|
272
|
+
<button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
|
|
273
|
+
<button type="submit" class="rk-btn rk-btn-ink">Send confirmation</button>
|
|
274
|
+
</div>
|
|
275
|
+
</form>
|
|
276
|
+
</dialog>`;
|
|
189
277
|
// ---- Developer ----
|
|
190
|
-
const keyRows =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
278
|
+
const keyRows = input.providerKeys.length
|
|
279
|
+
? input.providerKeys.map((k, i) => {
|
|
280
|
+
const id = `rk-pk-${i}`;
|
|
281
|
+
const tint = k.provider === "openai" ? "rk-tile-green" : k.provider === "gemini" ? "rk-tile-sky" : "rk-tile-violet";
|
|
282
|
+
const status = k.status || "active";
|
|
283
|
+
const lastUsed = k.last_used_at ? `used ${rkEscape(fmtDate(k.last_used_at))}` : "never used";
|
|
284
|
+
return `<div class="rk-item">
|
|
285
|
+
<span class="rk-tile rk-tile-txt ${tint}">${rkEscape((k.provider || "?").charAt(0).toUpperCase())}</span>
|
|
196
286
|
<div class="rk-item-main">
|
|
197
|
-
<div class="rk-item-title">${rkEscape(k.label)} <span class="rk-pill rk-pill-green">${rkEscape(
|
|
198
|
-
<div class="rk-item-sub"><span class="rk-mono" id="${id}" data-rk-secret="${rkEscape(k.secret)}">${"•".repeat(18)}</span> · ${rkEscape(k.provider)} ·
|
|
287
|
+
<div class="rk-item-title">${rkEscape(k.label || `${k.provider} key`)} <span class="rk-pill rk-pill-green">${rkEscape(status)}</span></div>
|
|
288
|
+
<div class="rk-item-sub"><span class="rk-mono" id="${id}" data-rk-secret="${rkEscape(k.secret)}">${"•".repeat(18)}</span> · ${rkEscape(k.provider)} · ${lastUsed}</div>
|
|
199
289
|
</div>
|
|
200
290
|
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal-text="${id}" aria-label="Show key">${EYE}</button>
|
|
201
|
-
<form method="post" action="/settings/provider-keys/delete" style="display:inline">
|
|
202
|
-
${acct}
|
|
291
|
+
<form method="post" action="/settings/provider-keys/${encodeURIComponent(k.id)}/delete" data-rk-confirm="Remove this AI key?" style="display:inline">
|
|
292
|
+
${acct}
|
|
203
293
|
<button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
|
|
204
294
|
</form>
|
|
205
295
|
</div>`;
|
|
206
|
-
|
|
296
|
+
}).join("")
|
|
297
|
+
: `<p class="rk-muted rk-set-empty">No AI keys yet — add one to enable clip tagging, recaption, and Swipe.</p>`;
|
|
207
298
|
const developer = `
|
|
208
299
|
<div class="rk-stack rk-set-panel">
|
|
209
300
|
<div class="rk-card">
|
|
@@ -214,19 +305,21 @@ export function renderReskinSettings() {
|
|
|
214
305
|
</div>
|
|
215
306
|
<div class="rk-field">
|
|
216
307
|
<div class="rk-input-group">
|
|
217
|
-
<input class="rk-input rk-mono" id="rk-vfkey" type="password" value="${rkEscape(
|
|
308
|
+
<input class="rk-input rk-mono" id="rk-vfkey" type="password" value="${rkEscape(input.vidfarmApiKey)}" readonly />
|
|
218
309
|
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-vfkey" aria-label="Show key">${EYE}</button>
|
|
219
310
|
<button class="rk-btn rk-btn-ink rk-btn-sm" type="button" data-rk-copy="rk-vfkey">Copy</button>
|
|
220
311
|
</div>
|
|
221
312
|
</div>
|
|
313
|
+
<div class="rk-row rk-wrap rk-set-skillrow">
|
|
314
|
+
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-skill-copy data-rk-skill-content="${rkEscape(input.directorSkill || "")}">Copy Director SKILL.md</button>
|
|
315
|
+
<span class="rk-hint">The full REST + CLI playbook for driving vidfarm from an agent — one click, paste into your agent.</span>
|
|
316
|
+
</div>
|
|
222
317
|
</div>
|
|
223
318
|
<div class="rk-card">
|
|
224
|
-
<div class="rk-
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<p class="rk-muted">Stored per workspace. Used for clip tagging, recaption, and Swipe — never billed by vidfarm.</p>
|
|
229
|
-
</div>
|
|
319
|
+
<div class="rk-card-head">
|
|
320
|
+
<span class="rk-eyebrow">AI keys</span>
|
|
321
|
+
<h2>Bring your own model keys</h2>
|
|
322
|
+
<p class="rk-muted">Stored per workspace. Used for clip tagging, recaption, and Swipe — never billed by vidfarm.</p>
|
|
230
323
|
</div>
|
|
231
324
|
<div class="rk-list rk-set-keys">${keyRows}</div>
|
|
232
325
|
<form class="rk-set-addkey" method="post" action="/settings/provider-keys">
|
|
@@ -235,7 +328,7 @@ export function renderReskinSettings() {
|
|
|
235
328
|
<div class="rk-field">
|
|
236
329
|
<label class="rk-label" for="rk-prov">Provider</label>
|
|
237
330
|
<select class="rk-select" id="rk-prov" name="provider">
|
|
238
|
-
<option>OpenAI</option><option>Gemini</option><option>OpenRouter</option><option>Anthropic</option>
|
|
331
|
+
<option value="openai">OpenAI</option><option value="gemini">Gemini</option><option value="openrouter">OpenRouter</option><option value="anthropic">Anthropic</option>
|
|
239
332
|
</select>
|
|
240
333
|
</div>
|
|
241
334
|
<div class="rk-field">
|
|
@@ -250,101 +343,387 @@ export function renderReskinSettings() {
|
|
|
250
343
|
<button class="rk-btn rk-btn-gold" type="submit">Add AI key</button>
|
|
251
344
|
</form>
|
|
252
345
|
</div>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<
|
|
346
|
+
</div>`;
|
|
347
|
+
// ---- Log out ----
|
|
348
|
+
const logout = `
|
|
349
|
+
<div class="rk-card rk-set-panel-card rk-set-logout">
|
|
350
|
+
<div class="rk-card-head">
|
|
351
|
+
<span class="rk-eyebrow">Session</span>
|
|
352
|
+
<h2>Log out</h2>
|
|
353
|
+
<p class="rk-muted">End this browser session. You'll need to sign back in as <b>${rkEscape(input.email)}</b> to return to your studio.</p>
|
|
259
354
|
</div>
|
|
355
|
+
<form method="post" action="/logout" class="rk-row rk-wrap rk-set-logout-actions">
|
|
356
|
+
${acct}
|
|
357
|
+
<button class="rk-btn rk-btn-danger" type="submit">Log out</button>
|
|
358
|
+
<button class="rk-btn rk-btn-ghost" type="button" data-rk-tab="profile">Cancel</button>
|
|
359
|
+
</form>
|
|
260
360
|
</div>`;
|
|
361
|
+
const tab = (slug, label, danger = false) => `<button type="button" class="rk-side-item${danger ? " rk-side-item-danger" : ""}${slug === activeTab ? " is-active" : ""}" data-rk-tab="${slug}">${label}</button>`;
|
|
261
362
|
const body = `
|
|
262
363
|
<main class="rk-container rk-page">
|
|
263
364
|
<div class="rk-page-head">
|
|
264
|
-
<span class="rk-badge">Account settings</span>
|
|
265
365
|
<h1 class="rk-h1">Settings</h1>
|
|
266
366
|
<p class="rk-sub">Manage your profile, wallet, publishing channels, and developer access.</p>
|
|
267
367
|
</div>
|
|
368
|
+
${flash}
|
|
268
369
|
<div class="rk-shell">
|
|
269
370
|
<nav class="rk-side" aria-label="Settings sections">
|
|
270
|
-
${
|
|
271
|
-
${
|
|
272
|
-
${
|
|
273
|
-
${
|
|
371
|
+
${tab("profile", "Profile")}
|
|
372
|
+
${tab("wallet", "Wallet")}
|
|
373
|
+
${tab("channels", "Channels")}
|
|
374
|
+
${tab("developer", "Developer")}
|
|
375
|
+
<div class="rk-side-sep"></div>
|
|
376
|
+
<a class="rk-side-item" href="https://billing.stripe.com/p/login/fZecQubkxcwOgDuaEE" target="_blank" rel="noopener noreferrer">Subscription</a>
|
|
377
|
+
${tab("logout", "Log out", true)}
|
|
274
378
|
</nav>
|
|
275
379
|
<div class="rk-set-panels">
|
|
276
|
-
<section class="rk-reveal" data-rk-panel="profile">${profile}</section>
|
|
277
|
-
<section class="rk-reveal" data-rk-panel="wallet" hidden>${wallet}</section>
|
|
278
|
-
<section class="rk-reveal" data-rk-panel="channels" hidden>${channels}</section>
|
|
279
|
-
<section class="rk-reveal" data-rk-panel="developer" hidden>${developer}</section>
|
|
380
|
+
<section class="rk-reveal" data-rk-panel="profile"${activeTab === "profile" ? "" : " hidden"}>${profile}</section>
|
|
381
|
+
<section class="rk-reveal" data-rk-panel="wallet"${activeTab === "wallet" ? "" : " hidden"}>${wallet}</section>
|
|
382
|
+
<section class="rk-reveal" data-rk-panel="channels"${activeTab === "channels" ? "" : " hidden"}>${channels}</section>
|
|
383
|
+
<section class="rk-reveal" data-rk-panel="developer"${activeTab === "developer" ? "" : " hidden"}>${developer}</section>
|
|
384
|
+
<section class="rk-reveal" data-rk-panel="logout" hidden>${logout}</section>
|
|
280
385
|
</div>
|
|
281
386
|
</div>
|
|
282
387
|
</main>`;
|
|
283
388
|
const pageCss = `
|
|
284
389
|
.rk-tile-txt{font-family:var(--rk-font-display);font-weight:700;font-size:14px;letter-spacing:.01em;line-height:1}
|
|
285
390
|
.rk-tile-txt.rk-tile-sm{font-size:15px}
|
|
391
|
+
.rk-set-flash{max-width:760px;margin-bottom:20px}
|
|
286
392
|
.rk-set-panel-card,.rk-set-panel{max-width:760px}
|
|
287
|
-
.rk-set-
|
|
288
|
-
.rk-set-
|
|
289
|
-
.rk-set-
|
|
290
|
-
.rk-set-
|
|
393
|
+
.rk-set-panel-card + .rk-set-panel-card{margin-top:20px}
|
|
394
|
+
.rk-set-empty{padding:8px 2px}
|
|
395
|
+
.rk-set-drop{display:flex;align-items:center;gap:14px;padding:20px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);font-size:14px;color:var(--rk-text-muted);cursor:pointer}
|
|
396
|
+
.rk-set-drop:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
397
|
+
.rk-set-file{margin-left:auto;font-size:13px;max-width:220px}
|
|
398
|
+
.rk-set-attach{margin-top:4px}
|
|
291
399
|
.rk-set-balance{display:grid;gap:10px;align-content:start}
|
|
292
400
|
.rk-set-balance-num{font-size:var(--rk-text-5xl)}
|
|
293
401
|
.rk-set-balance-cta{margin-top:6px}
|
|
402
|
+
.rk-set-topup-status{margin-top:4px}
|
|
294
403
|
.rk-set-amount{font-family:var(--rk-font-display);font-weight:700;font-size:15px;color:var(--rk-n-600);white-space:nowrap}
|
|
295
404
|
.rk-set-amount.is-credit{color:#16a34a}
|
|
296
|
-
.rk-set-ledger-foot{margin-top:16px;justify-content:center}
|
|
297
405
|
.rk-set-keys{margin-bottom:20px}
|
|
298
406
|
.rk-set-addkey{border-top:1px solid var(--rk-border);padding-top:20px;display:grid;gap:16px}
|
|
299
407
|
.rk-set-addkey-grid{gap:14px}
|
|
300
|
-
.rk-set-
|
|
301
|
-
|
|
408
|
+
.rk-set-skillrow{margin-top:14px;padding-top:14px;border-top:1px solid var(--rk-border)}
|
|
409
|
+
.rk-set-skillrow .rk-hint{flex:1;min-width:200px}
|
|
410
|
+
|
|
411
|
+
/* log-out tab — danger-styled side item + a hairline separator above it */
|
|
412
|
+
.rk-side-sep{height:1px;background:var(--rk-border);margin:8px 6px}
|
|
413
|
+
@media(max-width:860px){.rk-side-sep{display:none}}
|
|
414
|
+
.rk-side-item-danger{color:var(--rk-red)}
|
|
415
|
+
.rk-side-item-danger:hover{background:var(--rk-red-tint);color:#b91c1c}
|
|
416
|
+
.rk-side-item-danger.is-active{background:var(--rk-red-tint);color:#b91c1c}
|
|
417
|
+
.rk-set-logout{max-width:760px}
|
|
418
|
+
.rk-set-logout-actions{margin-top:2px}
|
|
419
|
+
|
|
420
|
+
/* channels topline: heading left, Connect + "…" menu right (mirrors live) */
|
|
421
|
+
.rk-set-channels-topline{align-items:flex-start;gap:16px}
|
|
422
|
+
.rk-set-channels-headcopy{margin-bottom:14px;flex:1;min-width:0}
|
|
423
|
+
.rk-set-channels-actions{flex:none;gap:8px}
|
|
424
|
+
.rk-set-menu-wrap{position:relative}
|
|
425
|
+
.rk-set-menu-btn{width:34px;padding:7px 0;font-weight:800;letter-spacing:.04em}
|
|
426
|
+
.rk-set-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:20;min-width:190px;background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-lg);padding:6px;display:grid}
|
|
427
|
+
.rk-set-menu[hidden]{display:none}
|
|
428
|
+
.rk-set-menu button{display:block;width:100%;text-align:left;padding:9px 12px;border:0;background:transparent;border-radius:var(--rk-r-md);font-family:var(--rk-font-body);font-size:13.5px;font-weight:600;color:var(--rk-n-700);cursor:pointer}
|
|
429
|
+
.rk-set-menu button:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
430
|
+
.rk-set-chsearch{position:relative;display:block;margin-bottom:14px}
|
|
431
|
+
.rk-set-chsearch-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
432
|
+
.rk-set-chsearch .rk-input{padding-left:38px;border-radius:var(--rk-r-full)}
|
|
433
|
+
.rk-set-chsearch .rk-input:disabled{background:var(--rk-n-100);cursor:not-allowed}
|
|
434
|
+
[data-rk-channel-hay][hidden]{display:none}
|
|
435
|
+
|
|
436
|
+
/* shared dialog chrome (FlockPoster connect + email channel) */
|
|
437
|
+
.rk-set-dialog{width:min(30rem,calc(100vw - 2rem));padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl)}
|
|
438
|
+
.rk-set-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
|
|
439
|
+
.rk-set-dialog-form{display:grid;gap:16px;margin:0;padding:26px}
|
|
440
|
+
.rk-set-dialog-actions{display:flex;justify-content:flex-end;align-items:center;gap:10px;margin-top:2px}
|
|
441
|
+
.rk-set-dialog-actions .rk-btn:first-child{margin-right:auto}
|
|
442
|
+
.rk-set-email-note{min-height:76px}
|
|
443
|
+
|
|
444
|
+
/* My Files — inline, always-open file explorer (reuses the chat panel's
|
|
445
|
+
rk-aichat-f* directory-nav visuals) with My Files + Temp as root folders */
|
|
446
|
+
.rk-set-myfiles{display:grid;gap:16px}
|
|
447
|
+
.rk-set-upload{flex:none}
|
|
448
|
+
.rk-set-explorer{border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);overflow:hidden}
|
|
449
|
+
.rk-set-fx-crumbs{padding:12px 14px 8px}
|
|
450
|
+
.rk-set-fx-body{max-height:360px;min-height:120px;overflow-y:auto;padding:2px 10px 12px;display:grid;gap:2px;align-content:start}
|
|
451
|
+
/* a revealed API key lives in .rk-item-sub (nowrap+ellipsis), so it truncates to
|
|
452
|
+
a few chars + "…". When shown (data-rk-shown=1), let that row wrap in full. */
|
|
453
|
+
.rk-set-panel .rk-item-sub:has([data-rk-shown="1"]){white-space:normal;overflow:visible;overflow-wrap:anywhere}
|
|
454
|
+
/* phone portrait: the channel / key rows pack tile + status pill + Resend +
|
|
455
|
+
Remove into one non-wrapping flex row that overflows the padded card and
|
|
456
|
+
forces horizontal page scroll. Let the actions wrap under the title. */
|
|
457
|
+
@media(max-width:620px){
|
|
458
|
+
.rk-set-panel .rk-item{flex-wrap:wrap}
|
|
459
|
+
.rk-set-panel .rk-item .rk-item-main{flex:1 1 100%;order:-1}
|
|
460
|
+
}
|
|
461
|
+
/* very narrow: let the dialog footer buttons wrap instead of cramping */
|
|
462
|
+
@media(max-width:420px){
|
|
463
|
+
.rk-set-dialog-actions{flex-wrap:wrap;gap:8px}
|
|
464
|
+
.rk-set-dialog-actions .rk-btn:first-child{margin-right:0}
|
|
465
|
+
}
|
|
302
466
|
`;
|
|
303
467
|
const script = `
|
|
304
468
|
(function(){
|
|
305
469
|
var root=document;
|
|
306
|
-
// tab
|
|
470
|
+
// Each panel that has its own canonical URL (/settings/<tab>). "logout" is a
|
|
471
|
+
// client-only confirm panel, so it never rewrites the address bar.
|
|
472
|
+
var URL_TABS={profile:1,wallet:1,channels:1,developer:1};
|
|
473
|
+
function setUrl(tab){
|
|
474
|
+
if(!URL_TABS[tab]||!history.pushState)return;
|
|
475
|
+
try{
|
|
476
|
+
var u=new URL(window.location.href);
|
|
477
|
+
var m=u.pathname.match(/^(\\/u\\/[^/]+)\\/settings/); // preserve account scope
|
|
478
|
+
u.pathname=(m?m[1]:'')+'/settings/'+tab; u.hash='';
|
|
479
|
+
history.pushState(null,'',u.pathname+u.search);
|
|
480
|
+
}catch(e){ history.pushState(null,'','/settings/'+tab); }
|
|
481
|
+
}
|
|
482
|
+
function show(tab,opts){
|
|
483
|
+
root.querySelectorAll('[data-rk-tab]').forEach(function(b){b.classList.toggle('is-active',b.getAttribute('data-rk-tab')===tab)});
|
|
484
|
+
root.querySelectorAll('[data-rk-panel]').forEach(function(p){p.hidden=p.getAttribute('data-rk-panel')!==tab});
|
|
485
|
+
if(!opts||!opts.skipUrl)setUrl(tab);
|
|
486
|
+
}
|
|
307
487
|
root.querySelectorAll('[data-rk-tab]').forEach(function(btn){
|
|
308
|
-
btn.addEventListener('click',function(){
|
|
309
|
-
var tab=btn.getAttribute('data-rk-tab');
|
|
310
|
-
root.querySelectorAll('[data-rk-tab]').forEach(function(b){b.classList.toggle('is-active',b===btn)});
|
|
311
|
-
root.querySelectorAll('[data-rk-panel]').forEach(function(p){p.hidden=p.getAttribute('data-rk-panel')!==tab});
|
|
312
|
-
if(history.replaceState)history.replaceState(null,'','#'+tab);
|
|
313
|
-
});
|
|
488
|
+
btn.addEventListener('click',function(){show(btn.getAttribute('data-rk-tab'));});
|
|
314
489
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
490
|
+
function tabFromLocation(){
|
|
491
|
+
var m=(location.pathname||'').match(/\\/settings\\/([a-z]+)/);
|
|
492
|
+
if(m&&URL_TABS[m[1]])return m[1];
|
|
493
|
+
var hash=(location.hash||'').replace('#','');
|
|
494
|
+
return (hash&&URL_TABS[hash])?hash:null;
|
|
495
|
+
}
|
|
496
|
+
var initial=tabFromLocation();
|
|
497
|
+
if(initial&&root.querySelector('[data-rk-tab="'+initial+'"]'))show(initial,{skipUrl:true});
|
|
498
|
+
// Back/forward between settings tabs — reflect the path without pushing again.
|
|
499
|
+
window.addEventListener('popstate',function(){ show(tabFromLocation()||'profile',{skipUrl:true}); });
|
|
500
|
+
// reveal password inputs
|
|
318
501
|
root.querySelectorAll('[data-rk-reveal]').forEach(function(btn){
|
|
319
|
-
btn.addEventListener('click',function(){
|
|
320
|
-
var el=root.getElementById(btn.getAttribute('data-rk-reveal'));
|
|
321
|
-
if(!el)return; el.type=el.type==='password'?'text':'password';
|
|
322
|
-
});
|
|
502
|
+
btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal'));if(el)el.type=el.type==='password'?'text':'password';});
|
|
323
503
|
});
|
|
324
504
|
// reveal masked secret text
|
|
325
505
|
root.querySelectorAll('[data-rk-reveal-text]').forEach(function(btn){
|
|
326
|
-
btn.addEventListener('click',function(){
|
|
327
|
-
var el=root.getElementById(btn.getAttribute('data-rk-reveal-text'));
|
|
328
|
-
if(!el)return; var real=el.getAttribute('data-rk-secret');
|
|
329
|
-
var shown=el.getAttribute('data-rk-shown')==='1';
|
|
330
|
-
el.textContent=shown?'\\u2022'.repeat(18):real; el.setAttribute('data-rk-shown',shown?'0':'1');
|
|
331
|
-
});
|
|
506
|
+
btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal-text'));if(!el)return;var real=el.getAttribute('data-rk-secret');var shown=el.getAttribute('data-rk-shown')==='1';el.textContent=shown?'\\u2022'.repeat(18):real;el.setAttribute('data-rk-shown',shown?'0':'1');});
|
|
332
507
|
});
|
|
333
508
|
// copy
|
|
334
509
|
root.querySelectorAll('[data-rk-copy]').forEach(function(btn){
|
|
335
|
-
btn.addEventListener('click',function(){
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
510
|
+
btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.value||el.textContent||'').then(function(){var t=btn.textContent;btn.textContent='Copied!';setTimeout(function(){btn.textContent=t;},1200);});});
|
|
511
|
+
});
|
|
512
|
+
// destructive form confirmations
|
|
513
|
+
root.querySelectorAll('form[data-rk-confirm]').forEach(function(f){
|
|
514
|
+
f.addEventListener('submit',function(e){if(!window.confirm(f.getAttribute('data-rk-confirm')||'Are you sure?'))e.preventDefault();});
|
|
515
|
+
});
|
|
516
|
+
// director skill — 1-click copy (content travels in the attribute, like live)
|
|
517
|
+
var skillCopy=root.querySelector('[data-rk-skill-copy]');
|
|
518
|
+
if(skillCopy){
|
|
519
|
+
skillCopy.addEventListener('click',function(){
|
|
520
|
+
var text=skillCopy.getAttribute('data-rk-skill-content')||'';
|
|
521
|
+
if(!navigator.clipboard)return;
|
|
522
|
+
navigator.clipboard.writeText(text).then(function(){
|
|
523
|
+
var t=skillCopy.textContent;skillCopy.textContent='Copied!';
|
|
524
|
+
setTimeout(function(){skillCopy.textContent=t;},1200);
|
|
340
525
|
});
|
|
341
526
|
});
|
|
342
|
-
}
|
|
527
|
+
}
|
|
528
|
+
// My Files — inline, always-open file explorer. Same directory-nav model as
|
|
529
|
+
// the chat pop panel: a single virtual root whose top-level entries are the
|
|
530
|
+
// "My Files" and "Temp" folders; navigate into them natively. Files open in
|
|
531
|
+
// a new tab (view URL); no drawer, no source tabs.
|
|
532
|
+
(function(){
|
|
533
|
+
var fx=root.querySelector('[data-rk-fx]');
|
|
534
|
+
if(!fx) return;
|
|
535
|
+
var crumbsEl=fx.querySelector('[data-rk-fx-crumbs]');
|
|
536
|
+
var bodyEl=fx.querySelector('[data-rk-fx-body]');
|
|
537
|
+
var ROOTS={
|
|
538
|
+
my_files:{url:'/api/v1/user/me/attachments',key:'attachments',label:'My Files'},
|
|
539
|
+
temp:{url:'/api/v1/user/me/temporary-files',key:'files',label:'Temp'},
|
|
540
|
+
raws:{url:'/raws/feed',key:'clips',label:'Raws'}
|
|
541
|
+
};
|
|
542
|
+
var VROOTS=[{key:'my_files',label:'My Files'},{key:'temp',label:'Temp'},{key:'raws',label:'Raws'}];
|
|
543
|
+
var st={root:'',path:{},data:{},status:{}};
|
|
544
|
+
VROOTS.forEach(function(v){ st.path[v.key]=''; st.data[v.key]=null; st.status[v.key]='idle'; });
|
|
545
|
+
|
|
546
|
+
function normFolder(v){ return String(v||'').split('/').map(function(s){return s.trim();}).filter(Boolean).join('/'); }
|
|
547
|
+
function normItem(raw){
|
|
548
|
+
// Handles attachments, temp files, AND raws/clips (which use clip_id +
|
|
549
|
+
// source_filename/description and carry no content_type/folder).
|
|
550
|
+
var id=raw.id||raw.attachment_id||raw.file_id||raw.clip_id||'';
|
|
551
|
+
var name=raw.fileName||raw.file_name||raw.source_filename||raw.title||raw.description||'';
|
|
552
|
+
var viewUrl=raw.viewUrl||raw.view_url||'';
|
|
553
|
+
if(!id||(!name&&!viewUrl)) return null;
|
|
554
|
+
if(!name) name=id;
|
|
555
|
+
var ct=raw.contentType||raw.content_type||'';
|
|
556
|
+
if(!ct&&(raw.clip_id||raw.file_path)) ct='video/mp4';
|
|
557
|
+
return {id:id,name:name,contentType:ct,viewUrl:viewUrl,
|
|
558
|
+
sizeBytes:typeof raw.sizeBytes==='number'?raw.sizeBytes:raw.size_bytes,folderPath:normFolder(raw.folderPath||raw.folder_path||'')};
|
|
559
|
+
}
|
|
560
|
+
function fmtBytes(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB'],v=b,i=0; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
|
|
561
|
+
function immediateFolders(data,current){
|
|
562
|
+
var cur=normFolder(current); var prefix=cur?cur+'/':''; var names={};
|
|
563
|
+
function consider(fp){ var n=normFolder(fp); if(!n||(cur&&n===cur)||n.indexOf(prefix)!==0) return; var child=n.slice(prefix.length).split('/')[0]; if(child) names[child]=prefix+child; }
|
|
564
|
+
(data.folders||[]).forEach(consider);
|
|
565
|
+
(data.items||[]).forEach(function(it){ consider(it.folderPath); });
|
|
566
|
+
return Object.keys(names).sort().map(function(n){ return {name:n,path:names[n]}; });
|
|
567
|
+
}
|
|
568
|
+
function loadRoot(rootKey,force){
|
|
569
|
+
if(!ROOTS[rootKey]) return;
|
|
570
|
+
if(!force&&st.status[rootKey]==='ready'){ render(); return; }
|
|
571
|
+
st.status[rootKey]='loading'; render();
|
|
572
|
+
fetch(ROOTS[rootKey].url,{credentials:'same-origin',headers:{accept:'application/json'}})
|
|
573
|
+
.then(function(r){ if(r.status===401||r.status===403){ st.status[rootKey]='auth'; return null; } if(!r.ok) throw new Error('http '+r.status); return r.json(); })
|
|
574
|
+
.then(function(j){ if(!j){ render(); return; } var raw=j[ROOTS[rootKey].key]||[]; var items=[]; for(var i=0;i<raw.length;i++){ var n=normItem(raw[i]); if(n) items.push(n); } st.data[rootKey]={items:items,folders:j.folders||[]}; st.status[rootKey]='ready'; render(); })
|
|
575
|
+
.catch(function(){ st.status[rootKey]='error'; render(); });
|
|
576
|
+
}
|
|
577
|
+
function stateNode(text,opts){
|
|
578
|
+
var wrap=document.createElement('div'); wrap.className='rk-aichat-fstate';
|
|
579
|
+
var p=document.createElement('div'); p.textContent=text; wrap.appendChild(p);
|
|
580
|
+
if(opts&&opts.href){ var a=document.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
|
|
581
|
+
if(opts&&opts.retry){ var b=document.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadRoot(st.root,true); }); wrap.appendChild(b); }
|
|
582
|
+
return wrap;
|
|
583
|
+
}
|
|
584
|
+
function folderRow(label,onOpen,sub){
|
|
585
|
+
var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-frow';
|
|
586
|
+
var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-folder'; ic.textContent='/';
|
|
587
|
+
var main=document.createElement('span'); main.className='rk-aichat-fmain';
|
|
588
|
+
var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=label; main.appendChild(name);
|
|
589
|
+
if(sub){ var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=sub; main.appendChild(meta); }
|
|
590
|
+
var chev=document.createElement('span'); chev.className='rk-aichat-fadd'; chev.textContent='Open \\u203a';
|
|
591
|
+
b.appendChild(ic); b.appendChild(main); b.appendChild(chev);
|
|
592
|
+
b.addEventListener('click',onOpen);
|
|
593
|
+
return b;
|
|
594
|
+
}
|
|
595
|
+
function vrootRow(v){
|
|
596
|
+
var sub=''; var s=st.status[v.key]; var d=st.data[v.key];
|
|
597
|
+
if(s==='ready'&&d) sub=d.items.length+(d.items.length===1?' file':' files');
|
|
598
|
+
else if(s==='loading') sub='Loading\\u2026'; else if(s==='auth') sub='Sign in to view';
|
|
599
|
+
return folderRow(v.label,function(){ enterRoot(v.key); },sub);
|
|
600
|
+
}
|
|
601
|
+
function fileRow(it){
|
|
602
|
+
var open=it.viewUrl?document.createElement('a'):document.createElement('div');
|
|
603
|
+
open.className='rk-aichat-frow';
|
|
604
|
+
if(it.viewUrl){ open.href=it.viewUrl; open.target='_blank'; open.rel='noopener'; }
|
|
605
|
+
var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-file';
|
|
606
|
+
if(it.viewUrl&&it.contentType.indexOf('image/')===0){ var img=document.createElement('img'); img.src=it.viewUrl; img.alt=''; img.loading='lazy'; ic.appendChild(img); }
|
|
607
|
+
else if(it.viewUrl&&it.contentType.indexOf('video/')===0){ var vid=document.createElement('video'); vid.src=it.viewUrl; vid.muted=true; vid.playsInline=true; ic.appendChild(vid); }
|
|
608
|
+
else { ic.textContent=(it.name.split('.').pop()||it.name.slice(0,1)).slice(0,3).toUpperCase(); }
|
|
609
|
+
var main=document.createElement('span'); main.className='rk-aichat-fmain';
|
|
610
|
+
var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=it.name;
|
|
611
|
+
var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=fmtBytes(it.sizeBytes)||it.contentType||'file';
|
|
612
|
+
main.appendChild(name); main.appendChild(meta);
|
|
613
|
+
var add=document.createElement('span'); add.className='rk-aichat-fadd'; add.textContent=it.viewUrl?'Open \\u2197':'';
|
|
614
|
+
open.appendChild(ic); open.appendChild(main); open.appendChild(add);
|
|
615
|
+
return open;
|
|
616
|
+
}
|
|
617
|
+
function crumb(label,current,onClick){
|
|
618
|
+
var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-crumb'+(current?' is-current':''); b.textContent=label; b.addEventListener('click',onClick); return b;
|
|
619
|
+
}
|
|
620
|
+
function crumbSep(){ var s=document.createElement('span'); s.className='rk-aichat-crumb-sep'; s.textContent='/'; return s; }
|
|
621
|
+
function renderCrumbs(){
|
|
622
|
+
crumbsEl.innerHTML='';
|
|
623
|
+
var atVirtual=!st.root;
|
|
624
|
+
crumbsEl.appendChild(crumb('Files',atVirtual,goVirtualRoot));
|
|
625
|
+
if(atVirtual) return;
|
|
626
|
+
if(st.status[st.root]!=='ready') return;
|
|
627
|
+
var cur=normFolder(st.path[st.root]);
|
|
628
|
+
crumbsEl.appendChild(crumbSep());
|
|
629
|
+
crumbsEl.appendChild(crumb(ROOTS[st.root].label,!cur,function(){ goPath(''); }));
|
|
630
|
+
var parts=cur.split('/').filter(Boolean); var acc='';
|
|
631
|
+
parts.forEach(function(part,idx){ crumbsEl.appendChild(crumbSep()); acc=acc?acc+'/'+part:part; var pth=acc; crumbsEl.appendChild(crumb(part,idx===parts.length-1,function(){ goPath(pth); })); });
|
|
632
|
+
}
|
|
633
|
+
var rendering=false;
|
|
634
|
+
function render(){ if(rendering) return; rendering=true; try{ renderInner(); } finally{ rendering=false; } }
|
|
635
|
+
function renderInner(){
|
|
636
|
+
renderCrumbs(); bodyEl.innerHTML='';
|
|
637
|
+
if(!st.root){
|
|
638
|
+
VROOTS.forEach(function(v){ if(st.status[v.key]==='idle') loadRoot(v.key,false); });
|
|
639
|
+
VROOTS.forEach(function(v){ bodyEl.appendChild(vrootRow(v)); });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
var s=st.status[st.root];
|
|
643
|
+
if(s==='idle'){ loadRoot(st.root,false); return; }
|
|
644
|
+
if(s==='loading'){ bodyEl.appendChild(stateNode('Loading your files\\u2026')); return; }
|
|
645
|
+
if(s==='auth'){ bodyEl.appendChild(stateNode('Sign in to browse your files.',{href:'/login',linkText:'Sign in'})); return; }
|
|
646
|
+
if(s==='error'){ bodyEl.appendChild(stateNode('Couldn\\u2019t reach your files.',{retry:true})); return; }
|
|
647
|
+
var data=st.data[st.root]||{items:[],folders:[]};
|
|
648
|
+
var cur=normFolder(st.path[st.root]);
|
|
649
|
+
var folders=immediateFolders(data,cur);
|
|
650
|
+
var here=data.items.filter(function(it){ return normFolder(it.folderPath)===cur; });
|
|
651
|
+
if(!folders.length&&!here.length){ bodyEl.appendChild(stateNode(cur?'This folder is empty.':'No files in '+ROOTS[st.root].label+' yet.')); return; }
|
|
652
|
+
folders.forEach(function(f){ bodyEl.appendChild(folderRow(f.name,function(){ goPath(f.path); })); });
|
|
653
|
+
here.forEach(function(it){ bodyEl.appendChild(fileRow(it)); });
|
|
654
|
+
}
|
|
655
|
+
function goPath(p){ st.path[st.root]=normFolder(p); render(); }
|
|
656
|
+
function goVirtualRoot(){ st.root=''; render(); }
|
|
657
|
+
function enterRoot(rootKey){ if(!ROOTS[rootKey]) return; st.root=rootKey; st.path[rootKey]=''; loadRoot(rootKey,false); render(); }
|
|
658
|
+
render();
|
|
659
|
+
})();
|
|
660
|
+
// channels: quick search filter
|
|
661
|
+
var chSearch=root.querySelector('[data-rk-channel-search]');
|
|
662
|
+
var chList=root.querySelector('[data-rk-channel-list]');
|
|
663
|
+
var chNone=root.querySelector('[data-rk-channel-noresults]');
|
|
664
|
+
if(chSearch&&chList){
|
|
665
|
+
chSearch.addEventListener('input',function(){
|
|
666
|
+
var q=(chSearch.value||'').trim().toLowerCase();
|
|
667
|
+
var shown=0;
|
|
668
|
+
chList.querySelectorAll('[data-rk-channel-hay]').forEach(function(row){
|
|
669
|
+
var ok=!q||(row.getAttribute('data-rk-channel-hay')||'').indexOf(q)>-1;
|
|
670
|
+
row.hidden=!ok;if(ok)shown++;
|
|
671
|
+
});
|
|
672
|
+
if(chNone)chNone.hidden=!(q&&shown===0);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// channels: "…" actions menu
|
|
676
|
+
var menuBtn=root.querySelector('[data-rk-channel-menu]');
|
|
677
|
+
var menuPop=root.querySelector('[data-rk-channel-menu-pop]');
|
|
678
|
+
if(menuBtn&&menuPop){
|
|
679
|
+
menuBtn.addEventListener('click',function(e){
|
|
680
|
+
e.stopPropagation();
|
|
681
|
+
var open=menuPop.hidden;
|
|
682
|
+
menuPop.hidden=!open;
|
|
683
|
+
menuBtn.setAttribute('aria-expanded',open?'true':'false');
|
|
684
|
+
});
|
|
685
|
+
root.addEventListener('click',function(e){
|
|
686
|
+
if(!menuPop.hidden&&!menuPop.contains(e.target)&&e.target!==menuBtn){menuPop.hidden=true;menuBtn.setAttribute('aria-expanded','false');}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
// channel dialogs (FlockPoster connect + email channel)
|
|
690
|
+
function wireDialog(dlgSel,openSel){
|
|
691
|
+
var dlg=root.querySelector(dlgSel);
|
|
692
|
+
if(!dlg)return null;
|
|
693
|
+
root.querySelectorAll(openSel).forEach(function(btn){
|
|
694
|
+
btn.addEventListener('click',function(){
|
|
695
|
+
if(menuPop)menuPop.hidden=true;
|
|
696
|
+
if(typeof dlg.showModal==='function'&&!dlg.open)dlg.showModal();
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
dlg.addEventListener('click',function(e){
|
|
700
|
+
var r=dlg.getBoundingClientRect();
|
|
701
|
+
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dlg.close();
|
|
702
|
+
});
|
|
703
|
+
dlg.querySelectorAll('[data-rk-dialog-close]').forEach(function(btn){btn.addEventListener('click',function(){dlg.close();});});
|
|
704
|
+
return dlg;
|
|
705
|
+
}
|
|
706
|
+
var flockDlg=wireDialog('[data-rk-flock-dialog]','[data-rk-open-flock]');
|
|
707
|
+
wireDialog('[data-rk-email-dialog]','[data-rk-open-email]');
|
|
708
|
+
if(flockDlg&&flockDlg.getAttribute('data-rk-autoopen')==='1'&&typeof flockDlg.showModal==='function'&&!flockDlg.open){flockDlg.showModal();}
|
|
709
|
+
// wallet top-up → real funding link
|
|
710
|
+
var topBtn=root.querySelector('[data-rk-topup]');
|
|
711
|
+
var topStatus=root.querySelector('[data-rk-topup-status]');
|
|
712
|
+
if(topBtn){
|
|
713
|
+
topBtn.addEventListener('click',function(){
|
|
714
|
+
topBtn.disabled=true;if(topStatus){topStatus.hidden=false;topStatus.textContent='Starting secure checkout…';}
|
|
715
|
+
fetch('/settings/wallet/funding-link',{method:'POST',headers:{'content-type':'application/json'},credentials:'same-origin',body:'{}'})
|
|
716
|
+
.then(function(r){return r.json();})
|
|
717
|
+
.then(function(j){if(j&&j.checkout_url){window.location.href=j.checkout_url;}else{throw new Error(j&&j.error||'Unable to start funding');}})
|
|
718
|
+
.catch(function(err){topBtn.disabled=false;if(topStatus){topStatus.hidden=false;topStatus.textContent=String(err.message||err);}});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
343
721
|
})();`;
|
|
344
722
|
return reskinDocument({
|
|
345
723
|
title: "vidfarm reskin — Settings",
|
|
346
|
-
description: "
|
|
724
|
+
description: "Vidfarm settings — profile, wallet, channels, developer.",
|
|
347
725
|
activeSlug: "settings",
|
|
726
|
+
account: { name: input.name, email: input.email },
|
|
348
727
|
pageCss,
|
|
349
728
|
body,
|
|
350
729
|
script
|