diff --git a/README.md b/README.md index ac97669..afab0fb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Implemented functionality includes: - active-board filtering and saved board selection; - board creation, renaming, recoloring, and deletion; - stack creation, renaming, reordering, and deletion; -- idle-aware background synchronization of boards, stacks, and cards; - horizontal and vertical layouts saved independently per board; - card creation, editing, deletion, due date, description, and color controls; - a directly visible completion checkmark that synchronizes Deck's `done` value diff --git a/app/UPDATES.md b/app/UPDATES.md index 4646a49..64b5c11 100644 --- a/app/UPDATES.md +++ b/app/UPDATES.md @@ -76,16 +76,6 @@ Remove-Item Env:FORGEJO_PAT git pull --ff-only ``` -For an interactive, double-clickable option, run: - -```text -scripts\Publish-DeckyRelease.cmd -``` - -It bypasses PowerShell's script policy only for the publisher process, prompts -for the version and a hidden personal access token, and leaves the window open -so any error can be read. The token is not saved. - The publisher creates the `v0.3.0` Forgejo release/tag at `main`, uploads the installer, signature, and metadata, then commits the tracked `releases/latest.json` through the Forgejo API. The final pull synchronizes that diff --git a/app/package-lock.json b/app/package-lock.json index 84f6305..9edc841 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "decky", - "version": "0.3.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decky", - "version": "0.3.1", + "version": "0.3.0", "dependencies": { "@lucide/svelte": "^0.545.0", "@tauri-apps/api": "^2.0.0", diff --git a/app/package.json b/app/package.json index d3c9127..91c90a6 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "decky", "private": true, - "version": "0.3.1", + "version": "0.3.0", "type": "module", "scripts": { "dev": "vite --configLoader runner", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index aa4f7f0..d556937 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -590,7 +590,7 @@ dependencies = [ [[package]] name = "decky" -version = "0.3.1" +version = "0.3.0" dependencies = [ "keyring", "serde", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 7a52e66..3d172b1 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "decky" -version = "0.3.1" +version = "0.3.0" description = "A focused cross-platform client for Nextcloud Deck" authors = ["Elijah Kuntz"] edition = "2021" diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 0fc5a14..3a07518 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Decky", - "version": "0.3.1", + "version": "0.3.0", "identifier": "com.elijahkuntz.decky", "build": { "beforeDevCommand": "npm run dev", @@ -34,7 +34,7 @@ "category": "Productivity", "shortDescription": "Nextcloud Deck on your desktop", "longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.", - "copyright": "Copyright © 2026 Elijah Kuntz", + "copyright": "Copyright © 2026 Elijah Kuntz", "windows": { "webviewInstallMode": { "type": "downloadBootstrapper" diff --git a/app/src/App.svelte b/app/src/App.svelte index 7fde8a8..906a5d6 100644 --- a/app/src/App.svelte +++ b/app/src/App.svelte @@ -26,7 +26,6 @@ X } from '@lucide/svelte'; import { cssColor, normalizeColor, textColor } from './lib/color'; - import { shouldAutoRefresh } from './lib/auto-refresh'; import { loadPassword, removePassword, savePassword } from './lib/credentials'; import { cardUpdateFrom, DeckApiClient } from './lib/deck-api'; import { loadSettings, saveSettings } from './lib/settings'; @@ -53,7 +52,7 @@ let dragOriginStacks: Stack[] | null = null; let dragGhost: { kind: 'card' | 'stack'; title: string; color?: string | null; x: number; y: number; width: number; offsetX: number; offsetY: number } | null = null; let reduceMotion = false; - let currentVersion = '0.3.1'; + let currentVersion = '0.3.0'; let pendingUpdate: Update | null = null; let updateState: 'idle' | 'checking' | 'available' | 'current' | 'downloading' | 'installing' | 'error' = 'idle'; let updateMessage = 'Updates are checked securely against releases from git.elijahkuntz.com.'; @@ -64,9 +63,6 @@ | { kind: 'stack'; mode: 'create' | 'edit'; id?: number; title: string; order: number } | null = null; let pendingDeletion: { kind: 'board' | 'stack'; id: number; title: string; cardCount: number } | null = null; - let lastInteractionAt = Date.now(); - let lastAutoRefreshAt = 0; - let autoRefreshInFlight = false; const appWindow = '__TAURI_INTERNALS__' in window ? getCurrentWindow() : null; const [sendCard, receiveCard] = crossfade({ duration: (distance) => reduceMotion ? 0 : Math.min(190, Math.max(120, Math.sqrt(distance * 220))), @@ -113,7 +109,7 @@ { 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 = 'Homework · 6 cards · Preview data'; return; } if (!serverUrl || !username) return; @@ -126,17 +122,6 @@ } }); - onMount(() => { - const noteInteraction = () => { lastInteractionAt = Date.now(); }; - const events: (keyof WindowEventMap)[] = ['pointerdown', 'keydown', 'wheel']; - for (const event of events) window.addEventListener(event, noteInteraction, { passive: true }); - const timer = window.setInterval(() => void runIdleRefresh(), 1_000); - return () => { - window.clearInterval(timer); - for (const event of events) window.removeEventListener(event, noteInteraction); - }; - }); - function friendlyError(error: unknown): string { if (error instanceof Error) return error.message; return String(error); @@ -149,7 +134,7 @@ return; } updateState = 'checking'; - updateMessage = 'Checking Forgejo for a newer signed release…'; + updateMessage = 'Checking Forgejo for a newer signed release…'; updateProgress = 0; pendingUpdate = null; try { @@ -170,7 +155,7 @@ async function installUpdate() { if (!pendingUpdate) return; updateState = 'downloading'; - updateMessage = `Downloading Decky ${pendingUpdate.version}…`; + updateMessage = `Downloading Decky ${pendingUpdate.version}…`; let downloaded = 0; let total = 0; try { @@ -184,7 +169,7 @@ } else if (event.event === 'Finished') { updateState = 'installing'; updateProgress = 100; - updateMessage = 'Download verified. Installing and restarting Decky…'; + updateMessage = 'Download verified. Installing and restarting Decky…'; } }); await relaunch(); @@ -196,7 +181,7 @@ async function connect(silent = false) { busy = true; - status = 'Connecting to Nextcloud…'; + status = 'Connecting to Nextcloud…'; try { client = await DeckApiClient.connect({ serverUrl, username, appPassword }); boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt); @@ -228,13 +213,13 @@ async function refreshBoard() { if (!client || !selectedBoard) return; busy = true; - status = `Loading ${selectedBoard.title}…`; + status = `Loading ${selectedBoard.title}…`; try { stacks = (await client.getStacks(selectedBoard.id)) .filter((stack) => !stack.deletedAt) .sort((a, b) => a.order - b.order); const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); - status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · 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) { status = friendlyError(error); } finally { @@ -242,68 +227,6 @@ } } - async function refreshFromNextcloud(background = false) { - if (!client) return; - const activeClient = client; - const startingBoardId = selectedBoard?.id; - const interactionAtStart = lastInteractionAt; - if (!background) { - busy = true; - status = selectedBoard ? `Loading ${selectedBoard.title}…` : 'Refreshing boards…'; - } - try { - const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt); - if (client !== activeClient || (background && (selectedBoard?.id !== startingBoardId || lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return; - boards = refreshedBoards; - const board = refreshedBoards.find((item) => item.id === startingBoardId) ?? refreshedBoards[0]; - if (!board) { - selectedBoard = null; - stacks = []; - status = 'Connected, but no active Deck boards were found.'; - return; - } - - selectedBoard = board; - if (settings.selectedBoardId !== board.id) { - settings = { ...settings, selectedBoardId: board.id }; - saveSettings(settings); - } - const refreshedStacks = (await activeClient.getStacks(board.id)) - .filter((stack) => !stack.deletedAt) - .sort((a, b) => a.order - b.order); - if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return; - stacks = refreshedStacks; - const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); - status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; - } catch (error) { - status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`; - } finally { - if (!background) busy = false; - } - } - - async function runIdleRefresh() { - const now = Date.now(); - if (autoRefreshInFlight || !shouldAutoRefresh({ - now, - lastInteractionAt, - lastRefreshAt: lastAutoRefreshAt, - connected: Boolean(client), - busy, - editing: Boolean(editor || structureEditor || pendingDeletion || settingsOpen), - dragging: Boolean(dragKind), - visible: document.visibilityState === 'visible' - })) return; - - autoRefreshInFlight = true; - lastAutoRefreshAt = now; - try { - await refreshFromNextcloud(true); - } finally { - autoRefreshInFlight = false; - } - } - function canEdit(board: Board | null): boolean { return Boolean(board) && (board?.permissions?.PERMISSION_EDIT ?? true); } @@ -503,7 +426,7 @@ } async function deleteCard(card: Card) { - if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return; + if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return; busy = true; try { await client.deleteCard(selectedBoard.id, card.stackId, card.id); @@ -676,7 +599,7 @@ const targetIndex = stacks.findIndex((stack) => stack.id === stackId); 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 = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`; + status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`; } } catch (error) { stacks = origin; @@ -711,7 +634,7 @@ } -{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'} +{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'}
- + @@ -867,7 +790,7 @@ diff --git a/app/src/lib/auto-refresh.test.ts b/app/src/lib/auto-refresh.test.ts deleted file mode 100644 index e0f8c30..0000000 --- a/app/src/lib/auto-refresh.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AUTO_REFRESH_IDLE_MS, AUTO_REFRESH_REPEAT_MS, shouldAutoRefresh } from './auto-refresh'; - -const idleState = { - now: AUTO_REFRESH_REPEAT_MS, - lastInteractionAt: 0, - lastRefreshAt: 0, - connected: true, - busy: false, - editing: false, - dragging: false, - visible: true -}; - -describe('automatic refresh scheduling', () => { - it('refreshes after the user has been idle long enough', () => { - expect(shouldAutoRefresh(idleState)).toBe(true); - }); - - it('waits for ten seconds of inactivity', () => { - expect(shouldAutoRefresh({ ...idleState, lastInteractionAt: idleState.now - AUTO_REFRESH_IDLE_MS + 1 })).toBe(false); - }); - - it('does not repeatedly refresh more often than the background interval', () => { - expect(shouldAutoRefresh({ ...idleState, lastRefreshAt: idleState.now - AUTO_REFRESH_REPEAT_MS + 1 })).toBe(false); - }); - - it.each([ - ['disconnected', { connected: false }], - ['busy', { busy: true }], - ['editing', { editing: true }], - ['dragging', { dragging: true }], - ['hidden', { visible: false }] - ])('pauses while %s', (_name, change) => { - expect(shouldAutoRefresh({ ...idleState, ...change })).toBe(false); - }); -}); diff --git a/app/src/lib/auto-refresh.ts b/app/src/lib/auto-refresh.ts deleted file mode 100644 index 039dfa3..0000000 --- a/app/src/lib/auto-refresh.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const AUTO_REFRESH_IDLE_MS = 10_000; -export const AUTO_REFRESH_REPEAT_MS = 30_000; - -export interface AutoRefreshState { - now: number; - lastInteractionAt: number; - lastRefreshAt: number; - connected: boolean; - busy: boolean; - editing: boolean; - dragging: boolean; - visible: boolean; -} - -export function shouldAutoRefresh(state: AutoRefreshState): boolean { - return state.connected - && state.visible - && !state.busy - && !state.editing - && !state.dragging - && state.now - state.lastInteractionAt >= AUTO_REFRESH_IDLE_MS - && state.now - state.lastRefreshAt >= AUTO_REFRESH_REPEAT_MS; -} diff --git a/scripts/Publish-DeckyRelease.cmd b/scripts/Publish-DeckyRelease.cmd deleted file mode 100644 index 6502488..0000000 --- a/scripts/Publish-DeckyRelease.cmd +++ /dev/null @@ -1,24 +0,0 @@ -@echo off -setlocal -title Publish Decky Release -cd /d "%~dp0.." - -echo Decky Forgejo release publisher -echo. -echo This creates the Forgejo release, uploads the signed installer, -echo and updates releases/latest.json for Decky's in-app updater. -echo. - -powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0Publish-ForgejoRelease.ps1" -set "DECKY_RESULT=%ERRORLEVEL%" - -echo. -if not "%DECKY_RESULT%"=="0" ( - echo Publishing failed. The error is shown above. - echo No successful release should be assumed until the script says it was published. -) else ( - echo Publishing completed successfully. -) -echo. -pause -exit /b %DECKY_RESULT% diff --git a/scripts/Publish-ForgejoRelease.ps1 b/scripts/Publish-ForgejoRelease.ps1 index 0d29d04..6c4570e 100644 --- a/scripts/Publish-ForgejoRelease.ps1 +++ b/scripts/Publish-ForgejoRelease.ps1 @@ -12,13 +12,7 @@ param( ) $ErrorActionPreference = 'Stop' -if (-not $Token) { - Write-Host 'A Forgejo personal access token is required to create the release.' -ForegroundColor Cyan - Write-Host 'The token is used only by this process and is not saved by Decky.' -ForegroundColor DarkGray - $secureToken = Read-Host 'Forgejo personal access token' -AsSecureString - $Token = [Net.NetworkCredential]::new('', $secureToken).Password -} -if (-not $Token) { throw 'No Forgejo personal access token was entered.' } +if (-not $Token) { throw 'FORGEJO_TOKEN or FORGEJO_PAT is required. Set it only for this PowerShell process.' } $repositoryRoot = Split-Path -Parent $PSScriptRoot $releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version"