decksmith 0.9.2__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 +22 -1
- decksmith/gui/static/js/main.js +169 -115
- 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.2.dist-info → decksmith-0.9.3.dist-info}/METADATA +1 -1
- decksmith-0.9.3.dist-info/RECORD +27 -0
- decksmith-0.9.2.dist-info/RECORD +0 -27
- {decksmith-0.9.2.dist-info → decksmith-0.9.3.dist-info}/WHEEL +0 -0
- {decksmith-0.9.2.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') {
|
decksmith/image_ops.py
CHANGED
|
@@ -119,3 +119,14 @@ class ImageOps:
|
|
|
119
119
|
if direction == "vertical":
|
|
120
120
|
return img.transpose(Image.FLIP_TOP_BOTTOM)
|
|
121
121
|
return img
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _filter_opacity(img: Image.Image, opacity: int) -> Image.Image:
|
|
125
|
+
opacity = max(opacity, 0)
|
|
126
|
+
opacity = min(opacity, 100)
|
|
127
|
+
|
|
128
|
+
img = img.convert("RGBA")
|
|
129
|
+
alpha = img.split()[3]
|
|
130
|
+
alpha = alpha.point(lambda p: int(p * opacity / 100))
|
|
131
|
+
img.putalpha(alpha)
|
|
132
|
+
return img
|
decksmith/main.py
CHANGED
|
@@ -80,8 +80,7 @@ def build(ctx, output, spec, data):
|
|
|
80
80
|
# pylint: disable=W0718
|
|
81
81
|
except Exception as exc:
|
|
82
82
|
logger.error("(x) Error building deck '%s' from spec '%s':", data, spec)
|
|
83
|
-
logger.error(" %s", exc)
|
|
84
|
-
logger.debug(traceback.format_exc())
|
|
83
|
+
logger.error(" %s\n%s", exc, traceback.format_exc())
|
|
85
84
|
ctx.exit(1)
|
|
86
85
|
|
|
87
86
|
logger.info("(✔) Deck built successfully.")
|
|
@@ -114,7 +113,8 @@ def build(ctx, output, spec, data):
|
|
|
114
113
|
default=[2, 2],
|
|
115
114
|
help="The horizontal and vertical page margins in millimeters.",
|
|
116
115
|
)
|
|
117
|
-
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def export(ctx, image_folder, output, page_size, width, height, gap, margins):
|
|
118
118
|
"""Exports images from a folder to a PDF file."""
|
|
119
119
|
try:
|
|
120
120
|
image_folder_path = Path(image_folder)
|
|
@@ -133,12 +133,13 @@ def export(image_folder, output, page_size, width, height, gap, margins):
|
|
|
133
133
|
exporter.export()
|
|
134
134
|
logger.info("(✔) Successfully exported PDF to %s", output)
|
|
135
135
|
except FileNotFoundError as exc:
|
|
136
|
-
|
|
136
|
+
click.echo(f"(x) {exc}")
|
|
137
|
+
ctx.exit(1)
|
|
137
138
|
# pylint: disable=W0718
|
|
138
139
|
except Exception as exc:
|
|
139
140
|
logger.error("(x) Error exporting images to '%s':", output)
|
|
140
|
-
logger.error(" %s", exc)
|
|
141
|
-
|
|
141
|
+
logger.error(" %s\n%s", exc, traceback.format_exc())
|
|
142
|
+
ctx.exit(1)
|
|
142
143
|
|
|
143
144
|
|
|
144
145
|
if __name__ == "__main__":
|
decksmith/project.py
CHANGED
|
@@ -30,16 +30,12 @@ class ProjectManager:
|
|
|
30
30
|
def create_project(self, path: Path):
|
|
31
31
|
"""
|
|
32
32
|
Creates a new project at the specified path.
|
|
33
|
+
|
|
33
34
|
Args:
|
|
34
|
-
path (Path): The path
|
|
35
|
+
path (Path): The path where the project directory will be created.
|
|
35
36
|
"""
|
|
36
37
|
path.mkdir(parents=True, exist_ok=True)
|
|
37
38
|
|
|
38
|
-
# Copy templates
|
|
39
|
-
# Assuming templates are in decksmith/templates relative to this file
|
|
40
|
-
# But actually they are in decksmith/templates relative to the package root
|
|
41
|
-
# Let's use importlib.resources or relative path from __file__
|
|
42
|
-
# Since we are in decksmith/project.py, templates are in ../templates
|
|
43
39
|
template_dir = Path(__file__).parent / "templates"
|
|
44
40
|
|
|
45
41
|
if not (path / "deck.yaml").exists():
|
|
@@ -50,52 +46,52 @@ class ProjectManager:
|
|
|
50
46
|
|
|
51
47
|
self.working_dir = path
|
|
52
48
|
|
|
53
|
-
def
|
|
49
|
+
def _load_file_or_template(self, filename: str) -> str:
|
|
54
50
|
"""
|
|
55
|
-
|
|
51
|
+
Helper to load a file from the working directory, or fall back to a template.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
filename (str): The name of the file (e.g., "deck.yaml").
|
|
55
|
+
|
|
56
56
|
Returns:
|
|
57
|
-
|
|
57
|
+
str: The content of the file or template.
|
|
58
58
|
"""
|
|
59
59
|
if self.working_dir is None:
|
|
60
|
-
return
|
|
60
|
+
return ""
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
file_path = self.working_dir / filename
|
|
63
|
+
template_path = Path(__file__).parent / "templates" / filename
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
data["csv"] = csv_file.read()
|
|
85
|
-
elif csv_template.exists():
|
|
86
|
-
with open(csv_template, "r", encoding="utf-8") as csv_template_file:
|
|
87
|
-
data["csv"] = csv_template_file.read()
|
|
88
|
-
else:
|
|
89
|
-
data["csv"] = ""
|
|
90
|
-
|
|
91
|
-
return data
|
|
65
|
+
if file_path.exists() and file_path.stat().st_size > 0:
|
|
66
|
+
with open(file_path, "r", encoding="utf-8") as file_object:
|
|
67
|
+
return file_object.read()
|
|
68
|
+
elif template_path.exists():
|
|
69
|
+
with open(template_path, "r", encoding="utf-8") as file_object:
|
|
70
|
+
return file_object.read()
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
def load_files(self) -> Dict[str, str]:
|
|
74
|
+
"""
|
|
75
|
+
Loads the deck.yaml and deck.csv files from the current project.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict[str, str]: A dictionary with keys "yaml" and "csv" containing file contents.
|
|
79
|
+
"""
|
|
80
|
+
return {
|
|
81
|
+
"yaml": self._load_file_or_template("deck.yaml"),
|
|
82
|
+
"csv": self._load_file_or_template("deck.csv"),
|
|
83
|
+
}
|
|
92
84
|
|
|
93
85
|
def save_files(self, yaml_content: Optional[str], csv_content: Optional[str]):
|
|
94
86
|
"""
|
|
95
87
|
Saves the deck.yaml and deck.csv files to the current project.
|
|
88
|
+
|
|
96
89
|
Args:
|
|
97
90
|
yaml_content (Optional[str]): The content of the deck.yaml file.
|
|
98
91
|
csv_content (Optional[str]): The content of the deck.csv file.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If no project is currently selected (working_dir is None).
|
|
99
95
|
"""
|
|
100
96
|
if self.working_dir is None:
|
|
101
97
|
raise ValueError("No project selected")
|
decksmith/renderers/image.py
CHANGED
|
@@ -9,7 +9,6 @@ from typing import Any, Dict, Optional
|
|
|
9
9
|
from PIL import Image
|
|
10
10
|
|
|
11
11
|
from decksmith.image_ops import ImageOps
|
|
12
|
-
from decksmith.logger import logger
|
|
13
12
|
from decksmith.utils import apply_anchor
|
|
14
13
|
|
|
15
14
|
|
|
@@ -46,11 +45,10 @@ class ImageRenderer:
|
|
|
46
45
|
if potential_path.exists():
|
|
47
46
|
path = potential_path
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
48
|
+
if not path.exists():
|
|
49
|
+
raise FileNotFoundError(f"Image not found: {path}")
|
|
50
|
+
|
|
51
|
+
img = Image.open(path)
|
|
54
52
|
|
|
55
53
|
img = ImageOps.apply_filters(img, element.get("filters", {}))
|
|
56
54
|
|