diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js b/com.pdma.notion-timer.sdPlugin/bin/plugin.js index b715cfb..d3b8621 100644 --- a/com.pdma.notion-timer.sdPlugin/bin/plugin.js +++ b/com.pdma.notion-timer.sdPlugin/bin/plugin.js @@ -6438,7 +6438,7 @@ async function stopTimer(token, entryId) { } // src/plugin.ts -var CURRENT_VERSION = "1.0.37"; +var CURRENT_VERSION = "1.0.38"; var GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild"; var SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= @@ -6528,17 +6528,18 @@ async function getGlobal() { return { ...stored, ...HARDCODED }; } var memRunningEntryId = void 0; -async function getRunningEntryId() { - if (memRunningEntryId === void 0) { - const stored = await plugin_default.settings.getGlobalSettings(); - memRunningEntryId = stored.runningEntryId ?? null; - } - return memRunningEntryId; -} -async function setRunningEntry(entryId) { - memRunningEntryId = entryId; +var memRunningActionId = void 0; +async function loadRunningState() { + if (memRunningEntryId !== void 0) return; const stored = await plugin_default.settings.getGlobalSettings(); - await plugin_default.settings.setGlobalSettings({ ...stored, runningEntryId: entryId }); + memRunningEntryId = stored.runningEntryId ?? null; + memRunningActionId = stored.runningActionId ?? null; +} +async function setRunningEntry(entryId, actionId) { + memRunningEntryId = entryId; + memRunningActionId = actionId; + const stored = await plugin_default.settings.getGlobalSettings(); + await plugin_default.settings.setGlobalSettings({ ...stored, runningEntryId: entryId, runningActionId: actionId }); } async function sendProjectsToPI(tokenOverride) { try { @@ -6568,42 +6569,34 @@ function buttonTitle(projectName) { return projectName.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "").trim(); } var TimerToggle = class extends SingletonAction { - settingsCache = /* @__PURE__ */ new Map(); + projectCache = /* @__PURE__ */ new Map(); async onWillAppear(ev) { - const { activeEntryId, projectName } = ev.payload.settings; - const title = buttonTitle(projectName || ""); - const running = await getRunningEntryId(); - const isRunning = !!activeEntryId && activeEntryId === running; - if (activeEntryId && !isRunning) { - const cleared = { ...ev.payload.settings, activeEntryId: null }; - await ev.action.setSettings(cleared); - this.settingsCache.set(ev.action.id, cleared); - await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); + this.projectCache.set(ev.action.id, ev.payload.settings); + const title = buttonTitle(ev.payload.settings.projectName || ""); + await loadRunningState(); + const isRunning = memRunningActionId === ev.action.id; + if (isRunning) { + await Promise.all([ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); } else { - this.settingsCache.set(ev.action.id, ev.payload.settings); - if (isRunning) { - await Promise.all([ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); - } else { - await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); - } + await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); } } async onPropertyInspectorDidAppear(_ev) { await sendProjectsToPI(); } async onKeyDown(ev) { - this.settingsCache.set(ev.action.id, ev.payload.settings); - const { projectId, projectName, activeEntryId } = ev.payload.settings; + const { projectId, projectName } = ev.payload.settings; const title = buttonTitle(projectName || ""); + const isRunning = memRunningActionId === ev.action.id; if (projectId) { - if (activeEntryId) { + if (isRunning) { await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); } else { for (const other of this.actions) { if (other.id === ev.action.id) continue; - const otherSettings = this.settingsCache.get(other.id); - if (otherSettings?.activeEntryId) { - await Promise.all([other.setState(0), other.setTitle(buttonTitle(otherSettings.projectName || ""))]); + if (memRunningActionId === other.id) { + const s = this.projectCache.get(other.id); + await Promise.all([other.setState(0), other.setTitle(buttonTitle(s?.projectName || ""))]); } } await Promise.all([ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); @@ -6611,7 +6604,6 @@ var TimerToggle = class extends SingletonAction { } const global = await getGlobal(); if (!isConfigured(global)) { - await Promise.all([ev.action.setState(activeEntryId ? 1 : 0), ev.action.setTitle(activeEntryId ? `\u23F1 ${title}` : title)]); await ev.action.showAlert(); return; } @@ -6620,40 +6612,21 @@ var TimerToggle = class extends SingletonAction { return; } try { - if (activeEntryId) { - await stopTimer(global.notionToken, activeEntryId); - const stopped = { ...ev.payload.settings, activeEntryId: null }; - this.settingsCache.set(ev.action.id, stopped); - await Promise.all([ev.action.setSettings(stopped), ev.action.setState(0), ev.action.setTitle(title)]); - await setRunningEntry(null); + if (isRunning) { + await stopTimer(global.notionToken, memRunningEntryId); + await setRunningEntry(null, null); } else { - const prevEntryId = await getRunningEntryId(); - if (prevEntryId) { - await stopTimer(global.notionToken, prevEntryId); - for (const other of this.actions) { - if (other.id === ev.action.id) continue; - const otherSettings = this.settingsCache.get(other.id); - if (otherSettings?.activeEntryId === prevEntryId) { - const stopped = { ...otherSettings, activeEntryId: null }; - this.settingsCache.set(other.id, stopped); - await Promise.all([other.setSettings(stopped), other.setState(0), other.setTitle(buttonTitle(otherSettings.projectName || ""))]); - } - } + if (memRunningEntryId) { + await stopTimer(global.notionToken, memRunningEntryId); } - const entryId = await startTimer( - global.notionToken, - global.timingDbId, - projectId, - projectName, - global.userId - ); - const started = { ...ev.payload.settings, activeEntryId: entryId }; - this.settingsCache.set(ev.action.id, started); - await Promise.all([ev.action.setSettings(started), ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); - await setRunningEntry(entryId); + const entryId = await startTimer(global.notionToken, global.timingDbId, projectId, projectName, global.userId); + await setRunningEntry(entryId, ev.action.id); } } catch (err) { - await Promise.all([ev.action.setState(activeEntryId ? 1 : 0), ev.action.setTitle(activeEntryId ? `\u23F1 ${title}` : title)]); + await Promise.all([ + ev.action.setState(isRunning ? 1 : 0), + ev.action.setTitle(isRunning ? `\u23F1 ${title}` : title) + ]); plugin_default.logger.error("Timer toggle failed:", err); await ev.action.showAlert(); } diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig index e65c5ea..287a506 100644 --- a/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig +++ b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig @@ -1 +1,2 @@ -ȩԝu1B!I- bhݵ Gr7L]x~cᅻdX ޹y9B// \ No newline at end of file +ʤCSߤ +֮}~P }̘Z;x{,NYPu4#ʄj᧾WC긩 d \ No newline at end of file diff --git a/com.pdma.notion-timer.sdPlugin/manifest.json b/com.pdma.notion-timer.sdPlugin/manifest.json index a946959..a569280 100644 --- a/com.pdma.notion-timer.sdPlugin/manifest.json +++ b/com.pdma.notion-timer.sdPlugin/manifest.json @@ -2,7 +2,7 @@ "Author": "Pete Marfleet", "Description": "Toggle Notion time tracking for a project with a single button press.", "Name": "Notion Timer", - "Version": "1.0.37", + "Version": "1.0.38", "SDKVersion": 2, "Software": { "MinimumVersion": "5.0" }, "OS": [{ "Platform": "mac", "MinimumVersion": "10.11" }], diff --git a/notion-timer.streamDeckPlugin b/notion-timer.streamDeckPlugin index 01d4fca..d8514a0 100644 Binary files a/notion-timer.streamDeckPlugin and b/notion-timer.streamDeckPlugin differ diff --git a/src/plugin.ts b/src/plugin.ts index 7671351..aed18c0 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,4 @@ -const CURRENT_VERSION = "1.0.37"; +const CURRENT_VERSION = "1.0.38"; const GITEA_BASE = "https://gitea.pdmarf.co.uk/pdm/stream_deck_notion_timer/raw/branch/stable-rebuild"; const SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= @@ -52,7 +52,6 @@ async function checkForUpdates(sendStatus?: (msg: string) => void): Promise void): Promise { return { ...stored, ...HARDCODED }; } -// In-memory cache so onWillAppear can check running state without an async round-trip -let memRunningEntryId: string | null | undefined = undefined; // undefined = not yet loaded +// In-memory running state — avoids async round-trips on every button press +let memRunningEntryId: string | null | undefined = undefined; +let memRunningActionId: string | null | undefined = undefined; -async function getRunningEntryId(): Promise { - if (memRunningEntryId === undefined) { - const stored = await streamDeck.settings.getGlobalSettings(); - memRunningEntryId = stored.runningEntryId ?? null; - } - return memRunningEntryId; +async function loadRunningState(): Promise { + if (memRunningEntryId !== undefined) return; + const stored = await streamDeck.settings.getGlobalSettings(); + memRunningEntryId = stored.runningEntryId ?? null; + memRunningActionId = stored.runningActionId ?? null; } -async function setRunningEntry(entryId: string | null): Promise { - memRunningEntryId = entryId; +async function setRunningEntry(entryId: string | null, actionId: string | null): Promise { + memRunningEntryId = entryId; + memRunningActionId = actionId; const stored = await streamDeck.settings.getGlobalSettings(); - await streamDeck.settings.setGlobalSettings({ ...stored, runningEntryId: entryId }); + await streamDeck.settings.setGlobalSettings({ ...stored, runningEntryId: entryId, runningActionId: actionId }); } async function sendProjectsToPI(tokenOverride?: string): Promise { @@ -159,36 +158,23 @@ function isConfigured(g: GlobalSettings): boolean { return !!(g.notionToken && g.userId); } -// Strip leading emoji characters so the button title shows just the project name function buttonTitle(projectName: string): string { return projectName.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "").trim(); } @action({ UUID: "com.pdma.notion-timer.toggle" }) class TimerToggle extends SingletonAction { - private settingsCache = new Map(); + private projectCache = new Map(); async onWillAppear(ev: WillAppearEvent): Promise { - const { activeEntryId, projectName } = ev.payload.settings; - const title = buttonTitle(projectName || ""); - - // Use in-memory cache to determine correct state before rendering — no flash - const running = await getRunningEntryId(); - const isRunning = !!activeEntryId && activeEntryId === running; - - if (activeEntryId && !isRunning) { - // Self-heal: this button thinks it's running but it's not — clear it - const cleared = { ...ev.payload.settings, activeEntryId: null }; - await ev.action.setSettings(cleared); - this.settingsCache.set(ev.action.id, cleared); - await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); + this.projectCache.set(ev.action.id, ev.payload.settings); + const title = buttonTitle(ev.payload.settings.projectName || ""); + await loadRunningState(); + const isRunning = memRunningActionId === ev.action.id; + if (isRunning) { + await Promise.all([ev.action.setState(1), ev.action.setTitle(`⏱ ${title}`)]); } else { - this.settingsCache.set(ev.action.id, ev.payload.settings); - if (isRunning) { - await Promise.all([ev.action.setState(1), ev.action.setTitle(`⏱ ${title}`)]); - } else { - await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); - } + await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); } } @@ -197,21 +183,20 @@ class TimerToggle extends SingletonAction { } async onKeyDown(ev: KeyDownEvent): Promise { - this.settingsCache.set(ev.action.id, ev.payload.settings); - const { projectId, projectName, activeEntryId } = ev.payload.settings; + const { projectId, projectName } = ev.payload.settings; const title = buttonTitle(projectName || ""); + const isRunning = memRunningActionId === ev.action.id; - // Instant visual feedback before any async work + // Instant visual feedback — no setSettings, no flash if (projectId) { - if (activeEntryId) { + if (isRunning) { await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); } else { - // Turn off any other running button immediately using the cache — no async needed for (const other of this.actions) { if (other.id === ev.action.id) continue; - const otherSettings = this.settingsCache.get(other.id); - if (otherSettings?.activeEntryId) { - await Promise.all([other.setState(0), other.setTitle(buttonTitle(otherSettings.projectName || ""))]); + if (memRunningActionId === other.id) { + const s = this.projectCache.get(other.id); + await Promise.all([other.setState(0), other.setTitle(buttonTitle(s?.projectName || ""))]); } } await Promise.all([ev.action.setState(1), ev.action.setTitle(`⏱ ${title}`)]); @@ -219,59 +204,26 @@ class TimerToggle extends SingletonAction { } const global = await getGlobal(); - - if (!isConfigured(global)) { - await Promise.all([ev.action.setState(activeEntryId ? 1 : 0), ev.action.setTitle(activeEntryId ? `⏱ ${title}` : title)]); - await ev.action.showAlert(); - return; - } - - if (!projectId) { - await ev.action.showAlert(); - return; - } + if (!isConfigured(global)) { await ev.action.showAlert(); return; } + if (!projectId) { await ev.action.showAlert(); return; } try { - if (activeEntryId) { - await stopTimer(global.notionToken, activeEntryId); - const stopped = { ...ev.payload.settings, activeEntryId: null }; - this.settingsCache.set(ev.action.id, stopped); - // setSettings and setState together so the state reset doesn't flash - await Promise.all([ev.action.setSettings(stopped), ev.action.setState(0), ev.action.setTitle(title)]); - await setRunningEntry(null); + if (isRunning) { + await stopTimer(global.notionToken, memRunningEntryId!); + await setRunningEntry(null, null); } else { - const prevEntryId = await getRunningEntryId(); - - // Stop previous timer and update its settings - if (prevEntryId) { - await stopTimer(global.notionToken, prevEntryId); - for (const other of this.actions) { - if (other.id === ev.action.id) continue; - const otherSettings = this.settingsCache.get(other.id); - if (otherSettings?.activeEntryId === prevEntryId) { - const stopped = { ...otherSettings, activeEntryId: null }; - this.settingsCache.set(other.id, stopped); - await Promise.all([other.setSettings(stopped), other.setState(0), other.setTitle(buttonTitle(otherSettings.projectName || ""))]); - } - } + if (memRunningEntryId) { + await stopTimer(global.notionToken, memRunningEntryId); } - - const entryId = await startTimer( - global.notionToken, - global.timingDbId, - projectId, - projectName, - global.userId, - ); - const started = { ...ev.payload.settings, activeEntryId: entryId }; - this.settingsCache.set(ev.action.id, started); - // setSettings and setState together so the state reset doesn't flash - await Promise.all([ev.action.setSettings(started), ev.action.setState(1), ev.action.setTitle(`⏱ ${title}`)]); - await setRunningEntry(entryId); + const entryId = await startTimer(global.notionToken, global.timingDbId, projectId, projectName, global.userId); + await setRunningEntry(entryId, ev.action.id); } } catch (err) { - // Revert optimistic visual on error - await Promise.all([ev.action.setState(activeEntryId ? 1 : 0), ev.action.setTitle(activeEntryId ? `⏱ ${title}` : title)]); + // Revert visual on error + await Promise.all([ + ev.action.setState(isRunning ? 1 : 0), + ev.action.setTitle(isRunning ? `⏱ ${title}` : title), + ]); streamDeck.logger.error("Timer toggle failed:", err); await ev.action.showAlert(); } @@ -281,7 +233,6 @@ class TimerToggle extends SingletonAction { const timerAction = new TimerToggle(); streamDeck.actions.registerAction(timerAction); -// v2 requires using streamDeck.ui.onSendToPlugin — the SingletonAction method does not fire streamDeck.ui.onSendToPlugin<{ event: string; settings?: TimerSettings; token?: string }>(async (ev) => { if (ev.payload.event === "saveSettings" && ev.payload.settings) { await ev.action.setSettings(ev.payload.settings); @@ -300,5 +251,4 @@ streamDeck.ui.onSendToPlugin<{ event: string; settings?: TimerSettings; token?: streamDeck.connect(); -// Check for updates 10 seconds after startup to avoid disrupting initial connection setTimeout(() => checkForUpdates(), 10_000); diff --git a/version.json b/version.json index 453a425..f8f6453 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{ "version": "1.0.37" } +{ "version": "1.0.38" }