decksmith 0.1.15__py3-none-any.whl → 0.9.2__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.
@@ -0,0 +1,583 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // --- Initialization ---
3
+
4
+ // Initialize Split.js
5
+ Split(['#left-pane', '#right-pane'], {
6
+ sizes: [50, 50],
7
+ minSize: 200,
8
+ gutterSize: 10,
9
+ cursor: 'col-resize'
10
+ });
11
+
12
+ Split(['#yaml-pane', '#csv-pane'], {
13
+ direction: 'vertical',
14
+ sizes: [50, 50],
15
+ minSize: 100,
16
+ gutterSize: 10,
17
+ cursor: 'row-resize'
18
+ });
19
+
20
+ // Initialize Ace Editors
21
+ const yamlEditor = ace.edit("yaml-editor");
22
+ yamlEditor.setTheme("ace/theme/tomorrow_night");
23
+ yamlEditor.session.setMode("ace/mode/yaml");
24
+ yamlEditor.setOptions({
25
+ fontSize: "12pt",
26
+ showPrintMargin: false,
27
+ });
28
+
29
+ const csvEditor = ace.edit("csv-editor");
30
+ csvEditor.setTheme("ace/theme/tomorrow_night");
31
+ csvEditor.session.setMode("ace/mode/text");
32
+ csvEditor.setOptions({
33
+ fontSize: "12pt",
34
+ showPrintMargin: false,
35
+ });
36
+
37
+ // --- Elements ---
38
+ const elements = {
39
+ cardSelector: document.getElementById('card-selector'),
40
+ buildBtn: document.getElementById('build-btn'),
41
+ exportBtn: document.getElementById('export-btn'),
42
+ previewImage: document.getElementById('preview-image'),
43
+ placeholderText: document.getElementById('placeholder-text'),
44
+ loadingIndicator: document.getElementById('loading-indicator'),
45
+ statusBar: document.getElementById('status-bar'),
46
+ statusText: document.getElementById('status-text'),
47
+ statusSpinner: document.getElementById('status-spinner'),
48
+ statusIcon: document.getElementById('status-icon'),
49
+ statusLine: document.getElementById('status-line'),
50
+ toastContainer: document.getElementById('toast-container'),
51
+
52
+ // Project controls
53
+ currentProjectPath: document.getElementById('current-project-path'),
54
+ openProjectBtn: document.getElementById('open-project-btn'),
55
+ newProjectBtn: document.getElementById('new-project-btn'),
56
+ closeProjectBtn: document.getElementById('close-project-btn'),
57
+ pathModal: document.getElementById('path-modal'),
58
+ modalTitle: document.getElementById('modal-title'),
59
+ projectPathLabel: document.getElementById('project-path-label'),
60
+ projectPathInput: document.getElementById('project-path-input'),
61
+ projectNameGroup: document.getElementById('project-name-group'),
62
+ projectNameInput: document.getElementById('project-name-input'),
63
+ modalCancelBtn: document.getElementById('modal-cancel-btn'),
64
+ modalConfirmBtn: document.getElementById('modal-confirm-btn'),
65
+
66
+ // Welcome screen & Browse
67
+ welcomeScreen: document.getElementById('welcome-screen'),
68
+ welcomeOpenBtn: document.getElementById('welcome-open-btn'),
69
+ welcomeNewBtn: document.getElementById('welcome-new-btn'),
70
+ browseBtn: document.getElementById('browse-btn'),
71
+
72
+ // Shutdown Screen
73
+ shutdownScreen: document.getElementById('shutdown-screen'),
74
+ shutdownReason: document.getElementById('shutdown-reason'),
75
+
76
+ // Export Modal
77
+ exportModal: document.getElementById('export-modal'),
78
+ exportFilename: document.getElementById('export-filename'),
79
+ exportPageSize: document.getElementById('export-page-size'),
80
+ exportWidth: document.getElementById('export-width'),
81
+ exportHeight: document.getElementById('export-height'),
82
+ exportGap: document.getElementById('export-gap'),
83
+ exportMarginX: document.getElementById('export-margin-x'),
84
+ exportMarginY: document.getElementById('export-margin-y'),
85
+ exportCancelBtn: document.getElementById('export-cancel-btn'),
86
+ exportConfirmBtn: document.getElementById('export-confirm-btn'),
87
+ };
88
+
89
+ // --- State ---
90
+ let state = {
91
+ currentCardIndex: -1,
92
+ isDirty: false,
93
+ modalMode: 'open', // 'open' or 'new'
94
+ isPreviewing: false,
95
+ pendingPreview: false,
96
+ isProjectOpen: false
97
+ };
98
+
99
+ // --- UI Helpers ---
100
+
101
+ function showShutdownScreen(reason) {
102
+ elements.shutdownReason.textContent = reason || 'The DeckSmith service stopped.';
103
+ elements.shutdownScreen.classList.remove('hidden');
104
+ }
105
+
106
+ function showNotification(message, type = 'info') {
107
+ const toast = document.createElement('div');
108
+ toast.className = `toast ${type}`;
109
+ toast.textContent = message;
110
+
111
+ elements.toastContainer.appendChild(toast);
112
+
113
+ requestAnimationFrame(() => {
114
+ toast.classList.add('show');
115
+ });
116
+
117
+ setTimeout(() => {
118
+ toast.classList.remove('show');
119
+ setTimeout(() => {
120
+ toast.remove();
121
+ }, 300);
122
+ }, 3000);
123
+ }
124
+
125
+ function updateStatusLine(status) {
126
+ elements.statusLine.className = 'status-line';
127
+ if (status) {
128
+ elements.statusLine.classList.add(status);
129
+ }
130
+ }
131
+
132
+ function setStatus(message, type = null) {
133
+ elements.statusText.textContent = message;
134
+
135
+ // Reset status bar state
136
+ elements.statusBar.classList.remove('processing', 'success');
137
+ elements.statusSpinner.classList.add('hidden');
138
+ elements.statusIcon.classList.remove('hidden');
139
+
140
+ if (type === 'processing') {
141
+ elements.statusBar.classList.add('processing');
142
+ elements.statusSpinner.classList.remove('hidden');
143
+ elements.statusIcon.classList.add('hidden');
144
+ updateStatusLine('loading');
145
+ } else if (type === 'success') {
146
+ elements.statusBar.classList.add('success');
147
+ updateStatusLine('success');
148
+ } else {
149
+ if (type) updateStatusLine(type);
150
+ }
151
+ }
152
+
153
+ // --- API Interactions ---
154
+
155
+ function loadCurrentProject() {
156
+ fetch('/api/project/current')
157
+ .then(response => response.json())
158
+ .then(data => {
159
+ if (data.path) {
160
+ state.isProjectOpen = true;
161
+ elements.currentProjectPath.textContent = data.path;
162
+ elements.currentProjectPath.title = data.path;
163
+ elements.welcomeScreen.classList.add('hidden');
164
+ loadInitialData();
165
+ } else {
166
+ state.isProjectOpen = false;
167
+ elements.currentProjectPath.textContent = 'No project selected';
168
+ elements.welcomeScreen.classList.remove('hidden');
169
+ }
170
+ })
171
+ .catch(err => console.error('Error loading project path:', err));
172
+ }
173
+
174
+ function loadInitialData() {
175
+ fetch('/api/load')
176
+ .then(response => response.json())
177
+ .then(data => {
178
+ yamlEditor.setValue(data.yaml || '', -1);
179
+ csvEditor.setValue(data.csv || '', -1);
180
+ loadCards();
181
+ })
182
+ .catch(err => console.error('Error loading data:', err));
183
+ }
184
+
185
+ function loadCards() {
186
+ fetch('/api/cards')
187
+ .then(response => response.json())
188
+ .then(cards => {
189
+ elements.cardSelector.innerHTML = '<option value="-1">Select a card...</option>';
190
+ cards.forEach((card, index) => {
191
+ const option = document.createElement('option');
192
+ option.value = index;
193
+ const label = card.Name || card.name || card.Title || card.title || `Card ${index + 1}`;
194
+ option.textContent = `${index + 1}: ${label}`;
195
+ elements.cardSelector.appendChild(option);
196
+ });
197
+
198
+ if (state.currentCardIndex >= 0 && state.currentCardIndex < cards.length) {
199
+ elements.cardSelector.value = state.currentCardIndex;
200
+ } else if (cards.length > 0) {
201
+ elements.cardSelector.value = 0;
202
+ updatePreview();
203
+ }
204
+ })
205
+ .catch(err => console.error('Error loading cards:', err));
206
+ }
207
+
208
+ function updatePreview() {
209
+ const index = parseInt(elements.cardSelector.value);
210
+ if (index === -1) {
211
+ elements.previewImage.classList.add('hidden');
212
+ elements.placeholderText.classList.remove('hidden');
213
+ return;
214
+ }
215
+
216
+ if (state.isPreviewing) {
217
+ state.pendingPreview = true;
218
+ return;
219
+ }
220
+
221
+ state.isDirty = false;
222
+ state.currentCardIndex = index;
223
+ state.isPreviewing = true;
224
+ state.pendingPreview = false;
225
+
226
+ elements.loadingIndicator.classList.remove('hidden');
227
+ elements.placeholderText.classList.add('hidden');
228
+ setStatus('Generating preview...', 'loading');
229
+
230
+ const payload = {
231
+ yaml: yamlEditor.getValue(),
232
+ csv: csvEditor.getValue()
233
+ };
234
+
235
+ fetch(`/api/preview/${index}`, {
236
+ method: 'POST',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ body: JSON.stringify(payload)
239
+ })
240
+ .then(response => {
241
+ if (!response.ok) {
242
+ return response.json().then(err => { throw new Error(err.error || 'Preview failed'); });
243
+ }
244
+ return response.blob();
245
+ })
246
+ .then(blob => {
247
+ const url = URL.createObjectURL(blob);
248
+ elements.previewImage.src = url;
249
+ elements.previewImage.onload = () => {
250
+ elements.loadingIndicator.classList.add('hidden');
251
+ elements.previewImage.classList.remove('hidden');
252
+ setStatus('Preview updated');
253
+ updateStatusLine('success');
254
+ };
255
+ })
256
+ .catch(err => {
257
+ elements.loadingIndicator.classList.add('hidden');
258
+ setStatus(`Error: ${err.message}`, 'error');
259
+ console.error(err);
260
+ })
261
+ .finally(() => {
262
+ state.isPreviewing = false;
263
+ if (state.pendingPreview) {
264
+ updatePreview();
265
+ }
266
+ });
267
+ }
268
+
269
+ function autoSave() {
270
+ if (!state.isProjectOpen) return;
271
+
272
+ if (!elements.statusText.textContent.includes('preview')) {
273
+ setStatus('Saving...');
274
+ }
275
+
276
+ const payload = {
277
+ yaml: yamlEditor.getValue(),
278
+ csv: csvEditor.getValue()
279
+ };
280
+
281
+ fetch('/api/save', {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify(payload)
285
+ })
286
+ .then(response => response.json())
287
+ .then(data => {
288
+ if (!elements.statusText.textContent.includes('preview')) {
289
+ setStatus('Saved');
290
+ }
291
+ loadCards();
292
+ })
293
+ .catch(err => {
294
+ setStatus('Error saving');
295
+ console.error(err);
296
+ });
297
+ }
298
+
299
+ function buildDeck() {
300
+ setStatus('Building deck...', 'processing');
301
+ const payload = {
302
+ yaml: yamlEditor.getValue(),
303
+ csv: csvEditor.getValue()
304
+ };
305
+
306
+ fetch('/api/build', {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify(payload)
310
+ })
311
+ .then(response => response.json())
312
+ .then(data => {
313
+ if (data.error) throw new Error(data.error);
314
+ setStatus(data.message, 'success');
315
+ })
316
+ .catch(err => {
317
+ setStatus('Build failed', 'error');
318
+ showNotification('Build failed: ' + err.message, 'error');
319
+ });
320
+ }
321
+
322
+ function showExportModal() {
323
+ elements.exportModal.classList.remove('hidden');
324
+ }
325
+
326
+ function hideExportModal() {
327
+ elements.exportModal.classList.add('hidden');
328
+ }
329
+
330
+ function handleExportConfirm() {
331
+ const params = {
332
+ filename: elements.exportFilename.value,
333
+ page_size: elements.exportPageSize.value,
334
+ width: parseFloat(elements.exportWidth.value),
335
+ height: parseFloat(elements.exportHeight.value),
336
+ gap: parseFloat(elements.exportGap.value),
337
+ margin_x: parseFloat(elements.exportMarginX.value),
338
+ margin_y: parseFloat(elements.exportMarginY.value)
339
+ };
340
+
341
+ hideExportModal();
342
+ exportPdf(params);
343
+ }
344
+
345
+ function exportPdf(params) {
346
+ setStatus('Exporting PDF...', 'processing');
347
+ fetch('/api/export', {
348
+ method: 'POST',
349
+ headers: { 'Content-Type': 'application/json' },
350
+ body: JSON.stringify(params)
351
+ })
352
+ .then(response => response.json())
353
+ .then(data => {
354
+ if (data.error) throw new Error(data.error);
355
+ setStatus(data.message, 'success');
356
+ })
357
+ .catch(err => {
358
+ setStatus('Export failed', 'error');
359
+ showNotification('Export failed: ' + err.message, 'error');
360
+ });
361
+ }
362
+
363
+ // --- Project Management ---
364
+
365
+ function showModal(mode) {
366
+ state.modalMode = mode;
367
+ elements.modalTitle.textContent = mode === 'open' ? 'Open Project' : 'New Project';
368
+ elements.projectPathInput.value = '';
369
+ elements.projectNameInput.value = '';
370
+ elements.pathModal.classList.remove('hidden');
371
+
372
+ if (mode === 'new') {
373
+ elements.projectPathLabel.textContent = 'Store project in:';
374
+ elements.projectNameGroup.classList.remove('hidden');
375
+ elements.projectPathInput.placeholder = 'Loading default path...';
376
+
377
+ fetch('/api/system/default-path')
378
+ .then(response => response.json())
379
+ .then(data => {
380
+ if (data.path) {
381
+ elements.projectPathInput.value = data.path;
382
+ }
383
+ })
384
+ .catch(err => console.error('Error fetching default path:', err));
385
+
386
+ elements.projectNameInput.focus();
387
+ } else {
388
+ elements.projectPathLabel.textContent = 'Folder Path:';
389
+ elements.projectNameGroup.classList.add('hidden');
390
+ elements.projectPathInput.placeholder = 'e.g. C:/Projects/MyDeck';
391
+ elements.projectPathInput.focus();
392
+ }
393
+ }
394
+
395
+ function hideModal() {
396
+ elements.pathModal.classList.add('hidden');
397
+ }
398
+
399
+ function browseFolder() {
400
+ fetch('/api/system/browse', { method: 'POST' })
401
+ .then(response => response.json())
402
+ .then(data => {
403
+ if (data.path) {
404
+ elements.projectPathInput.value = data.path;
405
+ }
406
+ })
407
+ .catch(err => console.error('Error browsing:', err));
408
+ }
409
+
410
+ function openProject(path) {
411
+ setStatus('Opening project...');
412
+ fetch('/api/project/select', {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify({ path: path })
416
+ })
417
+ .then(response => response.json())
418
+ .then(data => {
419
+ if (data.error) throw new Error(data.error);
420
+
421
+ state.isProjectOpen = true;
422
+ elements.currentProjectPath.textContent = data.path;
423
+ elements.currentProjectPath.title = data.path;
424
+
425
+ // showNotification('Project opened', 'success');
426
+ setStatus('Ready');
427
+
428
+ loadInitialData();
429
+ elements.welcomeScreen.classList.add('hidden');
430
+ })
431
+ .catch(err => {
432
+ showNotification(err.message, 'error');
433
+ setStatus('Error', 'error');
434
+ });
435
+ }
436
+
437
+ function handleDirectOpen() {
438
+ fetch('/api/system/browse', { method: 'POST' })
439
+ .then(response => response.json())
440
+ .then(data => {
441
+ if (data.path) {
442
+ openProject(data.path);
443
+ }
444
+ })
445
+ .catch(err => console.error('Error browsing:', err));
446
+ }
447
+
448
+ function handleProjectAction() {
449
+ let path = elements.projectPathInput.value.trim();
450
+ if (!path) {
451
+ showNotification('Please enter a path', 'error');
452
+ return;
453
+ }
454
+
455
+ if (state.modalMode === 'new') {
456
+ const name = elements.projectNameInput.value.trim();
457
+ if (!name) {
458
+ showNotification('Please enter a project name', 'error');
459
+ return;
460
+ }
461
+ const separator = path.includes('\\') ? '\\' : '/';
462
+ path = path.endsWith(separator) ? path + name : path + separator + name;
463
+ }
464
+
465
+ const endpoint = state.modalMode === 'open' ? '/api/project/select' : '/api/project/create';
466
+
467
+ setStatus(state.modalMode === 'open' ? 'Opening project...' : 'Creating project...');
468
+
469
+ fetch(endpoint, {
470
+ method: 'POST',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ path: path })
473
+ })
474
+ .then(response => response.json())
475
+ .then(data => {
476
+ if (data.error) throw new Error(data.error);
477
+
478
+ hideModal();
479
+ state.isProjectOpen = true;
480
+ elements.currentProjectPath.textContent = data.path;
481
+ elements.currentProjectPath.title = data.path;
482
+
483
+ // showNotification(state.modalMode === 'open' ? 'Project opened' : 'Project created', 'success');
484
+ setStatus('Ready');
485
+
486
+ // Reload data
487
+ loadInitialData();
488
+ elements.welcomeScreen.classList.add('hidden');
489
+ })
490
+ .catch(err => {
491
+ showNotification(err.message, 'error');
492
+ setStatus('Error', 'error');
493
+ });
494
+ }
495
+
496
+ function closeProject() {
497
+ fetch('/api/project/close', { method: 'POST' })
498
+ .then(response => response.json())
499
+ .then(data => {
500
+ state.isProjectOpen = false;
501
+ elements.currentProjectPath.textContent = 'No project selected';
502
+ elements.currentProjectPath.title = '';
503
+
504
+ // Clear editors
505
+ yamlEditor.setValue('', -1);
506
+ csvEditor.setValue('', -1);
507
+ state.isDirty = false; // Reset dirty flag after clearing editors
508
+
509
+ elements.cardSelector.innerHTML = '<option value="-1">Select a card...</option>';
510
+ elements.previewImage.classList.add('hidden');
511
+ elements.placeholderText.classList.remove('hidden');
512
+
513
+ elements.welcomeScreen.classList.remove('hidden');
514
+ // showNotification('Project closed', 'info');
515
+ })
516
+ .catch(err => console.error('Error closing project:', err));
517
+ }
518
+
519
+ // --- Event Listeners ---
520
+
521
+ elements.cardSelector.addEventListener('change', updatePreview);
522
+ elements.buildBtn.addEventListener('click', buildDeck);
523
+ elements.exportBtn.addEventListener('click', showExportModal);
524
+
525
+ elements.exportCancelBtn.addEventListener('click', hideExportModal);
526
+ elements.exportConfirmBtn.addEventListener('click', handleExportConfirm);
527
+ elements.exportModal.querySelector('.modal-overlay').addEventListener('click', hideExportModal);
528
+
529
+ elements.openProjectBtn.addEventListener('click', handleDirectOpen);
530
+ elements.newProjectBtn.addEventListener('click', () => showModal('new'));
531
+ elements.closeProjectBtn.addEventListener('click', closeProject);
532
+ elements.modalCancelBtn.addEventListener('click', hideModal);
533
+ elements.modalConfirmBtn.addEventListener('click', handleProjectAction);
534
+ elements.pathModal.querySelector('.modal-overlay').addEventListener('click', hideModal);
535
+
536
+ elements.welcomeOpenBtn.addEventListener('click', handleDirectOpen);
537
+ elements.welcomeNewBtn.addEventListener('click', () => showModal('new'));
538
+ elements.browseBtn.addEventListener('click', browseFolder);
539
+
540
+ const markDirty = () => {
541
+ state.isDirty = true;
542
+ };
543
+
544
+ yamlEditor.session.on('change', markDirty);
545
+ csvEditor.session.on('change', markDirty);
546
+
547
+ // Auto-save loop
548
+ setInterval(() => {
549
+ if (state.isDirty) {
550
+ autoSave();
551
+ if (state.currentCardIndex !== -1) {
552
+ updatePreview();
553
+ } else {
554
+ state.isDirty = false;
555
+ }
556
+ }
557
+ }, 2000);
558
+
559
+ // Start
560
+ loadCurrentProject();
561
+
562
+ // Shutdown
563
+ window.addEventListener('beforeunload', () => {
564
+ navigator.sendBeacon('/api/shutdown');
565
+ });
566
+
567
+ // SSE for Server Events (Shutdown)
568
+ const evtSource = new EventSource("/api/events");
569
+ evtSource.onmessage = (e) => {
570
+ // Ignore keepalive
571
+ if (e.data === ': keepalive') return;
572
+
573
+ try {
574
+ const data = JSON.parse(e.data);
575
+ if (data.type === 'shutdown') {
576
+ showShutdownScreen(data.reason);
577
+ evtSource.close();
578
+ }
579
+ } catch (err) {
580
+ // Ignore parse errors or keepalives that might slip through
581
+ }
582
+ };
583
+ });