diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index ab71a62262..8b3417a2d2 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -896,6 +896,10 @@ "description": "Control playback from your Windows taskbar", "name": "Taskbar Media Control" }, + "taskbar-progress": { + "name": "Taskbar Progress", + "description": "Shows the current track playback progress on the taskbar" + }, "touchbar": { "description": "Adds a TouchBar widget for macOS users", "name": "TouchBar" diff --git a/src/i18n/resources/ru.json b/src/i18n/resources/ru.json index 23bccb1fe6..f7a4fcbe67 100644 --- a/src/i18n/resources/ru.json +++ b/src/i18n/resources/ru.json @@ -879,6 +879,10 @@ "description": "Управляйте воспроизведением с панели задач Windows", "name": "Управление мультимедиа на панели задач" }, + "taskbar-progress": { + "name": "Прогресс на панели задач", + "description": "Отображает прогресс воспроизведения текущего трека на панели задач" + }, "touchbar": { "description": "Добавляет виджет тачбара для пользователей macOS", "name": "Тачбар" diff --git a/src/plugins/taskbar-progress/index.ts b/src/plugins/taskbar-progress/index.ts new file mode 100644 index 0000000000..3ecdff4a6b --- /dev/null +++ b/src/plugins/taskbar-progress/index.ts @@ -0,0 +1,120 @@ +import { z } from 'zod'; + +import { createPlugin } from '@/utils'; +import { + registerCallback, + getCurrentSongInfo, + type SongInfo, +} from '@/providers/song-info'; +import { t } from '@/i18n'; + +import type { BrowserWindow } from 'electron'; + +const requiredSongInfoSchema = z.object({ + title: z.string().min(1), + elapsedSeconds: z.number().optional(), + songDuration: z.number(), + isPaused: z.boolean().optional(), +}); + +let lastSongInfo: SongInfo | null = null; +let progressInterval: ReturnType | null = null; +let isEnabled = false; +let intervalStart: number | null = null; + +const stopProgressInterval = () => { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + intervalStart = null; + } +}; + +const updateProgressBar = (songInfo: SongInfo, window: BrowserWindow) => { + const validated = requiredSongInfoSchema.safeParse(songInfo); + + if (!validated.success) { + return; + } + + const { title, elapsedSeconds, songDuration, isPaused } = validated.data; + + if ( + !lastSongInfo || + title !== lastSongInfo.title || + elapsedSeconds !== lastSongInfo.elapsedSeconds || + isPaused !== lastSongInfo.isPaused + ) { + lastSongInfo = songInfo; + } + + const progress = (elapsedSeconds ?? 0) / songDuration; + const options: { mode: 'normal' | 'paused' } = { + mode: isPaused ? 'paused' : 'normal', + }; + window.setProgressBar(progress, options); +}; + +const startProgressInterval = (songInfo: SongInfo, window: BrowserWindow) => { + stopProgressInterval(); + if (!songInfo.isPaused) { + intervalStart = performance.now(); + progressInterval = setInterval(() => { + if ( + lastSongInfo && + !lastSongInfo.isPaused && + typeof lastSongInfo.elapsedSeconds === 'number' && + intervalStart !== null + ) { + const timeDelta = (performance.now() - intervalStart) / 1000; + const elapsedSeconds = Math.floor( + lastSongInfo.elapsedSeconds + timeDelta, + ); + updateProgressBar( + { + ...lastSongInfo, + elapsedSeconds, + }, + window, + ); + } + }, 1000); + } +}; + +export default createPlugin({ + name: () => t('plugins.taskbar-progress.name'), + description: () => t('plugins.taskbar-progress.description'), + restartNeeded: false, + config: { enabled: false }, + + backend: { + start({ window }) { + isEnabled = true; + + const currentSongInfo = getCurrentSongInfo(); + if (currentSongInfo?.title) { + updateProgressBar(currentSongInfo, window); + if (!currentSongInfo.isPaused) { + startProgressInterval(currentSongInfo, window); + } + } + + registerCallback((songInfo) => { + if (!isEnabled || !songInfo?.title) return; + updateProgressBar(songInfo, window); + if (songInfo.isPaused) { + stopProgressInterval(); + } else { + startProgressInterval(songInfo, window); + } + }); + }, + + stop({ window }) { + isEnabled = false; + stopProgressInterval(); + window.setProgressBar(-1); + }, + }, +}); diff --git a/src/providers/song-info.ts b/src/providers/song-info.ts index 7957287473..f5865398e9 100644 --- a/src/providers/song-info.ts +++ b/src/providers/song-info.ts @@ -183,11 +183,18 @@ export type SongInfoCallback = ( ) => void; const callbacks: Set = new Set(); +let currentSongInfo: SongInfo | null = null; + // This function will allow plugins to register callback that will be triggered when data changes export const registerCallback = (callback: SongInfoCallback) => { callbacks.add(callback); }; +// This function allows plugins to get the current song info at any time +export const getCurrentSongInfo = (): SongInfo | null => { + return currentSongInfo; +}; + const registerProvider = (win: BrowserWindow) => { const dataMutex = new Mutex(); let songInfo: SongInfo | null = null; @@ -197,6 +204,7 @@ const registerProvider = (win: BrowserWindow) => { const tempSongInfo = await dataMutex.runExclusive( async () => { songInfo = await handleData(data, win); + currentSongInfo = songInfo; return songInfo; }, ); @@ -223,6 +231,7 @@ const registerProvider = (win: BrowserWindow) => { songInfo.isPaused = isPaused; songInfo.elapsedSeconds = elapsedSeconds; + currentSongInfo = songInfo; return songInfo; }); @@ -242,6 +251,7 @@ const registerProvider = (win: BrowserWindow) => { } songInfo.elapsedSeconds = seconds; + currentSongInfo = songInfo; return songInfo; });