|
1 | 1 | <script> |
2 | 2 | import { onMount } from 'svelte'; |
3 | | - import { Plus, Search, Edit, Trash2, FolderOpen, Save, X, Check, RefreshCw, ToggleLeft, FileJson } from 'lucide-svelte'; |
| 3 | + import { Plus, Search, Edit, Trash2, FolderOpen, Save, X, Check, RefreshCw, ToggleLeft, FileJson, Loader2 } from 'lucide-svelte'; |
4 | 4 | import { CheckAppInstalled, GetApp, GetApps, OpenConfigFolder, SaveApp, DeleteApp, SelectExeFromLocalPrograms, SelectFolderFromHome, SelectFolderFromRoaming, SelectFolderFromLocalPrograms, ToggleApp, GetPlatformInfo } from '../../wailsjs/go/main/App.js'; |
5 | 5 | import { confirm } from './ConfirmModal.svelte'; |
6 | 6 | import { toast } from './Toast.svelte'; |
|
12 | 12 | let apps = []; |
13 | 13 | let loading = false; |
14 | 14 | let platformInfo = {}; |
| 15 | +
|
| 16 | + // Loading states |
| 17 | + let loadingSave = false; |
| 18 | + let loadingDelete = {}; // { appKey: boolean } |
| 19 | + let loadingToggle = {}; // { appKey: boolean } |
15 | 20 | |
16 | 21 | // Context menu state |
17 | 22 | let contextMenu = { show: false, x: 0, y: 0, app: null }; |
|
86 | 91 | } |
87 | 92 |
|
88 | 93 | async function handleToggle(app) { |
| 94 | + if (loadingToggle[app.app_name]) return; |
| 95 | +
|
| 96 | + loadingToggle = { ...loadingToggle, [app.app_name]: true }; |
89 | 97 | try { |
90 | 98 | await ToggleApp(app.app_name); |
91 | 99 | log(`Toggled ${app.display_name}: ${app.active ? 'Inactive' : 'Active'}`); |
92 | 100 | await loadApps(); |
93 | 101 | } catch (e) { |
94 | 102 | log(`Error: ${e}`); |
| 103 | + } finally { |
| 104 | + loadingToggle = { ...loadingToggle, [app.app_name]: false }; |
95 | 105 | } |
96 | 106 | } |
97 | 107 |
|
98 | 108 | async function handleDelete(app) { |
| 109 | + if (loadingDelete[app.app_name]) return; |
| 110 | +
|
99 | 111 | if ($settings.confirmBeforeDelete) { |
100 | 112 | const confirmed = await confirm.delete(app.display_name); |
101 | 113 | if (!confirmed) return; |
102 | 114 | } |
103 | | - |
| 115 | +
|
| 116 | + loadingDelete = { ...loadingDelete, [app.app_name]: true }; |
104 | 117 | try { |
105 | 118 | await DeleteApp(app.app_name); |
106 | 119 | log(`Deleted: ${app.display_name}`); |
107 | 120 | await loadApps(); |
108 | 121 | } catch (e) { |
109 | 122 | log(`Error: ${e}`); |
| 123 | + } finally { |
| 124 | + loadingDelete = { ...loadingDelete, [app.app_name]: false }; |
110 | 125 | } |
111 | 126 | } |
112 | 127 |
|
|
319 | 334 | } |
320 | 335 |
|
321 | 336 | async function saveNewApp() { |
| 337 | + if (loadingSave) return; |
| 338 | +
|
322 | 339 | if (!newApp.app_name || !newApp.exe_path || !newApp.data_path) { |
323 | 340 | alert('Please fill in all required fields'); |
324 | 341 | return; |
|
362 | 379 | addon_backup_paths: newApp.addon_paths |
363 | 380 | }; |
364 | 381 |
|
| 382 | + loadingSave = true; |
365 | 383 | try { |
366 | 384 | await SaveApp(config); |
367 | 385 | log(`${dialogMode === 'edit' ? 'Updated' : 'Added'}: ${config.display_name}`); |
| 386 | + toast.success(`${dialogMode === 'edit' ? 'Updated' : 'Added'} ${config.display_name}`, 3000); |
368 | 387 | showAddDialog = false; |
369 | 388 | resetNewApp(); |
370 | 389 | await loadApps(); |
371 | 390 | } catch (e) { |
372 | 391 | log(`Error: ${e}`); |
| 392 | + toast.error(`Failed to save: ${e}`, 5000); |
| 393 | + } finally { |
| 394 | + loadingSave = false; |
373 | 395 | } |
374 | 396 | } |
375 | 397 |
|
|
501 | 523 | <td class="p-3"> |
502 | 524 | <div class="flex items-center gap-2"> |
503 | 525 | <button |
504 | | - class="px-3 py-1 rounded text-xs font-medium transition-all |
505 | | - {app.active |
506 | | - ? 'bg-[var(--success)]/20 text-[var(--success)] hover:bg-[var(--success)]/30' |
| 526 | + class="px-3 py-1 rounded text-xs font-medium transition-all flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed |
| 527 | + {app.active |
| 528 | + ? 'bg-[var(--success)]/20 text-[var(--success)] hover:bg-[var(--success)]/30' |
507 | 529 | : 'bg-[var(--bg-hover)] text-[var(--text-muted)] hover:bg-[var(--border)]'}" |
508 | 530 | on:click={() => handleToggle(app)} |
| 531 | + disabled={loadingToggle[app.app_name]} |
509 | 532 | > |
| 533 | + {#if loadingToggle[app.app_name]} |
| 534 | + <Loader2 size={12} class="animate-spin" /> |
| 535 | + {/if} |
510 | 536 | {app.active ? 'Active' : 'Inactive'} |
511 | 537 | </button> |
512 | 538 | <button |
|
517 | 543 | <Edit size={14} /> |
518 | 544 | </button> |
519 | 545 | <button |
520 | | - class="p-1.5 rounded text-[var(--text-secondary)] hover:text-[var(--danger)] hover:bg-[var(--bg-hover)] transition-colors" |
| 546 | + class="p-1.5 rounded text-[var(--text-secondary)] hover:text-[var(--danger)] hover:bg-[var(--bg-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
521 | 547 | on:click={() => handleDelete(app)} |
522 | 548 | title="Delete" |
| 549 | + disabled={loadingDelete[app.app_name]} |
523 | 550 | > |
524 | | - <Trash2 size={14} /> |
| 551 | + {#if loadingDelete[app.app_name]} |
| 552 | + <Loader2 size={14} class="animate-spin" /> |
| 553 | + {:else} |
| 554 | + <Trash2 size={14} /> |
| 555 | + {/if} |
525 | 556 | </button> |
526 | 557 | </div> |
527 | 558 | </td> |
|
846 | 877 | </div> |
847 | 878 |
|
848 | 879 | <div class="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--border)]"> |
849 | | - <button |
850 | | - class="px-4 py-2 rounded-lg font-medium bg-[var(--bg-hover)] hover:bg-[var(--border)] border border-[var(--border)] text-[var(--text-secondary)] transition-all" |
| 880 | + <button |
| 881 | + class="px-4 py-2 rounded-lg font-medium bg-[var(--bg-hover)] hover:bg-[var(--border)] border border-[var(--border)] text-[var(--text-secondary)] transition-all disabled:opacity-50 disabled:cursor-not-allowed" |
851 | 882 | on:click={() => { showAddDialog = false; resetNewApp(); }} |
| 883 | + disabled={loadingSave} |
852 | 884 | > |
853 | 885 | Cancel |
854 | 886 | </button> |
855 | | - <button |
856 | | - class="px-4 py-2 rounded-lg font-medium bg-[var(--primary)] hover:bg-[var(--primary-light)] hover:text-black text-white transition-all" |
| 887 | + <button |
| 888 | + class="px-4 py-2 rounded-lg font-medium bg-[var(--primary)] hover:bg-[var(--primary-light)] hover:text-black text-white transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" |
857 | 889 | on:click={saveNewApp} |
| 890 | + disabled={loadingSave} |
858 | 891 | > |
859 | | - Save |
| 892 | + {#if loadingSave} |
| 893 | + <Loader2 size={16} class="animate-spin" /> |
| 894 | + Saving... |
| 895 | + {:else} |
| 896 | + Save |
| 897 | + {/if} |
860 | 898 | </button> |
861 | 899 | </div> |
862 | 900 | </div> |
|
0 commit comments