decksmith 0.9.1__py3-none-any.whl → 0.9.3__py3-none-any.whl
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.
- decksmith/card_builder.py +66 -35
- decksmith/deck_builder.py +95 -85
- decksmith/export.py +4 -1
- decksmith/gui/app.py +25 -20
- decksmith/gui/static/css/style.css +105 -33
- decksmith/gui/static/img/decksmith.ico +0 -0
- decksmith/gui/static/js/main.js +169 -115
- decksmith/gui/templates/index.html +184 -182
- decksmith/image_ops.py +11 -0
- decksmith/main.py +7 -6
- decksmith/project.py +35 -39
- decksmith/renderers/image.py +4 -6
- decksmith/renderers/text.py +153 -127
- decksmith/validate.py +14 -2
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/METADATA +3 -1
- decksmith-0.9.3.dist-info/RECORD +27 -0
- decksmith-0.9.1.dist-info/RECORD +0 -26
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/WHEEL +0 -0
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/entry_points.txt +0 -0
decksmith/gui/static/js/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
2
2
|
// --- Initialization ---
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
// Initialize Split.js
|
|
5
5
|
Split(['#left-pane', '#right-pane'], {
|
|
6
6
|
sizes: [50, 50],
|
|
@@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
48
48
|
statusIcon: document.getElementById('status-icon'),
|
|
49
49
|
statusLine: document.getElementById('status-line'),
|
|
50
50
|
toastContainer: document.getElementById('toast-container'),
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
// Project controls
|
|
53
53
|
currentProjectPath: document.getElementById('current-project-path'),
|
|
54
54
|
openProjectBtn: document.getElementById('open-project-btn'),
|
|
@@ -62,7 +62,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
62
62
|
projectNameInput: document.getElementById('project-name-input'),
|
|
63
63
|
modalCancelBtn: document.getElementById('modal-cancel-btn'),
|
|
64
64
|
modalConfirmBtn: document.getElementById('modal-confirm-btn'),
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
// Welcome screen & Browse
|
|
67
67
|
welcomeScreen: document.getElementById('welcome-screen'),
|
|
68
68
|
welcomeOpenBtn: document.getElementById('welcome-open-btn'),
|
|
@@ -93,9 +93,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
93
93
|
modalMode: 'open', // 'open' or 'new'
|
|
94
94
|
isPreviewing: false,
|
|
95
95
|
pendingPreview: false,
|
|
96
|
-
isProjectOpen: false
|
|
96
|
+
isProjectOpen: false,
|
|
97
|
+
hasSyntaxError: false
|
|
97
98
|
};
|
|
98
99
|
|
|
100
|
+
// --- Annotation Handler ---
|
|
101
|
+
function checkAnnotations() {
|
|
102
|
+
// Wait slightly for Ace to update annotations
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
const annotations = yamlEditor.getSession().getAnnotations();
|
|
105
|
+
const errors = annotations.filter(a => a.type === 'error');
|
|
106
|
+
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
state.hasSyntaxError = true;
|
|
109
|
+
// Use the first error message
|
|
110
|
+
setStatus(`YAML Syntax Error: ${errors[0].text} (Line ${errors[0].row + 1})`, 'error');
|
|
111
|
+
} else {
|
|
112
|
+
if (state.hasSyntaxError) {
|
|
113
|
+
state.hasSyntaxError = false;
|
|
114
|
+
setStatus('Ready'); // Clear error status
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, 100);
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
// --- UI Helpers ---
|
|
100
121
|
|
|
101
122
|
function showShutdownScreen(reason) {
|
|
@@ -107,9 +128,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
107
128
|
const toast = document.createElement('div');
|
|
108
129
|
toast.className = `toast ${type}`;
|
|
109
130
|
toast.textContent = message;
|
|
110
|
-
|
|
131
|
+
|
|
111
132
|
elements.toastContainer.appendChild(toast);
|
|
112
|
-
|
|
133
|
+
|
|
113
134
|
requestAnimationFrame(() => {
|
|
114
135
|
toast.classList.add('show');
|
|
115
136
|
});
|
|
@@ -131,12 +152,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
131
152
|
|
|
132
153
|
function setStatus(message, type = null) {
|
|
133
154
|
elements.statusText.textContent = message;
|
|
134
|
-
|
|
155
|
+
|
|
135
156
|
// Reset status bar state
|
|
136
|
-
elements.statusBar.classList.remove('processing', 'success');
|
|
157
|
+
elements.statusBar.classList.remove('processing', 'success', 'error');
|
|
137
158
|
elements.statusSpinner.classList.add('hidden');
|
|
138
159
|
elements.statusIcon.classList.remove('hidden');
|
|
139
160
|
|
|
161
|
+
// Reset icon to default
|
|
162
|
+
elements.statusIcon.className = 'fa-solid fa-circle-info fa-fw';
|
|
163
|
+
|
|
140
164
|
if (type === 'processing') {
|
|
141
165
|
elements.statusBar.classList.add('processing');
|
|
142
166
|
elements.statusSpinner.classList.remove('hidden');
|
|
@@ -145,6 +169,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
145
169
|
} else if (type === 'success') {
|
|
146
170
|
elements.statusBar.classList.add('success');
|
|
147
171
|
updateStatusLine('success');
|
|
172
|
+
} else if (type === 'error') {
|
|
173
|
+
elements.statusBar.classList.add('error');
|
|
174
|
+
updateStatusLine('error');
|
|
175
|
+
// Change icon to danger
|
|
176
|
+
elements.statusIcon.className = 'fa-solid fa-triangle-exclamation fa-fw';
|
|
148
177
|
} else {
|
|
149
178
|
if (type) updateStatusLine(type);
|
|
150
179
|
}
|
|
@@ -194,7 +223,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
194
223
|
option.textContent = `${index + 1}: ${label}`;
|
|
195
224
|
elements.cardSelector.appendChild(option);
|
|
196
225
|
});
|
|
197
|
-
|
|
226
|
+
|
|
198
227
|
if (state.currentCardIndex >= 0 && state.currentCardIndex < cards.length) {
|
|
199
228
|
elements.cardSelector.value = state.currentCardIndex;
|
|
200
229
|
} else if (cards.length > 0) {
|
|
@@ -222,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
222
251
|
state.currentCardIndex = index;
|
|
223
252
|
state.isPreviewing = true;
|
|
224
253
|
state.pendingPreview = false;
|
|
225
|
-
|
|
254
|
+
|
|
226
255
|
elements.loadingIndicator.classList.remove('hidden');
|
|
227
256
|
elements.placeholderText.classList.add('hidden');
|
|
228
257
|
setStatus('Generating preview...', 'loading');
|
|
@@ -237,33 +266,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
237
266
|
headers: { 'Content-Type': 'application/json' },
|
|
238
267
|
body: JSON.stringify(payload)
|
|
239
268
|
})
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
.then(response => {
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
return response.json().then(err => { throw new Error(err.error || 'Preview failed'); });
|
|
272
|
+
}
|
|
273
|
+
return response.blob();
|
|
274
|
+
})
|
|
275
|
+
.then(blob => {
|
|
276
|
+
const url = URL.createObjectURL(blob);
|
|
277
|
+
elements.previewImage.src = url;
|
|
278
|
+
elements.previewImage.onload = () => {
|
|
279
|
+
elements.loadingIndicator.classList.add('hidden');
|
|
280
|
+
elements.previewImage.classList.remove('hidden');
|
|
281
|
+
setStatus('Preview updated');
|
|
282
|
+
updateStatusLine('success');
|
|
283
|
+
};
|
|
284
|
+
})
|
|
285
|
+
.catch(err => {
|
|
250
286
|
elements.loadingIndicator.classList.add('hidden');
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})
|
|
261
|
-
.finally(() => {
|
|
262
|
-
state.isPreviewing = false;
|
|
263
|
-
if (state.pendingPreview) {
|
|
264
|
-
updatePreview();
|
|
265
|
-
}
|
|
266
|
-
});
|
|
287
|
+
setStatus(`Error: ${err.message}`, 'error');
|
|
288
|
+
console.error(err);
|
|
289
|
+
})
|
|
290
|
+
.finally(() => {
|
|
291
|
+
state.isPreviewing = false;
|
|
292
|
+
if (state.pendingPreview) {
|
|
293
|
+
updatePreview();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
267
296
|
}
|
|
268
297
|
|
|
269
298
|
function autoSave() {
|
|
@@ -272,7 +301,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
272
301
|
if (!elements.statusText.textContent.includes('preview')) {
|
|
273
302
|
setStatus('Saving...');
|
|
274
303
|
}
|
|
275
|
-
|
|
304
|
+
|
|
276
305
|
const payload = {
|
|
277
306
|
yaml: yamlEditor.getValue(),
|
|
278
307
|
csv: csvEditor.getValue()
|
|
@@ -283,17 +312,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
283
312
|
headers: { 'Content-Type': 'application/json' },
|
|
284
313
|
body: JSON.stringify(payload)
|
|
285
314
|
})
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
setStatus('Saved');
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
315
|
+
.then(response => response.json())
|
|
316
|
+
.then(data => {
|
|
317
|
+
// if (!elements.statusText.textContent.includes('preview')) {
|
|
318
|
+
// setStatus('Saved');
|
|
319
|
+
// }
|
|
320
|
+
loadCards();
|
|
321
|
+
})
|
|
322
|
+
.catch(err => {
|
|
323
|
+
setStatus('Error saving');
|
|
324
|
+
console.error(err);
|
|
325
|
+
});
|
|
297
326
|
}
|
|
298
327
|
|
|
299
328
|
function buildDeck() {
|
|
@@ -302,21 +331,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
302
331
|
yaml: yamlEditor.getValue(),
|
|
303
332
|
csv: csvEditor.getValue()
|
|
304
333
|
};
|
|
305
|
-
|
|
306
|
-
fetch('/api/build', {
|
|
334
|
+
|
|
335
|
+
return fetch('/api/build', {
|
|
307
336
|
method: 'POST',
|
|
308
337
|
headers: { 'Content-Type': 'application/json' },
|
|
309
338
|
body: JSON.stringify(payload)
|
|
310
339
|
})
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
340
|
+
.then(response => response.json())
|
|
341
|
+
.then(data => {
|
|
342
|
+
if (data.error) throw new Error(data.error);
|
|
343
|
+
setStatus(data.message, 'success');
|
|
344
|
+
showNotification(data.message, 'success');
|
|
345
|
+
return data;
|
|
346
|
+
})
|
|
347
|
+
.catch(err => {
|
|
348
|
+
setStatus('Build failed: ' + err.message, 'error');
|
|
349
|
+
showNotification('Build failed: ' + err.message, 'error');
|
|
350
|
+
throw err;
|
|
351
|
+
});
|
|
320
352
|
}
|
|
321
353
|
|
|
322
354
|
function showExportModal() {
|
|
@@ -339,7 +371,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
339
371
|
};
|
|
340
372
|
|
|
341
373
|
hideExportModal();
|
|
342
|
-
|
|
374
|
+
|
|
375
|
+
// Chain build then export
|
|
376
|
+
buildDeck()
|
|
377
|
+
.then(() => {
|
|
378
|
+
exportPdf(params);
|
|
379
|
+
})
|
|
380
|
+
.catch(() => {
|
|
381
|
+
// Build failed, error already handled by buildDeck UI updates
|
|
382
|
+
console.log('Export cancelled due to build failure');
|
|
383
|
+
});
|
|
343
384
|
}
|
|
344
385
|
|
|
345
386
|
function exportPdf(params) {
|
|
@@ -349,15 +390,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
349
390
|
headers: { 'Content-Type': 'application/json' },
|
|
350
391
|
body: JSON.stringify(params)
|
|
351
392
|
})
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
393
|
+
.then(response => response.json())
|
|
394
|
+
.then(data => {
|
|
395
|
+
if (data.error) throw new Error(data.error);
|
|
396
|
+
setStatus(data.message, 'success');
|
|
397
|
+
showNotification(data.message, 'success');
|
|
398
|
+
})
|
|
399
|
+
.catch(err => {
|
|
400
|
+
setStatus('Export failed: ' + err.message, 'error');
|
|
401
|
+
showNotification('Export failed: ' + err.message, 'error');
|
|
402
|
+
});
|
|
361
403
|
}
|
|
362
404
|
|
|
363
405
|
// --- Project Management ---
|
|
@@ -367,6 +409,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
367
409
|
elements.modalTitle.textContent = mode === 'open' ? 'Open Project' : 'New Project';
|
|
368
410
|
elements.projectPathInput.value = '';
|
|
369
411
|
elements.projectNameInput.value = '';
|
|
412
|
+
elements.projectPathInput.classList.remove('input-error');
|
|
413
|
+
elements.projectNameInput.classList.remove('input-error');
|
|
370
414
|
elements.pathModal.classList.remove('hidden');
|
|
371
415
|
|
|
372
416
|
if (mode === 'new') {
|
|
@@ -414,24 +458,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
414
458
|
headers: { 'Content-Type': 'application/json' },
|
|
415
459
|
body: JSON.stringify({ path: path })
|
|
416
460
|
})
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
461
|
+
.then(response => response.json())
|
|
462
|
+
.then(data => {
|
|
463
|
+
if (data.error) throw new Error(data.error);
|
|
464
|
+
|
|
465
|
+
state.isProjectOpen = true;
|
|
466
|
+
elements.currentProjectPath.textContent = data.path;
|
|
467
|
+
elements.currentProjectPath.title = data.path;
|
|
468
|
+
|
|
469
|
+
// showNotification('Project opened', 'success');
|
|
470
|
+
setStatus('Ready');
|
|
471
|
+
|
|
472
|
+
loadInitialData();
|
|
473
|
+
elements.welcomeScreen.classList.add('hidden');
|
|
474
|
+
})
|
|
475
|
+
.catch(err => {
|
|
476
|
+
setStatus('Error: ' + err.message, 'error');
|
|
477
|
+
});
|
|
435
478
|
}
|
|
436
479
|
|
|
437
480
|
function handleDirectOpen() {
|
|
@@ -445,17 +488,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
445
488
|
.catch(err => console.error('Error browsing:', err));
|
|
446
489
|
}
|
|
447
490
|
|
|
491
|
+
// Clear error on input
|
|
492
|
+
elements.projectPathInput.addEventListener('input', () => {
|
|
493
|
+
elements.projectPathInput.classList.remove('input-error');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
elements.projectNameInput.addEventListener('input', () => {
|
|
497
|
+
elements.projectNameInput.classList.remove('input-error');
|
|
498
|
+
});
|
|
499
|
+
|
|
448
500
|
function handleProjectAction() {
|
|
449
501
|
let path = elements.projectPathInput.value.trim();
|
|
450
502
|
if (!path) {
|
|
451
|
-
|
|
503
|
+
elements.projectPathInput.classList.add('input-error');
|
|
504
|
+
// showNotification('Please enter a path', 'error');
|
|
452
505
|
return;
|
|
453
506
|
}
|
|
454
507
|
|
|
455
508
|
if (state.modalMode === 'new') {
|
|
456
509
|
const name = elements.projectNameInput.value.trim();
|
|
457
510
|
if (!name) {
|
|
458
|
-
|
|
511
|
+
elements.projectNameInput.classList.add('input-error');
|
|
512
|
+
// showNotification('Please enter a project name', 'error');
|
|
459
513
|
return;
|
|
460
514
|
}
|
|
461
515
|
const separator = path.includes('\\') ? '\\' : '/';
|
|
@@ -463,7 +517,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
463
517
|
}
|
|
464
518
|
|
|
465
519
|
const endpoint = state.modalMode === 'open' ? '/api/project/select' : '/api/project/create';
|
|
466
|
-
|
|
520
|
+
|
|
467
521
|
setStatus(state.modalMode === 'open' ? 'Opening project...' : 'Creating project...');
|
|
468
522
|
|
|
469
523
|
fetch(endpoint, {
|
|
@@ -471,26 +525,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
471
525
|
headers: { 'Content-Type': 'application/json' },
|
|
472
526
|
body: JSON.stringify({ path: path })
|
|
473
527
|
})
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
});
|
|
528
|
+
.then(response => response.json())
|
|
529
|
+
.then(data => {
|
|
530
|
+
if (data.error) throw new Error(data.error);
|
|
531
|
+
|
|
532
|
+
hideModal();
|
|
533
|
+
state.isProjectOpen = true;
|
|
534
|
+
elements.currentProjectPath.textContent = data.path;
|
|
535
|
+
elements.currentProjectPath.title = data.path;
|
|
536
|
+
|
|
537
|
+
// showNotification(state.modalMode === 'open' ? 'Project opened' : 'Project created', 'success');
|
|
538
|
+
setStatus('Ready');
|
|
539
|
+
|
|
540
|
+
// Reload data
|
|
541
|
+
loadInitialData();
|
|
542
|
+
elements.welcomeScreen.classList.add('hidden');
|
|
543
|
+
})
|
|
544
|
+
.catch(err => {
|
|
545
|
+
setStatus('Error: ' + err.message, 'error');
|
|
546
|
+
});
|
|
494
547
|
}
|
|
495
548
|
|
|
496
549
|
function closeProject() {
|
|
@@ -500,16 +553,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
500
553
|
state.isProjectOpen = false;
|
|
501
554
|
elements.currentProjectPath.textContent = 'No project selected';
|
|
502
555
|
elements.currentProjectPath.title = '';
|
|
503
|
-
|
|
556
|
+
|
|
504
557
|
// Clear editors
|
|
505
558
|
yamlEditor.setValue('', -1);
|
|
506
559
|
csvEditor.setValue('', -1);
|
|
507
560
|
state.isDirty = false; // Reset dirty flag after clearing editors
|
|
508
|
-
|
|
561
|
+
|
|
509
562
|
elements.cardSelector.innerHTML = '<option value="-1">Select a card...</option>';
|
|
510
563
|
elements.previewImage.classList.add('hidden');
|
|
511
564
|
elements.placeholderText.classList.remove('hidden');
|
|
512
|
-
|
|
565
|
+
|
|
513
566
|
elements.welcomeScreen.classList.remove('hidden');
|
|
514
567
|
// showNotification('Project closed', 'info');
|
|
515
568
|
})
|
|
@@ -542,6 +595,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
542
595
|
};
|
|
543
596
|
|
|
544
597
|
yamlEditor.session.on('change', markDirty);
|
|
598
|
+
yamlEditor.session.on('changeAnnotation', checkAnnotations);
|
|
545
599
|
csvEditor.session.on('change', markDirty);
|
|
546
600
|
|
|
547
601
|
// Auto-save loop
|
|
@@ -569,7 +623,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
569
623
|
evtSource.onmessage = (e) => {
|
|
570
624
|
// Ignore keepalive
|
|
571
625
|
if (e.data === ': keepalive') return;
|
|
572
|
-
|
|
626
|
+
|
|
573
627
|
try {
|
|
574
628
|
const data = JSON.parse(e.data);
|
|
575
629
|
if (data.type === 'shutdown') {
|