Simplify refresh status layout
Some checks are pending
Validate Decky / web-client (push) Waiting to run
Some checks are pending
Validate Decky / web-client (push) Waiting to run
This commit is contained in:
parent
42ed790956
commit
c5287352fd
3 changed files with 26 additions and 31 deletions
|
|
@ -34,7 +34,7 @@
|
||||||
"category": "Productivity",
|
"category": "Productivity",
|
||||||
"shortDescription": "Nextcloud Deck on your desktop",
|
"shortDescription": "Nextcloud Deck on your desktop",
|
||||||
"longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.",
|
"longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.",
|
||||||
"copyright": "Copyright © 2026 Elijah Kuntz",
|
"copyright": "Copyright (c) 2026 Elijah Kuntz",
|
||||||
"windows": {
|
"windows": {
|
||||||
"webviewInstallMode": {
|
"webviewInstallMode": {
|
||||||
"type": "downloadBootstrapper"
|
"type": "downloadBootstrapper"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Columns3,
|
Columns3,
|
||||||
LayoutList,
|
LayoutList,
|
||||||
LoaderCircle,
|
|
||||||
Minus,
|
Minus,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
|
|
@ -113,7 +112,7 @@
|
||||||
{ id: 106, stackId: 13, title: 'Patho Concept Map', type: 'plain', owner: 'admin', order: 2, color: 'E69A88', done: null }
|
{ id: 106, stackId: 13, title: 'Patho Concept Map', type: 'plain', owner: 'admin', order: 2, color: 'E69A88', done: null }
|
||||||
]}
|
]}
|
||||||
];
|
];
|
||||||
status = 'Homework · 6 cards · Preview data';
|
status = 'Preview data';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!serverUrl || !username) return;
|
if (!serverUrl || !username) return;
|
||||||
|
|
@ -149,7 +148,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateState = 'checking';
|
updateState = 'checking';
|
||||||
updateMessage = 'Checking Forgejo for a newer signed release…';
|
updateMessage = 'Checking Forgejo for a newer signed release...';
|
||||||
updateProgress = 0;
|
updateProgress = 0;
|
||||||
pendingUpdate = null;
|
pendingUpdate = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -170,7 +169,7 @@
|
||||||
async function installUpdate() {
|
async function installUpdate() {
|
||||||
if (!pendingUpdate) return;
|
if (!pendingUpdate) return;
|
||||||
updateState = 'downloading';
|
updateState = 'downloading';
|
||||||
updateMessage = `Downloading Decky ${pendingUpdate.version}…`;
|
updateMessage = `Downloading Decky ${pendingUpdate.version}...`;
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
try {
|
try {
|
||||||
|
|
@ -184,7 +183,7 @@
|
||||||
} else if (event.event === 'Finished') {
|
} else if (event.event === 'Finished') {
|
||||||
updateState = 'installing';
|
updateState = 'installing';
|
||||||
updateProgress = 100;
|
updateProgress = 100;
|
||||||
updateMessage = 'Download verified. Installing and restarting Decky…';
|
updateMessage = 'Download verified. Installing and restarting Decky...';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await relaunch();
|
await relaunch();
|
||||||
|
|
@ -196,7 +195,7 @@
|
||||||
|
|
||||||
async function connect(silent = false) {
|
async function connect(silent = false) {
|
||||||
busy = true;
|
busy = true;
|
||||||
status = 'Connecting to Nextcloud…';
|
status = 'Connecting...';
|
||||||
try {
|
try {
|
||||||
client = await DeckApiClient.connect({ serverUrl, username, appPassword });
|
client = await DeckApiClient.connect({ serverUrl, username, appPassword });
|
||||||
boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt);
|
boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt);
|
||||||
|
|
@ -228,13 +227,12 @@
|
||||||
async function refreshBoard() {
|
async function refreshBoard() {
|
||||||
if (!client || !selectedBoard) return;
|
if (!client || !selectedBoard) return;
|
||||||
busy = true;
|
busy = true;
|
||||||
status = `Loading ${selectedBoard.title}…`;
|
status = 'Refreshing...';
|
||||||
try {
|
try {
|
||||||
stacks = (await client.getStacks(selectedBoard.id))
|
stacks = (await client.getStacks(selectedBoard.id))
|
||||||
.filter((stack) => !stack.deletedAt)
|
.filter((stack) => !stack.deletedAt)
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0);
|
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
||||||
status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status = friendlyError(error);
|
status = friendlyError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -249,7 +247,7 @@
|
||||||
const interactionAtStart = lastInteractionAt;
|
const interactionAtStart = lastInteractionAt;
|
||||||
if (!background) {
|
if (!background) {
|
||||||
busy = true;
|
busy = true;
|
||||||
status = selectedBoard ? `Loading ${selectedBoard.title}…` : 'Refreshing boards…';
|
status = 'Refreshing...';
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt);
|
const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt);
|
||||||
|
|
@ -273,8 +271,7 @@
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return;
|
if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return;
|
||||||
stacks = refreshedStacks;
|
stacks = refreshedStacks;
|
||||||
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0);
|
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
||||||
status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`;
|
status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -503,7 +500,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCard(card: Card) {
|
async function deleteCard(card: Card) {
|
||||||
if (!client || !selectedBoard || !confirm(`Delete “${card.title}�`)) return;
|
if (!client || !selectedBoard || !confirm(`Delete "${card.title}"?`)) return;
|
||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
await client.deleteCard(selectedBoard.id, card.stackId, card.id);
|
await client.deleteCard(selectedBoard.id, card.stackId, card.id);
|
||||||
|
|
@ -675,8 +672,7 @@
|
||||||
} else if (stackId !== null) {
|
} else if (stackId !== null) {
|
||||||
const targetIndex = stacks.findIndex((stack) => stack.id === stackId);
|
const targetIndex = stacks.findIndex((stack) => stack.id === stackId);
|
||||||
stacks = await client.reorderStack(board.id, stackId, targetIndex);
|
stacks = await client.reorderStack(board.id, stackId, targetIndex);
|
||||||
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((item) => !item.archived).length ?? 0), 0);
|
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
||||||
status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
stacks = origin;
|
stacks = origin;
|
||||||
|
|
@ -711,7 +707,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><title>{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'}</title></svelte:head>
|
<svelte:head><title>{selectedBoard ? `${selectedBoard.title} - Decky` : 'Decky'}</title></svelte:head>
|
||||||
|
|
||||||
<div class:sidebar-collapsed={settings.sidebarCollapsed} class="app-shell">
|
<div class:sidebar-collapsed={settings.sidebarCollapsed} class="app-shell">
|
||||||
<header class="window-titlebar" role="presentation" on:mousedown={dragWindow} on:dblclick={() => runWindowAction('maximize')}>
|
<header class="window-titlebar" role="presentation" on:mousedown={dragWindow} on:dblclick={() => runWindowAction('maximize')}>
|
||||||
|
|
@ -731,7 +727,7 @@
|
||||||
<label>Username<input bind:value={username} autocomplete="username" /></label>
|
<label>Username<input bind:value={username} autocomplete="username" /></label>
|
||||||
<label>App password<input type="password" bind:value={appPassword} autocomplete="current-password" /></label>
|
<label>App password<input type="password" bind:value={appPassword} autocomplete="current-password" /></label>
|
||||||
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
|
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
|
||||||
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting…' : 'Connect'}</button>
|
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting...' : 'Connect'}</button>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -759,7 +755,10 @@
|
||||||
<button class:active={orientation === 'horizontal'} title="Horizontal layout" on:click={() => setOrientation('horizontal')}><Columns3 size={18} /><span>Columns</span></button>
|
<button class:active={orientation === 'horizontal'} title="Horizontal layout" on:click={() => setOrientation('horizontal')}><Columns3 size={18} /><span>Columns</span></button>
|
||||||
<button class:active={orientation === 'vertical'} title="Vertical layout" on:click={() => setOrientation('vertical')}><LayoutList size={18} /><span>List</span></button>
|
<button class:active={orientation === 'vertical'} title="Vertical layout" on:click={() => setOrientation('vertical')}><LayoutList size={18} /><span>List</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="refresh-row">
|
||||||
<button class="sidebar-tool" title="Refresh" disabled={!client || busy} on:click={() => refreshFromNextcloud()}><RefreshCw size={18} class={busy ? 'spinning' : ''} /><span>Refresh</span></button>
|
<button class="sidebar-tool" title="Refresh" disabled={!client || busy} on:click={() => refreshFromNextcloud()}><RefreshCw size={18} class={busy ? 'spinning' : ''} /><span>Refresh</span></button>
|
||||||
|
<span class="sync-status" title={status}>{status}</span>
|
||||||
|
</div>
|
||||||
<button class="sidebar-tool" title="Settings" on:click={() => (settingsOpen = true)}><Settings size={18} /><span>Settings</span></button>
|
<button class="sidebar-tool" title="Settings" on:click={() => (settingsOpen = true)}><Settings size={18} /><span>Settings</span></button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -841,10 +840,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="statusbar">
|
|
||||||
{#if busy}<LoaderCircle size={14} class="spinning" />{:else}<span class:connected={Boolean(client)} class="status-dot"></span>{/if}
|
|
||||||
<span>{status}</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if dragGhost}
|
{#if dragGhost}
|
||||||
|
|
@ -867,7 +862,7 @@
|
||||||
<form class="modal" on:submit|preventDefault={saveCard}>
|
<form class="modal" on:submit|preventDefault={saveCard}>
|
||||||
<header><div><small>{editor.mode === 'create' ? 'NEW CARD' : 'EDIT CARD'}</small><h2>{editor.mode === 'create' ? 'Create a card' : editor.card?.title}</h2></div><button type="button" class="icon-button" on:click={() => (editor = null, colorPickerOpen = false)}><X size={20} /></button></header>
|
<header><div><small>{editor.mode === 'create' ? 'NEW CARD' : 'EDIT CARD'}</small><h2>{editor.mode === 'create' ? 'Create a card' : editor.card?.title}</h2></div><button type="button" class="icon-button" on:click={() => (editor = null, colorPickerOpen = false)}><X size={20} /></button></header>
|
||||||
<label>Title<input bind:value={editor.draft.title} maxlength="255" /></label>
|
<label>Title<input bind:value={editor.draft.title} maxlength="255" /></label>
|
||||||
<label>Description<textarea bind:value={editor.draft.description} rows="5" placeholder="Add details…"></textarea></label>
|
<label>Description<textarea bind:value={editor.draft.description} rows="5" placeholder="Add details..."></textarea></label>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Due date<input type="datetime-local" bind:value={editor.draft.dueDate} /></label>
|
<label>Due date<input type="datetime-local" bind:value={editor.draft.dueDate} /></label>
|
||||||
<label>Card color
|
<label>Card color
|
||||||
|
|
@ -943,7 +938,7 @@
|
||||||
<div class="modal-backdrop" role="presentation" on:click={(event) => event.currentTarget === event.target && (pendingDeletion = null)}>
|
<div class="modal-backdrop" role="presentation" on:click={(event) => event.currentTarget === event.target && (pendingDeletion = null)}>
|
||||||
<div class="modal confirm-modal" role="alertdialog" aria-modal="true" aria-labelledby="delete-title">
|
<div class="modal confirm-modal" role="alertdialog" aria-modal="true" aria-labelledby="delete-title">
|
||||||
<header>
|
<header>
|
||||||
<div><small>CONFIRM DELETE</small><h2 id="delete-title">Delete “{pendingDeletion.title}�</h2></div>
|
<div><small>CONFIRM DELETE</small><h2 id="delete-title">Delete "{pendingDeletion.title}"?</h2></div>
|
||||||
<button type="button" class="icon-button" aria-label="Close" on:click={() => (pendingDeletion = null)}><X size={20} /></button>
|
<button type="button" class="icon-button" aria-label="Close" on:click={() => (pendingDeletion = null)}><X size={20} /></button>
|
||||||
</header>
|
</header>
|
||||||
<p class="modal-copy">
|
<p class="modal-copy">
|
||||||
|
|
@ -953,7 +948,7 @@
|
||||||
This permanently deletes the stack and its {pendingDeletion.cardCount} card{pendingDeletion.cardCount === 1 ? '' : 's'} from Nextcloud.
|
This permanently deletes the stack and its {pendingDeletion.cardCount} card{pendingDeletion.cardCount === 1 ? '' : 's'} from Nextcloud.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<footer><button type="button" class="secondary-button" on:click={() => (pendingDeletion = null)}>Cancel</button><button type="button" class="danger-button" disabled={busy} on:click={confirmStructureDeletion}>{busy ? 'Deleting…' : `Delete ${pendingDeletion.kind}`}</button></footer>
|
<footer><button type="button" class="secondary-button" on:click={() => (pendingDeletion = null)}>Cancel</button><button type="button" class="danger-button" disabled={busy} on:click={confirmStructureDeletion}>{busy ? 'Deleting...' : `Delete ${pendingDeletion.kind}`}</button></footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -972,7 +967,7 @@
|
||||||
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
|
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
{#if client}<button type="button" class="secondary-button" on:click={disconnect}>Disconnect</button>{/if}
|
{#if client}<button type="button" class="secondary-button" on:click={disconnect}>Disconnect</button>{/if}
|
||||||
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting…' : client ? 'Reconnect' : 'Connect'}</button>
|
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting...' : client ? 'Reconnect' : 'Connect'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -996,7 +991,7 @@
|
||||||
<button type="button" class="primary-button" on:click={installUpdate}>Download and install {pendingUpdate?.version}</button>
|
<button type="button" class="primary-button" on:click={installUpdate}>Download and install {pendingUpdate?.version}</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button type="button" class="secondary-button" disabled={updateState === 'checking' || updateState === 'downloading' || updateState === 'installing'} on:click={checkForUpdates}>
|
<button type="button" class="secondary-button" disabled={updateState === 'checking' || updateState === 'downloading' || updateState === 'installing'} on:click={checkForUpdates}>
|
||||||
{updateState === 'checking' ? 'Checking…' : 'Check for updates'}
|
{updateState === 'checking' ? 'Checking...' : 'Check for updates'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ button { color: inherit; }
|
||||||
.app-shell {
|
.app-shell {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: 34px minmax(0, 1fr) 28px / 250px minmax(0, 1fr);
|
grid-template: 34px minmax(0, 1fr) / 250px minmax(0, 1fr);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +115,8 @@ textarea { resize: vertical; min-height: 90px; }
|
||||||
.sidebar-layout button { justify-content: center; gap: 6px; padding: 0 6px; background: #292929; }
|
.sidebar-layout button { justify-content: center; gap: 6px; padding: 0 6px; background: #292929; }
|
||||||
.sidebar-layout button.active { color: #fff; background: #3a3a3a; box-shadow: inset 0 -2px var(--accent); }
|
.sidebar-layout button.active { color: #fff; background: #3a3a3a; box-shadow: inset 0 -2px var(--accent); }
|
||||||
.sidebar-tool span, .sidebar-layout span { font-size: 12px; }
|
.sidebar-tool span, .sidebar-layout span { font-size: 12px; }
|
||||||
|
.refresh-row { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 4px; }
|
||||||
|
.sync-status { overflow: hidden; padding-right: 7px; color: var(--muted); font-size: 10px; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
.workspace { grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: #232323; }
|
.workspace { grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: #232323; }
|
||||||
.workspace-tools { min-height: 46px; flex: 0 0 auto; display: grid; grid-template-columns: minmax(120px, 1fr) minmax(180px, 280px) minmax(80px, 1fr); align-items: center; gap: 16px; padding: 6px 18px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); }
|
.workspace-tools { min-height: 46px; flex: 0 0 auto; display: grid; grid-template-columns: minmax(120px, 1fr) minmax(180px, 280px) minmax(80px, 1fr); align-items: center; gap: 16px; padding: 6px 18px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); }
|
||||||
|
|
@ -173,8 +175,6 @@ textarea { resize: vertical; min-height: 90px; }
|
||||||
.empty-illustration span { position: absolute; width: 66px; height: 44px; z-index: -1; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); transform: translate(-28px, 13px) rotate(-12deg); }
|
.empty-illustration span { position: absolute; width: 66px; height: 44px; z-index: -1; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); transform: translate(-28px, 13px) rotate(-12deg); }
|
||||||
.empty-illustration span:last-child { transform: translate(28px, 13px) rotate(12deg); }
|
.empty-illustration span:last-child { transform: translate(28px, 13px) rotate(12deg); }
|
||||||
|
|
||||||
.statusbar { grid-column: 1 / -1; grid-row: 3; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-top: 1px solid var(--line); color: var(--muted); background: #161616; font-size: 11px; }
|
|
||||||
.statusbar span:last-child { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
||||||
.spinning { animation: spin .9s linear infinite; }
|
.spinning { animation: spin .9s linear infinite; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue