From fd3eb2a74792bd122238f36fe809c2a3d97890b8 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Fri, 22 May 2026 15:03:22 +0100 Subject: [PATCH] feat: add Windows taskbar thumbnail toolbar media controls Adds prev/play-pause/next buttons to the Windows taskbar thumbnail for both the main window and mini player, with the play/pause icon swapping based on live playback state from the playbackExtended WS feed. Co-Authored-By: Claude Sonnet 4.6 --- assets/taskbar/next.png | Bin 0 -> 413 bytes assets/taskbar/pause.png | Bin 0 -> 259 bytes assets/taskbar/play.png | Bin 0 -> 387 bytes assets/taskbar/prev.png | Bin 0 -> 462 bytes package.json | 3 ++ src/main.ts | 58 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 assets/taskbar/next.png create mode 100644 assets/taskbar/pause.png create mode 100644 assets/taskbar/play.png create mode 100644 assets/taskbar/prev.png diff --git a/assets/taskbar/next.png b/assets/taskbar/next.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a6a358454dac89445ccc53fb864101003370fe GIT binary patch literal 413 zcmV;O0b>4%P)04cwuv$3$8z%el9FCAdN;K^s?lLbit{eOaf0aw6Q0RjV-(+>$eznytJ+s|xG zfz%_=w6y-9P~^$`o*X0O!DlQ~^g1IFKyB?C;eCwxtRN}yz@}6I#}jZUS->&{TEGrM1sreE1-wy#sxc~1F-8R{#;8C!yh;_A0M}Bl5|2P_ z)jTEwfltZT>8{j*W+=6!97`_hds5pf?@}9ES*)$abkzbqw&rfKq`iI_dwac}vpP4x z-ntW?%hT|9;5ZEpUIT7{7vMfPke@`d;E5?IE2p9;iYbI23|H&qZ@sYa00000NkvXX Hu0mjf;IE{d literal 0 HcmV?d00001 diff --git a/assets/taskbar/pause.png b/assets/taskbar/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..5ee8be1406d9416fb44bfd244b6a111a40b751dd GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND7e+r z#WAE}&fA-gd`%8KZVw~-rkhom*|Zi+@vJjG`6g?tfM@f*{L@RGFlt9}HQZNysq|7U zbLy3pt%bR-(lz3q&vP$v$e57goJ@$Mc?IV?JkNQa|5Fr=M&n!P#}bZlgHx(K@I7kmzPY$^6`KG5{+vs#pA77JzZt9QhHTfvfPCd+> zryi&8QZF>ISIX#1eLHom(n%d`-KLHfAL}yQeNG>*U!*^9tfoIP^f`DeAPcgr_27 zOqmb`rc8(egLzPKfjJy@rU%s%2)!S3^~wrV{&Tt;EL8G`<}JxY4R@6lsQgcTH(06U zSY^OqYvjEnj7o07&)FY~7%jF681Am)CYL$|kJk@|jMwW~PfCUzaO{jL@B)_by-%HF zC2WQ)bR-&mj+h#gY`&W$xAV68iBqig+z^RGBB4zD2d)t0e=;=CG5`Po07*qoM6N<$ Eg8YxcWB>pF literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 48347bd..67d8ec5 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,9 @@ "renderer/dist/**", "package.json" ], + "extraResources": [ + { "from": "assets/taskbar", "to": "assets/taskbar" } + ], "win": { "target": "nsis", "icon": "icon.ico", diff --git a/src/main.ts b/src/main.ts index c672662..64d09fc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import { config as loadEnv } from 'dotenv'; loadEnv(); // loads .env from cwd (repo root) in dev; no-op if file absent -import { app, BrowserWindow, ipcMain, safeStorage, session, shell, Menu, IpcMainInvokeEvent } from 'electron'; +import { app, BrowserWindow, ipcMain, nativeImage, safeStorage, session, shell, Menu, IpcMainInvokeEvent } from 'electron'; import { autoUpdater } from 'electron-updater'; import { officePubSub } from './pubsub'; import { EntraAuth } from './auth-entra'; @@ -567,6 +567,13 @@ function handleWsMessage(raw: Buffer | string): void { return; } + if (ns === 'playbackExtended') { + const state = (payload as Record | null)?.['playback'] as Record | undefined; + if (state?.['playbackState']) { + updateThumbar(state['playbackState'] === 'PLAYBACK_STATE_PLAYING'); + } + } + broadcastToRenderers('ws:message', [header, payload]); } @@ -926,6 +933,52 @@ let miniWin: BrowserWindow | null = null; let debugWin: BrowserWindow | null = null; let httpDebugWin: BrowserWindow | null = null; let authConfirmed = false; // prevents onAuthReady firing on every /api/content/ 200 +let thumbarIsPlaying = false; + +function taskbarAsset(name: string): Electron.NativeImage { + const base = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..'); + return nativeImage.createFromPath(path.join(base, 'assets', 'taskbar', name)); +} + +function setThumbar(win: BrowserWindow, isPlaying: boolean): void { + if (process.platform !== 'win32') return; + win.setThumbarButtons([ + { + tooltip: 'Previous', + icon: taskbarAsset('prev.png'), + click() { + const groupId = config.groupId; + if (ws && ws.readyState === WebSocket.OPEN) + wsSend({ namespace: 'playback', groupId, command: 'skipBack' }, {}).catch(() => {}); + }, + }, + { + tooltip: isPlaying ? 'Pause' : 'Play', + icon: taskbarAsset(isPlaying ? 'pause.png' : 'play.png'), + click() { + const groupId = config.groupId; + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const cmd = thumbarIsPlaying ? 'pause' : 'play'; + wsSend({ namespace: 'playback', groupId, command: cmd }, { allowTvPauseRestore: true, deviceFeedback: 'NONE' }).catch(() => {}); + }, + }, + { + tooltip: 'Next', + icon: taskbarAsset('next.png'), + click() { + const groupId = config.groupId; + if (ws && ws.readyState === WebSocket.OPEN) + wsSend({ namespace: 'playback', groupId, command: 'skipToNextTrack' }, {}).catch(() => {}); + }, + }, + ]); +} + +function updateThumbar(isPlaying: boolean): void { + thumbarIsPlaying = isPlaying; + if (uiWin && !uiWin.isDestroyed()) setThumbar(uiWin, isPlaying); + if (miniWin && !miniWin.isDestroyed()) setThumbar(miniWin, isPlaying); +} /** Send a channel/args pair to all live renderer windows (main + mini player). */ function broadcastToRenderers(channel: string, ...args: unknown[]): void { @@ -1193,6 +1246,7 @@ function createUIWindow(): void { uiWin.on('maximize', () => uiWin?.webContents.send('win:maximized', true)); uiWin.on('unmaximize', () => uiWin?.webContents.send('win:maximized', false)); + setThumbar(uiWin, thumbarIsPlaying); if (app.isPackaged) { uiWin.loadFile(path.join(__dirname, '..', 'renderer', 'dist', 'index.html')); @@ -1259,6 +1313,8 @@ function createMiniPlayerWindow(): void { } }); + setThumbar(miniWin, thumbarIsPlaying); + miniWin.on('closed', () => { miniWin = null; });