screencraft 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +30 -0
- package/.env.example +3 -0
- package/MCP_README.md +200 -0
- package/README.md +148 -0
- package/bin/screencraft.js +61 -0
- package/package.json +31 -0
- package/src/auth/keystore.js +148 -0
- package/src/commands/init.js +119 -0
- package/src/commands/launch.js +405 -0
- package/src/detectors/detectBrand.js +1222 -0
- package/src/detectors/simulator.js +317 -0
- package/src/generators/analyzeStyleReference.js +471 -0
- package/src/generators/compositePSD.js +682 -0
- package/src/generators/copy.js +147 -0
- package/src/mcp/index.js +394 -0
- package/src/pipeline/aeSwap.js +369 -0
- package/src/pipeline/download.js +32 -0
- package/src/pipeline/queue.js +101 -0
- package/src/server/index.js +627 -0
- package/src/server/public/app.js +738 -0
- package/src/server/public/index.html +255 -0
- package/src/server/public/style.css +751 -0
- package/src/server/session.js +36 -0
- package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
- package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
- package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
- package/templates/ae_swap.jsx +284 -0
- package/templates/layouts/minimal.psd +0 -0
- package/templates/screencraft.config.example.js +165 -0
- package/test/output/layout_test.png +0 -0
- package/test/output/style_profile.json +64 -0
- package/test/reference.png +0 -0
- package/test/test_brand.js +69 -0
- package/test/test_psd.js +83 -0
- package/test/test_style_analysis.js +114 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenCraft Web UI — Frontend
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ── State ──────────────────────────────────────────────────────────
|
|
6
|
+
let currentStep = 0;
|
|
7
|
+
let brand = {};
|
|
8
|
+
let selectedTemplate = null;
|
|
9
|
+
let templates = [];
|
|
10
|
+
let screenshots = [];
|
|
11
|
+
let suggestions = [];
|
|
12
|
+
let approvedTexts = [];
|
|
13
|
+
let currentHeadline = 0;
|
|
14
|
+
let licenseTier = null;
|
|
15
|
+
|
|
16
|
+
// ── Step navigation ────────────────────────────────────────────────
|
|
17
|
+
function goStep(n) {
|
|
18
|
+
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
|
|
19
|
+
document.getElementById(`step-${n}`).classList.add('active');
|
|
20
|
+
currentStep = n;
|
|
21
|
+
|
|
22
|
+
// Update progress bar
|
|
23
|
+
document.querySelectorAll('.progress-bar .seg').forEach(seg => {
|
|
24
|
+
const s = parseInt(seg.dataset.step);
|
|
25
|
+
seg.className = 'seg';
|
|
26
|
+
if (s < n) seg.classList.add('done');
|
|
27
|
+
else if (s === n) seg.classList.add('current');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Step 0 → 1: Detect project ────────────────────────────────────
|
|
34
|
+
async function detectProject() {
|
|
35
|
+
const projectPath = document.getElementById('projectPath').value.trim();
|
|
36
|
+
if (!projectPath) return;
|
|
37
|
+
|
|
38
|
+
const btn = event.target;
|
|
39
|
+
btn.disabled = true;
|
|
40
|
+
btn.textContent = 'Detecting...';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/api/detect', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ projectPath }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
if (!res.ok) throw new Error(data.error);
|
|
51
|
+
|
|
52
|
+
brand = data.brand;
|
|
53
|
+
|
|
54
|
+
// Populate brand UI
|
|
55
|
+
document.getElementById('brandFramework').innerHTML =
|
|
56
|
+
`<span class="badge badge-framework">${data.framework.name}</span>`;
|
|
57
|
+
document.getElementById('brandAppName').value = brand.appName || '';
|
|
58
|
+
|
|
59
|
+
// Colors
|
|
60
|
+
setColorSwatch('primary', brand.primary);
|
|
61
|
+
setColorSwatch('secondary', brand.secondary);
|
|
62
|
+
setColorSwatch('accent', brand.accent);
|
|
63
|
+
setColorSwatch('background', brand.background);
|
|
64
|
+
|
|
65
|
+
// Icon
|
|
66
|
+
if (brand.icon) {
|
|
67
|
+
document.getElementById('brandIconStatus').innerHTML = '<span class="badge badge-found">Found</span>';
|
|
68
|
+
document.getElementById('iconImg').src = brand.icon;
|
|
69
|
+
document.getElementById('brandIconPreview').style.display = 'block';
|
|
70
|
+
} else {
|
|
71
|
+
document.getElementById('brandIconStatus').innerHTML = '<span class="badge badge-missing">Not found</span>';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Font
|
|
75
|
+
document.getElementById('brandFont').textContent = brand.font?.family || 'System default';
|
|
76
|
+
|
|
77
|
+
goStep(1);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
alert('Detection failed: ' + err.message);
|
|
80
|
+
} finally {
|
|
81
|
+
btn.disabled = false;
|
|
82
|
+
btn.textContent = 'Detect Project';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setColorSwatch(name, hex) {
|
|
87
|
+
const cap = name.charAt(0).toUpperCase() + name.slice(1);
|
|
88
|
+
document.getElementById(`swatch${cap}`).style.background = hex;
|
|
89
|
+
document.getElementById(`color${cap}`).value = hex;
|
|
90
|
+
document.getElementById(`hex${cap}`).textContent = hex;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function updateColor(name, hex) {
|
|
94
|
+
const cap = name.charAt(0).toUpperCase() + name.slice(1);
|
|
95
|
+
document.getElementById(`swatch${cap}`).style.background = hex;
|
|
96
|
+
document.getElementById(`hex${cap}`).textContent = hex;
|
|
97
|
+
brand[name] = hex;
|
|
98
|
+
|
|
99
|
+
// Sync to server
|
|
100
|
+
fetch('/api/brand/update', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ [name]: hex }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Step 1 → 2: Load templates ────────────────────────────────────
|
|
108
|
+
async function loadTemplates() {
|
|
109
|
+
// Sync app name
|
|
110
|
+
const appName = document.getElementById('brandAppName').value.trim();
|
|
111
|
+
if (appName !== brand.appName) {
|
|
112
|
+
brand.appName = appName;
|
|
113
|
+
await fetch('/api/brand/update', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ appName }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
goStep(2);
|
|
121
|
+
document.getElementById('templateLoading').style.display = 'flex';
|
|
122
|
+
document.getElementById('templateGrid').style.display = 'none';
|
|
123
|
+
document.getElementById('templateBtns').style.display = 'none';
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch('/api/templates');
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
|
|
129
|
+
templates = data.templates;
|
|
130
|
+
selectedTemplate = data.templates.length > 0 ? data.templates[0].name : null;
|
|
131
|
+
|
|
132
|
+
renderTemplateGrid();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// If endpoint doesn't exist yet, show fallback
|
|
135
|
+
templates = [{ name: 'auto', label: 'Auto', description: 'Automatically select the best available template' }];
|
|
136
|
+
selectedTemplate = 'auto';
|
|
137
|
+
renderTemplateGrid();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderTemplateGrid() {
|
|
142
|
+
const grid = document.getElementById('templateGrid');
|
|
143
|
+
grid.innerHTML = '';
|
|
144
|
+
|
|
145
|
+
templates.forEach(t => {
|
|
146
|
+
const card = document.createElement('div');
|
|
147
|
+
card.className = 'template-card' + (selectedTemplate === t.name ? ' selected' : '');
|
|
148
|
+
card.onclick = () => {
|
|
149
|
+
selectedTemplate = t.name;
|
|
150
|
+
// Sync to server
|
|
151
|
+
fetch('/api/template/select', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ template: t.name }),
|
|
155
|
+
});
|
|
156
|
+
renderTemplateGrid();
|
|
157
|
+
};
|
|
158
|
+
card.innerHTML = `
|
|
159
|
+
<div class="template-name">${t.label || t.name}</div>
|
|
160
|
+
<div class="template-desc">${t.description || ''}</div>
|
|
161
|
+
${t.badge ? `<div class="template-badge">${t.badge}</div>` : ''}
|
|
162
|
+
`;
|
|
163
|
+
grid.appendChild(card);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
document.getElementById('templateLoading').style.display = 'none';
|
|
167
|
+
grid.style.display = 'grid';
|
|
168
|
+
document.getElementById('templateBtns').style.display = 'flex';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Step 2 → 3: Init screenshots ──────────────────────────────────
|
|
172
|
+
async function captureScreenshots() {
|
|
173
|
+
goStep(3);
|
|
174
|
+
document.getElementById('screenshotLoading').style.display = 'flex';
|
|
175
|
+
document.getElementById('captureMode').style.display = 'none';
|
|
176
|
+
document.getElementById('manualMode').style.display = 'none';
|
|
177
|
+
document.getElementById('screenshotBtns').style.display = 'none';
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch('/api/screenshots/init', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const data = await res.json();
|
|
186
|
+
if (!res.ok) throw new Error(data.error);
|
|
187
|
+
|
|
188
|
+
document.getElementById('screenshotLoading').style.display = 'none';
|
|
189
|
+
|
|
190
|
+
if (data.mode === 'manual') {
|
|
191
|
+
screenshots = data.screenshots;
|
|
192
|
+
renderManualGrid();
|
|
193
|
+
} else if (data.mode === 'simulator') {
|
|
194
|
+
screenshots = [];
|
|
195
|
+
document.getElementById('captureMode').style.display = 'block';
|
|
196
|
+
document.getElementById('captureInstructions').textContent =
|
|
197
|
+
`${data.device || 'Simulator'} is ready. Navigate your app in the Simulator, then click "Capture Screen" for each screen (up to 6).`;
|
|
198
|
+
} else if (data.mode === 'waiting') {
|
|
199
|
+
document.getElementById('captureMode').style.display = 'block';
|
|
200
|
+
document.getElementById('captureInstructions').innerHTML =
|
|
201
|
+
`<strong>No simulator with your app detected.</strong><br><br>` +
|
|
202
|
+
`1. Open your project in Xcode<br>` +
|
|
203
|
+
`2. Build & Run (Cmd+R) to launch in the Simulator<br>` +
|
|
204
|
+
`3. Once your app is running, click "Check Again" below`;
|
|
205
|
+
document.getElementById('captureBtn').textContent = 'Check Again';
|
|
206
|
+
document.getElementById('captureBtn').onclick = () => {
|
|
207
|
+
document.getElementById('captureBtn').textContent = 'Capture Screen';
|
|
208
|
+
document.getElementById('captureBtn').onclick = captureOne;
|
|
209
|
+
captureScreenshots();
|
|
210
|
+
};
|
|
211
|
+
} else if (data.mode === 'none') {
|
|
212
|
+
document.getElementById('captureMode').style.display = 'block';
|
|
213
|
+
document.getElementById('captureInstructions').innerHTML =
|
|
214
|
+
`<strong>No screenshots found.</strong><br><br>` +
|
|
215
|
+
`Place PNG files in a <code>screenshots/</code> folder in your project directory, then click "Check Again".`;
|
|
216
|
+
document.getElementById('captureBtn').textContent = 'Check Again';
|
|
217
|
+
document.getElementById('captureBtn').onclick = () => captureScreenshots();
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
alert('Screenshot init failed: ' + err.message);
|
|
221
|
+
goStep(2);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function captureOne() {
|
|
226
|
+
const btn = document.getElementById('captureBtn');
|
|
227
|
+
btn.disabled = true;
|
|
228
|
+
btn.textContent = 'Capturing...';
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const res = await fetch('/api/screenshots/capture', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
if (!res.ok) throw new Error(data.error);
|
|
238
|
+
|
|
239
|
+
screenshots.push({
|
|
240
|
+
index: data.index,
|
|
241
|
+
label: null,
|
|
242
|
+
filename: data.filename,
|
|
243
|
+
base64: data.base64,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
renderCaptureGrid();
|
|
247
|
+
document.getElementById('screenshotCount').textContent = `${screenshots.length} screens`;
|
|
248
|
+
|
|
249
|
+
if (screenshots.length >= 6) {
|
|
250
|
+
btn.style.display = 'none';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
document.getElementById('screenshotBtns').style.display = 'flex';
|
|
254
|
+
} catch (err) {
|
|
255
|
+
alert('Capture failed: ' + err.message);
|
|
256
|
+
} finally {
|
|
257
|
+
btn.disabled = false;
|
|
258
|
+
btn.textContent = 'Capture Screen';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function renderCaptureGrid() {
|
|
263
|
+
const grid = document.getElementById('screenshotGrid');
|
|
264
|
+
grid.innerHTML = '';
|
|
265
|
+
|
|
266
|
+
screenshots.forEach((s, i) => {
|
|
267
|
+
const card = document.createElement('div');
|
|
268
|
+
card.className = 'screenshot-card';
|
|
269
|
+
card.innerHTML = `
|
|
270
|
+
<img src="${s.base64}" alt="Screen ${i + 1}">
|
|
271
|
+
<div class="screen-label">
|
|
272
|
+
Screen ${i + 1}
|
|
273
|
+
<button class="btn-ghost" style="font-size:11px; padding:2px 6px; margin-left:4px;" onclick="removeScreenshot(${i})">Remove</button>
|
|
274
|
+
</div>
|
|
275
|
+
`;
|
|
276
|
+
grid.appendChild(card);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function removeScreenshot(idx) {
|
|
281
|
+
await fetch('/api/screenshots/remove', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({ index: idx }),
|
|
285
|
+
});
|
|
286
|
+
screenshots.splice(idx, 1);
|
|
287
|
+
renderCaptureGrid();
|
|
288
|
+
document.getElementById('screenshotCount').textContent = `${screenshots.length} screens`;
|
|
289
|
+
document.getElementById('captureBtn').style.display = '';
|
|
290
|
+
if (screenshots.length === 0) {
|
|
291
|
+
document.getElementById('screenshotBtns').style.display = 'none';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderManualGrid() {
|
|
296
|
+
const grid = document.getElementById('manualGrid');
|
|
297
|
+
grid.innerHTML = '';
|
|
298
|
+
|
|
299
|
+
screenshots.forEach((s, i) => {
|
|
300
|
+
const card = document.createElement('div');
|
|
301
|
+
card.className = 'screenshot-card';
|
|
302
|
+
card.draggable = true;
|
|
303
|
+
card.dataset.index = i;
|
|
304
|
+
card.innerHTML = `
|
|
305
|
+
<img src="${s.base64}" alt="Screen ${i + 1}">
|
|
306
|
+
<div class="screen-label">Screen ${i + 1}${s.label ? ' — ' + s.label : ''}</div>
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
card.addEventListener('dragstart', e => {
|
|
310
|
+
e.dataTransfer.setData('text/plain', i);
|
|
311
|
+
card.classList.add('dragging');
|
|
312
|
+
});
|
|
313
|
+
card.addEventListener('dragend', () => card.classList.remove('dragging'));
|
|
314
|
+
card.addEventListener('dragover', e => e.preventDefault());
|
|
315
|
+
card.addEventListener('drop', e => {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
const from = parseInt(e.dataTransfer.getData('text/plain'));
|
|
318
|
+
const to = i;
|
|
319
|
+
if (from !== to) {
|
|
320
|
+
const item = screenshots.splice(from, 1)[0];
|
|
321
|
+
screenshots.splice(to, 0, item);
|
|
322
|
+
renderManualGrid();
|
|
323
|
+
fetch('/api/screenshots/reorder', {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { 'Content-Type': 'application/json' },
|
|
326
|
+
body: JSON.stringify({ order: screenshots.map((_, idx) => idx) }),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
grid.appendChild(card);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
document.getElementById('screenshotCount').textContent = `${screenshots.length} screens`;
|
|
335
|
+
document.getElementById('manualMode').style.display = 'block';
|
|
336
|
+
document.getElementById('screenshotBtns').style.display = 'flex';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Step 3 → 4: Generate headlines ─────────────────────────────────
|
|
340
|
+
async function generateHeadlines() {
|
|
341
|
+
goStep(4);
|
|
342
|
+
document.getElementById('headlineLoading').style.display = 'flex';
|
|
343
|
+
document.getElementById('headlineEditor').style.display = 'none';
|
|
344
|
+
document.getElementById('headlineBtns').style.display = 'none';
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch('/api/headlines', {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const data = await res.json();
|
|
353
|
+
if (!res.ok) throw new Error(data.error);
|
|
354
|
+
|
|
355
|
+
suggestions = data.suggestions;
|
|
356
|
+
approvedTexts = suggestions.map(s => ({ ...s.options[0] }));
|
|
357
|
+
currentHeadline = 0;
|
|
358
|
+
|
|
359
|
+
document.getElementById('headlineLoading').style.display = 'none';
|
|
360
|
+
document.getElementById('headlineEditor').style.display = 'block';
|
|
361
|
+
document.getElementById('headlineBtns').style.display = 'block';
|
|
362
|
+
|
|
363
|
+
buildHeadlineDots();
|
|
364
|
+
showHeadline();
|
|
365
|
+
} catch (err) {
|
|
366
|
+
alert('Headline generation failed: ' + err.message);
|
|
367
|
+
goStep(3);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildHeadlineDots() {
|
|
372
|
+
const dots = document.getElementById('headlineDots');
|
|
373
|
+
dots.innerHTML = '';
|
|
374
|
+
screenshots.forEach((_, i) => {
|
|
375
|
+
const dot = document.createElement('div');
|
|
376
|
+
dot.className = 'dot' + (i === 0 ? ' active' : '');
|
|
377
|
+
dot.onclick = () => { currentHeadline = i; showHeadline(); };
|
|
378
|
+
dots.appendChild(dot);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function showHeadline() {
|
|
383
|
+
const i = currentHeadline;
|
|
384
|
+
const s = screenshots[i];
|
|
385
|
+
const opts = suggestions[i];
|
|
386
|
+
const approved = approvedTexts[i];
|
|
387
|
+
|
|
388
|
+
const editor = document.getElementById('headlineEditor');
|
|
389
|
+
editor.innerHTML = `
|
|
390
|
+
<div class="headline-editor">
|
|
391
|
+
<div class="headline-preview">
|
|
392
|
+
<div class="phone-mockup">
|
|
393
|
+
<div class="phone-frame">
|
|
394
|
+
<img src="${s.base64}" alt="Screen ${i + 1}">
|
|
395
|
+
<div class="phone-notch"></div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="headline-controls">
|
|
400
|
+
<h3>Screen ${i + 1} of ${screenshots.length}</h3>
|
|
401
|
+
<p class="screen-desc">${opts.description || ''}</p>
|
|
402
|
+
<div class="option-list">
|
|
403
|
+
${opts.options.map((o, idx) => `
|
|
404
|
+
<button class="option-btn ${approved.white === o.white && approved.accent === o.accent ? 'selected' : ''}"
|
|
405
|
+
onclick="selectOption(${i}, ${idx})">
|
|
406
|
+
<span class="opt-num">${idx + 1}</span>
|
|
407
|
+
<span><span class="opt-white">${o.white}</span> <span class="opt-accent">${o.accent}</span></span>
|
|
408
|
+
</button>
|
|
409
|
+
`).join('')}
|
|
410
|
+
</div>
|
|
411
|
+
<p class="text-dim text-sm mt-8">Or write custom:</p>
|
|
412
|
+
<div class="custom-inputs">
|
|
413
|
+
<input type="text" placeholder="White text" id="customWhite" value="${approved.white}"
|
|
414
|
+
oninput="customHeadline(${i})">
|
|
415
|
+
<input type="text" placeholder="Accent word" id="customAccent" value="${approved.accent}"
|
|
416
|
+
oninput="customHeadline(${i})">
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
`;
|
|
421
|
+
|
|
422
|
+
// Update dots
|
|
423
|
+
document.querySelectorAll('.headline-dots .dot').forEach((dot, idx) => {
|
|
424
|
+
dot.className = 'dot';
|
|
425
|
+
if (idx === i) dot.classList.add('active');
|
|
426
|
+
else if (approvedTexts[idx]) dot.classList.add('done');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Update nav buttons
|
|
430
|
+
const prevBtn = document.getElementById('headlinePrev');
|
|
431
|
+
const nextBtn = document.getElementById('headlineNext');
|
|
432
|
+
prevBtn.style.visibility = i === 0 ? 'hidden' : 'visible';
|
|
433
|
+
|
|
434
|
+
if (i === screenshots.length - 1) {
|
|
435
|
+
nextBtn.textContent = 'Preview All';
|
|
436
|
+
nextBtn.onclick = () => showPreview();
|
|
437
|
+
} else {
|
|
438
|
+
nextBtn.textContent = 'Next Screen';
|
|
439
|
+
nextBtn.onclick = () => headlineNav(1);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function selectOption(screenIdx, optIdx) {
|
|
444
|
+
const opt = suggestions[screenIdx].options[optIdx];
|
|
445
|
+
approvedTexts[screenIdx] = { white: opt.white, accent: opt.accent };
|
|
446
|
+
showHeadline();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function customHeadline(screenIdx) {
|
|
450
|
+
const w = document.getElementById('customWhite').value;
|
|
451
|
+
const a = document.getElementById('customAccent').value;
|
|
452
|
+
approvedTexts[screenIdx] = { white: w, accent: a };
|
|
453
|
+
|
|
454
|
+
// Deselect options
|
|
455
|
+
document.querySelectorAll('.option-btn').forEach(btn => btn.classList.remove('selected'));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function headlineNav(dir) {
|
|
459
|
+
currentHeadline = Math.max(0, Math.min(screenshots.length - 1, currentHeadline + dir));
|
|
460
|
+
showHeadline();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── Step 4 → 5: Preview ───────────────────────────────────────────
|
|
464
|
+
function showPreview() {
|
|
465
|
+
goStep(5);
|
|
466
|
+
|
|
467
|
+
const grid = document.getElementById('previewGrid');
|
|
468
|
+
grid.innerHTML = '';
|
|
469
|
+
|
|
470
|
+
screenshots.forEach((s, i) => {
|
|
471
|
+
const text = approvedTexts[i];
|
|
472
|
+
const card = document.createElement('div');
|
|
473
|
+
card.className = 'preview-card';
|
|
474
|
+
card.innerHTML = `
|
|
475
|
+
<div class="preview-headline">
|
|
476
|
+
<span class="white">${text.white}</span>
|
|
477
|
+
<span class="accent-text">${text.accent}</span>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="phone-frame">
|
|
480
|
+
<img src="${s.base64}" alt="Screen ${i + 1}">
|
|
481
|
+
<div class="phone-notch"></div>
|
|
482
|
+
</div>
|
|
483
|
+
`;
|
|
484
|
+
grid.appendChild(card);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Step 5 → 6: License check ──────────────────────────────────────
|
|
489
|
+
async function checkLicense() {
|
|
490
|
+
goStep(6);
|
|
491
|
+
|
|
492
|
+
const content = document.getElementById('licenseContent');
|
|
493
|
+
const btns = document.getElementById('licenseBtns');
|
|
494
|
+
content.innerHTML = '<div class="loading"><div class="render-spinner"></div><span>Checking license...</span></div>';
|
|
495
|
+
btns.innerHTML = '';
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const res = await fetch('/api/license/check', {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
501
|
+
body: JSON.stringify({}),
|
|
502
|
+
});
|
|
503
|
+
const data = await res.json();
|
|
504
|
+
|
|
505
|
+
if (data.valid && data.tier) {
|
|
506
|
+
licenseTier = data.tier;
|
|
507
|
+
content.innerHTML = `
|
|
508
|
+
<div class="license-box">
|
|
509
|
+
<div style="font-size:48px; margin-bottom:16px;">◆</div>
|
|
510
|
+
<h3>License Active</h3>
|
|
511
|
+
<p><span class="tier-badge ${data.tier}">${data.tier}</span></p>
|
|
512
|
+
<p>Your launch kit will include screenshots, PSDs, AE project, and video.</p>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
btns.innerHTML = `
|
|
516
|
+
<button class="btn btn-secondary" onclick="goStep(5)">Back</button>
|
|
517
|
+
<button class="btn btn-primary" onclick="startRender(false)">Generate Full Kit</button>
|
|
518
|
+
`;
|
|
519
|
+
} else {
|
|
520
|
+
content.innerHTML = `
|
|
521
|
+
<div class="license-box">
|
|
522
|
+
<div class="lock-icon">🔒</div>
|
|
523
|
+
<h3>License Required for Full Kit</h3>
|
|
524
|
+
<p>A license key unlocks video rendering, AE project file, and layered PSDs.<br>
|
|
525
|
+
Free mode generates App Store screenshot PNGs only.</p>
|
|
526
|
+
<div class="license-input-row mt-16">
|
|
527
|
+
<input type="text" id="licenseKeyInput" placeholder="MF-XXXX-XXXX-XXXX">
|
|
528
|
+
<button class="btn btn-primary" onclick="activateLicense()">Activate</button>
|
|
529
|
+
</div>
|
|
530
|
+
<p class="mt-16 text-sm">
|
|
531
|
+
<a href="https://screencraft.dev/activate" target="_blank" style="color: var(--accent)">Get a license key</a>
|
|
532
|
+
</p>
|
|
533
|
+
</div>
|
|
534
|
+
`;
|
|
535
|
+
btns.innerHTML = `
|
|
536
|
+
<button class="btn btn-secondary" onclick="goStep(5)">Back</button>
|
|
537
|
+
<button class="btn btn-secondary" onclick="startRender(true)">Screenshots Only (Free)</button>
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
content.innerHTML = `<p class="text-center">Error checking license: ${err.message}</p>`;
|
|
542
|
+
btns.innerHTML = `
|
|
543
|
+
<button class="btn btn-secondary" onclick="goStep(5)">Back</button>
|
|
544
|
+
<button class="btn btn-secondary" onclick="startRender(true)">Screenshots Only (Free)</button>
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function activateLicense() {
|
|
550
|
+
const key = document.getElementById('licenseKeyInput').value.trim();
|
|
551
|
+
if (!key) return;
|
|
552
|
+
|
|
553
|
+
const res = await fetch('/api/license/check', {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
556
|
+
body: JSON.stringify({ key }),
|
|
557
|
+
});
|
|
558
|
+
const data = await res.json();
|
|
559
|
+
|
|
560
|
+
if (data.valid) {
|
|
561
|
+
licenseTier = data.tier;
|
|
562
|
+
checkLicense();
|
|
563
|
+
} else {
|
|
564
|
+
alert('Invalid license key. Please check and try again.');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Step 6 → 7: Render ────────────────────────────────────────────
|
|
569
|
+
async function startRender(screenshotsOnly) {
|
|
570
|
+
// Send approved texts
|
|
571
|
+
await fetch('/api/approve', {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
headers: { 'Content-Type': 'application/json' },
|
|
574
|
+
body: JSON.stringify({ approvedTexts }),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
goStep(7);
|
|
578
|
+
document.getElementById('renderProgress').style.display = 'block';
|
|
579
|
+
document.getElementById('outputSection').style.display = 'none';
|
|
580
|
+
|
|
581
|
+
// Start render
|
|
582
|
+
await fetch('/api/render', {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
headers: { 'Content-Type': 'application/json' },
|
|
585
|
+
body: JSON.stringify({ screenshotsOnly }),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Poll status
|
|
589
|
+
pollRender();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function pollRender() {
|
|
593
|
+
try {
|
|
594
|
+
const res = await fetch('/api/render/status');
|
|
595
|
+
const status = await res.json();
|
|
596
|
+
|
|
597
|
+
document.getElementById('renderFill').style.width = status.progress + '%';
|
|
598
|
+
document.getElementById('renderMessage').textContent = status.message;
|
|
599
|
+
|
|
600
|
+
if (status.phase === 'done') {
|
|
601
|
+
showOutput();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (status.phase === 'error') {
|
|
606
|
+
document.getElementById('renderMessage').textContent = 'Error: ' + status.message;
|
|
607
|
+
document.getElementById('renderMessage').style.color = 'var(--red)';
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
} catch {}
|
|
611
|
+
|
|
612
|
+
setTimeout(pollRender, 800);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function showOutput() {
|
|
616
|
+
document.getElementById('renderProgress').style.display = 'none';
|
|
617
|
+
document.getElementById('outputSection').style.display = 'block';
|
|
618
|
+
|
|
619
|
+
const res = await fetch('/api/output');
|
|
620
|
+
const data = await res.json();
|
|
621
|
+
|
|
622
|
+
const grid = document.getElementById('outputGrid');
|
|
623
|
+
grid.innerHTML = '';
|
|
624
|
+
|
|
625
|
+
data.files.forEach(f => {
|
|
626
|
+
if (f.type === 'screenshot') {
|
|
627
|
+
const card = document.createElement('div');
|
|
628
|
+
card.className = 'output-card';
|
|
629
|
+
card.innerHTML = `
|
|
630
|
+
<img src="${f.url}" alt="${f.filename}">
|
|
631
|
+
<div class="output-info">
|
|
632
|
+
<span class="output-name">${f.filename}</span>
|
|
633
|
+
<a href="${f.url}" download class="output-download">Download</a>
|
|
634
|
+
</div>
|
|
635
|
+
`;
|
|
636
|
+
grid.appendChild(card);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const videoFile = data.files.find(f => f.type === 'video');
|
|
641
|
+
const aepFile = data.files.find(f => f.type === 'aep');
|
|
642
|
+
const videoError = data.files.find(f => f.type === 'video-error');
|
|
643
|
+
|
|
644
|
+
if (videoFile || aepFile || videoError) {
|
|
645
|
+
const extras = document.createElement('div');
|
|
646
|
+
extras.style.cssText = 'grid-column: 1 / -1; margin-top: 8px;';
|
|
647
|
+
extras.className = 'card';
|
|
648
|
+
let inner = '';
|
|
649
|
+
|
|
650
|
+
if (videoFile) {
|
|
651
|
+
inner += `<div class="card-row">
|
|
652
|
+
<span class="label">Video</span>
|
|
653
|
+
<a href="${videoFile.url}" download class="output-download">${videoFile.filename}</a>
|
|
654
|
+
</div>`;
|
|
655
|
+
}
|
|
656
|
+
if (aepFile) {
|
|
657
|
+
inner += `<div class="card-row">
|
|
658
|
+
<span class="label">AE Project</span>
|
|
659
|
+
<span class="value">${aepFile.filename}</span>
|
|
660
|
+
</div>`;
|
|
661
|
+
}
|
|
662
|
+
if (videoError) {
|
|
663
|
+
inner += `<div class="card-row">
|
|
664
|
+
<span class="label">Video</span>
|
|
665
|
+
<span class="text-dim text-sm">${videoError.message}</span>
|
|
666
|
+
</div>`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
extras.innerHTML = inner;
|
|
670
|
+
grid.appendChild(extras);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function openOutputFolder() {
|
|
675
|
+
fetch('/api/output/open', { method: 'POST' });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ── Init ───────────────────────────────────────────────────────────
|
|
679
|
+
document.getElementById('projectPath').addEventListener('keydown', e => {
|
|
680
|
+
if (e.key === 'Enter') detectProject();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Check if MCP has pre-populated session data
|
|
684
|
+
(async function checkMcpBootstrap() {
|
|
685
|
+
try {
|
|
686
|
+
const res = await fetch('/api/session');
|
|
687
|
+
const data = await res.json();
|
|
688
|
+
|
|
689
|
+
if (!data.hasBrand) return; // Nothing pre-loaded
|
|
690
|
+
|
|
691
|
+
// Populate brand
|
|
692
|
+
brand = data.brand;
|
|
693
|
+
document.getElementById('projectPath').value = data.projectPath || '';
|
|
694
|
+
document.getElementById('brandFramework').innerHTML =
|
|
695
|
+
`<span class="badge badge-framework">${data.framework?.name || 'unknown'}</span>`;
|
|
696
|
+
document.getElementById('brandAppName').value = brand.appName || '';
|
|
697
|
+
setColorSwatch('primary', brand.primary);
|
|
698
|
+
setColorSwatch('secondary', brand.secondary);
|
|
699
|
+
setColorSwatch('accent', brand.accent);
|
|
700
|
+
setColorSwatch('background', brand.background);
|
|
701
|
+
if (brand.icon) {
|
|
702
|
+
document.getElementById('brandIconStatus').innerHTML = '<span class="badge badge-found">Found</span>';
|
|
703
|
+
document.getElementById('iconImg').src = brand.icon;
|
|
704
|
+
document.getElementById('brandIconPreview').style.display = 'block';
|
|
705
|
+
}
|
|
706
|
+
document.getElementById('brandFont').textContent = brand.font?.family || 'System default';
|
|
707
|
+
|
|
708
|
+
// Populate screenshots if available
|
|
709
|
+
if (data.hasScreenshots && data.screenshots) {
|
|
710
|
+
screenshots = data.screenshots;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Populate headlines if available
|
|
714
|
+
if (data.hasHeadlines && data.headlines) {
|
|
715
|
+
approvedTexts = data.headlines;
|
|
716
|
+
suggestions = data.suggestions || approvedTexts.map(h => ({
|
|
717
|
+
description: '',
|
|
718
|
+
options: [h, h, h],
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Jump to the right step
|
|
723
|
+
if (data.skipToStep != null) {
|
|
724
|
+
goStep(data.skipToStep);
|
|
725
|
+
} else if (data.hasHeadlines && data.hasScreenshots) {
|
|
726
|
+
// Everything ready — go to preview
|
|
727
|
+
showPreview();
|
|
728
|
+
} else if (data.hasScreenshots) {
|
|
729
|
+
// Have screenshots, need headlines
|
|
730
|
+
goStep(4);
|
|
731
|
+
buildHeadlineDots();
|
|
732
|
+
showHeadline();
|
|
733
|
+
} else {
|
|
734
|
+
// Have brand, need screenshots
|
|
735
|
+
goStep(1);
|
|
736
|
+
}
|
|
737
|
+
} catch {}
|
|
738
|
+
})();
|