const CURRENT_VERSION = "1.0.1"; const GITEA_BASE = "http://100.120.125.113:3000/pdm/stream_deck_notion_timer/raw/branch/master"; async function checkForUpdates(): Promise { try { const resp = await fetch(`${GITEA_BASE}/version.json`); if (!resp.ok) return; const { version } = await resp.json() as { version: string }; if (version === CURRENT_VERSION) return; const pluginResp = await fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js`); if (!pluginResp.ok) return; const newCode = await pluginResp.text(); const fs = await import("fs"); fs.writeFileSync(__filename, newCode); streamDeck.logger.info(`Updated to ${version}, restarting…`); process.exit(0); } catch { // Silently ignore — don't disrupt normal operation if update check fails } } import streamDeck, { action, KeyDownEvent, PropertyInspectorDidAppearEvent, SingletonAction, WillAppearEvent, } from "@elgato/streamdeck"; import { fetchProjects, startTimer, stopTimer } from "./notion.js"; interface GlobalSettings { notionToken: string; timingDbId: string; projectsDbId: string; userId: string; } interface TimerSettings { projectId: string; projectName: string; activeEntryId: string | null; } const HARDCODED = { timingDbId: "2ec93117da3a80799ee2cf91244ee264", projectsDbId: "cd65c760-0f0a-4d6e-a262-d572c9e31585", }; async function getGlobal(): Promise { const stored = await streamDeck.settings.getGlobalSettings(); return { ...stored, ...HARDCODED }; } 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(); async onWillAppear(ev: WillAppearEvent): Promise { this.settingsCache.set(ev.action.id, ev.payload.settings); const { activeEntryId, projectName } = ev.payload.settings; const title = buttonTitle(projectName || ""); if (activeEntryId) { await Promise.all([ev.action.setState(1), ev.action.setTitle(`⏱ ${title}`)]); } else { await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); } } async onPropertyInspectorDidAppear(ev: PropertyInspectorDidAppearEvent): Promise { try { const global = await getGlobal(); if (!isConfigured(global)) { await streamDeck.ui.sendToPropertyInspector({ event: "projects", data: [], error: "Configure Notion credentials in plugin settings first." }); return; } const projects = await fetchProjects(global.notionToken, global.projectsDbId); await streamDeck.ui.sendToPropertyInspector({ event: "projects", data: projects }); } catch (err) { streamDeck.logger.error("Failed to fetch projects:", err); await streamDeck.ui.sendToPropertyInspector({ event: "projects", data: [], error: String(err) }); } } async onKeyDown(ev: KeyDownEvent): Promise { this.settingsCache.set(ev.action.id, ev.payload.settings); const global = await getGlobal(); const { projectId, projectName, activeEntryId } = ev.payload.settings; const title = buttonTitle(projectName || ""); 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 }; await ev.action.setSettings(stopped); this.settingsCache.set(ev.action.id, stopped); await ev.action.setState(0); await ev.action.setTitle(title); await ev.action.showOk(); } else { // Stop any other running timer first for (const other of this.actions) { if (other.id === ev.action.id) continue; const otherSettings = this.settingsCache.get(other.id); if (otherSettings?.activeEntryId) { await stopTimer(global.notionToken, otherSettings.activeEntryId); const stopped = { ...otherSettings, activeEntryId: null }; await other.setSettings(stopped); this.settingsCache.set(other.id, stopped); await other.setState(0); await other.setTitle(buttonTitle(otherSettings.projectName || "")); } } const entryId = await startTimer( global.notionToken, global.timingDbId, projectId, projectName, global.userId, ); const started = { ...ev.payload.settings, activeEntryId: entryId }; await ev.action.setSettings(started); this.settingsCache.set(ev.action.id, started); await ev.action.setState(1); await ev.action.setTitle(`⏱ ${title}`); await ev.action.showOk(); } } catch (err) { streamDeck.logger.error("Timer toggle failed:", err); await ev.action.showAlert(); } } } 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 }>(async (ev) => { if (ev.payload.event === "saveSettings" && ev.payload.settings) { await ev.action.setSettings(ev.payload.settings); const title = buttonTitle(ev.payload.settings.projectName || ""); if (title) await ev.action.setTitle(title); } }); streamDeck.connect(); // Check for updates 10 seconds after startup to avoid disrupting initial connection setTimeout(() => checkForUpdates(), 10_000);