diff --git a/README.md b/README.md
index afab0fb..ac97669 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ 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/package-lock.json b/app/package-lock.json
index 9edc841..84f6305 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "decky",
- "version": "0.3.0",
+ "version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "decky",
- "version": "0.3.0",
+ "version": "0.3.1",
"dependencies": {
"@lucide/svelte": "^0.545.0",
"@tauri-apps/api": "^2.0.0",
diff --git a/app/package.json b/app/package.json
index 91c90a6..d3c9127 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,7 +1,7 @@
{
"name": "decky",
"private": true,
- "version": "0.3.0",
+ "version": "0.3.1",
"type": "module",
"scripts": {
"dev": "vite --configLoader runner",
diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock
index d556937..aa4f7f0 100644
--- a/app/src-tauri/Cargo.lock
+++ b/app/src-tauri/Cargo.lock
@@ -590,7 +590,7 @@ dependencies = [
[[package]]
name = "decky"
-version = "0.3.0"
+version = "0.3.1"
dependencies = [
"keyring",
"serde",
diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml
index 3d172b1..7a52e66 100644
--- a/app/src-tauri/Cargo.toml
+++ b/app/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "decky"
-version = "0.3.0"
+version = "0.3.1"
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 3a07518..0fc5a14 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.0",
+ "version": "0.3.1",
"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 906a5d6..7fde8a8 100644
--- a/app/src/App.svelte
+++ b/app/src/App.svelte
@@ -26,6 +26,7 @@
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';
@@ -52,7 +53,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.0';
+ let currentVersion = '0.3.1';
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.';
@@ -63,6 +64,9 @@
| { 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))),
@@ -109,7 +113,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;
@@ -122,6 +126,17 @@
}
});
+ 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);
@@ -134,7 +149,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 {
@@ -155,7 +170,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 {
@@ -169,7 +184,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();
@@ -181,7 +196,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);
@@ -213,13 +228,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 {
@@ -227,6 +242,68 @@
}
}
+ 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);
}
@@ -426,7 +503,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);
@@ -599,7 +676,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;
@@ -634,7 +711,7 @@
}
-