feat: Unified Downloader + Taskbar Widget + Audio Visualizer#4350
feat: Unified Downloader + Taskbar Widget + Audio Visualizer#4350digitalnomad91 wants to merge 44 commits intopear-devs:masterfrom
Conversation
…oader-plugins ci: add concurrency settings to prevent workflow cancellation
… prefix - Fix incorrect console output when file already exists: replace broken 'find newest mp3' fallback with pre-resolved expected filename from yt-dlp - Detect yt-dlp 'already downloaded' output and show accurate log messages - Namespace all ytdlp IPC channels (download-song-ytdlp, downloader-ytdlp-feedback, downloader-ytdlp-error-toast, download-playlist-request-ytdlp) to prevent conflicts when both downloader and downloader-ytdlp plugins are enabled - Change ytdlp button text to 'Download (ytdlp)' and use distinct element ID so both download buttons work independently - Fix 'NA - ' filename prefix by using yt-dlp conditional template syntax that only includes artist when metadata is available
fix(downloader-ytdlp): fix file-exists logging, IPC conflicts, and NA prefix
Creates a small frameless always-on-top window positioned above the Windows taskbar that displays album art, song title, artist name, and previous/play-pause/next media controls. The widget updates in real-time via IPC as songs change and communicates control commands back to the main player. Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…ove alt text Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Update downloader plugin with unified download logic - Add menu integration for downloader - Update UI components for downloader interface - Add i18n strings for downloader features
- Position widget directly ON the taskbar surface using display bounds vs workArea geometry detection - Auto-detect taskbar height (fallback to 48px for Windows 11) - Place widget to the left of the system tray / notification area - Compact UI scaled to fit within taskbar height (~48px): album art, title, artist, prev/play-pause/next controls - Transparent background so widget blends with taskbar - Non-draggable, non-movable, locked to taskbar position - Reposition on display configuration changes - Window is non-focusable to avoid stealing focus from taskbar Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Use setAlwaysOnTop(true, 'screen-saver') z-level so the widget renders above the taskbar instead of underneath it - Set window type to 'toolbar' to prevent third-party window managers like DisplayFusion from attaching overlays (move to next monitor, etc.) - Add monitorIndex config option with menu radio buttons so users can choose which monitor the widget appears on - Update getTaskbarGeometry to use the selected display instead of always the primary display Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Add periodic repositioning (every 2s) that reasserts setAlwaysOnTop to recover from z-index loss when other windows are focused/moved - Periodic recheck also handles auto-hide taskbar state changes by recalculating bounds from the current display workArea - Add offsetX/offsetY config with multiInput prompt dialog so users can fine-tune horizontal and vertical position of the widget - Add i18n strings for position offset menu Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
… blur - Add onConfigChange handler to live-apply offset/blur changes without restart - Export updateConfig() for live offset + blur updates from config changes - Reduce reposition interval from 2000ms to 500ms for faster z-index recovery - Increase SYSTEM_TRAY_ESTIMATED_WIDTH from 300 to 450 to avoid pinned icons - Make layout more compact: reduce gaps, padding, button/icon sizes - Add background blur option with backdrop-filter: blur(20px) - Add blur-bg CSS class toggled via IPC, with menu checkbox toggle - Add i18n string for background blur menu item Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…yout
- Replace fixed 300px width with content-driven dynamic sizing (150-350px)
via ResizeObserver + IPC resize channel from renderer to main
- CSS: inline-flex container, 3px gap, max-width:160px info section
- Add on('hide') handler for instant recovery from external hides
- Add recoverVisibility() helper used by both hide handler and timer
- Reduce reposition interval to 250ms, add moveTop() for z-order
- Add isShowing/intentionalClose flags for proper state management
- Debounce resize reports (50ms) to avoid IPC floods
- Clean up resize IPC handler in cleanup()
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Remove on('hide') handler that fought with OS/shell causing broken
rendering state where window was shown but not rendered
- Add setIgnoreMouseEvents(true, {forward: true}) to make window
click-through until a song is actually displaying
- Only call setBounds when bounds actually change to reduce flickering
- Move getConfig() inside position dialog click handler so it reads
fresh values each time the dialog is opened
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Add moveTop() to repositionWidget so the widget always reasserts z-order above any overlays (fixes Start menu hiding widget permanently) - Add 'hide' event handler with immediate + retry recovery (fixes brief disappearance on taskbar clicks being visible too long) - Add 'always-on-top-changed' event handler to immediately reassert z-order when stolen by external processes - Clean up recovery timers on plugin cleanup Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…, UI tweaks 1. Fix Start menu hiding widget permanently: replace fixed-retry recovery with a persistent 100ms interval that keeps retrying for 3 seconds after any hide event. Handle minimize events. Always reassert z-order in recoverVisibility() even when isVisible() returns true. 2. Reduce reposition polling from 250ms to 100ms for faster recovery from brief disappearances caused by taskbar interactions. 3. Click anywhere on widget (outside control buttons) to show/focus the main YouTube Music window via new taskbar-widget:show-window IPC channel. 4. Increase gap between album art and title/artist text from 3px to 8px. 5. Increase font sizes: title 12→13px, artist 10→11px (at standard taskbar height ≥48px). Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…overy The widget could get stuck behind the taskbar after Start menu interactions because calling setAlwaysOnTop(true) on an already-TOPMOST window is a no-op on Windows — the OS does not re-evaluate z-band position. Fix: toggle setAlwaysOnTop off then on (HWND_NOTOPMOST → HWND_TOPMOST) to force z-order re-evaluation. The toggle happens: - Immediately on hide/minimize events (existing handlers) - Every ~500ms via the periodic reposition timer (new tick counter) - On main window blur/focus events (new handlers for event-driven recovery) Also: always call showInactive() in recoverVisibility() even when isVisible() returns true, since the widget can appear "visible" while rendered behind the taskbar surface. Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…olor blur - Remove all periodic z-order manipulation from repositionWidget() to eliminate constant stutter; timer now only handles bounds checking - Use opacity trick (0 → toggle → 1) in recoverVisibility() to prevent visible flash during z-order recovery - Schedule staggered recovery attempts (300/800/1500/3000ms) after main window blur for reliable Start menu recovery - Increase container padding (top/bottom and left for blur-bg) - Increase blur background border-radius to 8px (semi-rounded corners) - Extract dominant colour from album art via nativeImage in main process and send to widget for dynamic gradient blur background Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…ur padding - Bring back FORCE_ZORDER_EVERY_N_TICKS periodic z-order toggle in repositionWidget() so the widget reliably recovers from being pushed behind the taskbar (Start menu, third-party taskbar tools, etc.) even when the main YTM window is not focused. - The toggle is wrapped in the opacity:0 guard (recoverVisibility()) to prevent the visible stutter that plagued the earlier implementation. - On intermediate ticks, lightweight setAlwaysOnTop + moveTop keeps the widget above without toggling. - Body CSS changed to flex with vertical centering; container no longer fills full taskbar height, giving the blur background a few pixels of outer padding so it doesn't butt up against the taskbar edges. Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
- Album art sized to widgetHeight-20 (was -12) to double the vertical gap between blur background and taskbar edges - Gradient alpha reduced from 0.45/0.55 to 0.25/0.35 - Default fallback alpha reduced from 0.3 to 0.15 Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…d values Album art sized to widgetHeight-16 (was -20, originally -12) to split the difference as requested — more padding than the original but less than the doubled version. Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…e flicker - Border: 1px solid rgba(255,255,255,0.12) on blur background - Border-radius: 8px → 4px (slightly rounded, not square) - Album art already 4px, now matches blur background - Gradient opacity: 0.25/0.35 → 0.35/0.45; fallback: 0.15 → 0.25 - Flicker: increase z-order toggle interval 1.5s → 3s, skip redundant setAlwaysOnTop on intermediate ticks, guard moveTop behind isVisible() check Co-authored-by: digitalnomad91 <2067771+digitalnomad91@users.noreply.github.com>
…y, and positioning Adds an audio visualizer that renders animated frequency bars adjacent to the taskbar widget mini player, inspired by FluentFlyout's Taskbar Visualizer. Features: - Real-time FFT audio analysis from the YouTube Music renderer - Configurable bar count (4-64), centered or bottom-rising bars - Show/hide baseline indicator - Audio sensitivity and peak threshold controls - Position left or right of the widget - Dynamic bar colors based on album art dominant color - Separate BrowserWindow for isolation from the main widget - Menu integration with enable/disable toggle and all settings - i18n support for all menu labels
…dd year display, add blur opacity and visualizer width config options - Rewrite visualizer frequency mapping with mel-like perceptual spacing, amplitude compression (sqrt), and dynamic rolling-peak normalization so all bars react evenly across the spectrum - Skip sub-bass bins (DC offset) and use 0.6 power curve for bin mapping - Faster attack / slower release smoothing for natural bar animation - Strip duplicate artist prefix/suffix from song title display - Show upload year in parenthesis after artist name in smaller text - Add configurable blur opacity (0.1-1.0) with menu option - Add configurable visualizer width (40-300px) with menu option - Reduce default visualizer width from 120px to 84px
…ibution - Replace custom frequency mapping with faithful port of FluentFlyout's ProcessFftData() from Visualizer.cs - Logarithmic frequency band mapping (40 Hz - 8000 Hz) using exponential spacing: minFreq * pow(maxFreq/minFreq, i/barCount) - Max amplitude per band (not average) matching FluentFlyout behavior - Large linear boost for higher-frequency bars (1 + progress * 75) compensating for natural energy rolloff in music - dB-scale intensity mapping with configurable min/max dB range - Asymmetric smoothing: instant attack, 0.8/0.2 weighted decay (FluentFlyout's exact smoothing constants) - Increase FFT size from 128 to 1024 (512 bins) for better frequency resolution; disable AnalyserNode smoothing for raw data - Convert byte frequency data (0-255) to linear amplitude via dB intermediary before applying FluentFlyout's processing pipeline
…mize - Default audioSensitivity: 0.3 -> 0.01 - Default audioPeakThreshold: 0.85 -> 1.0 - Default barCount: 20 -> 64 - centeredBars and showBaseline already defaulted to true - Add allowed ranges to menu labels (e.g. 'Bar Count (4-64)') - Fix visualizer freeze when main window is minimized/hidden: switch from requestAnimationFrame (pauses for hidden windows) to setInterval so audio data continues flowing to the visualizer
Chromium aggressively throttles setInterval in minimized/hidden BrowserWindows down to ~1000ms intervals. Since the audio FFT analysis runs in the main renderer via setInterval, minimizing or closing the main window caused the visualizer to become choppy. Call mainWindow.webContents.setBackgroundThrottling(false) when the visualizer is enabled to keep the audio data loop running at full ~30fps. Re-enable throttling when the visualizer is disabled or the plugin is cleaned up.
|
|
||
| const pluginMenus = await Promise.all( | ||
| availablePlugins | ||
| .filter((id) => !downloaderIds.includes(id as typeof downloaderIds[number])) |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace (id)·=>·!downloaderIds.includes(id·as·typeof·downloaderIds[number]) with ⏎········(id)·=>·!downloaderIds.includes(id·as·(typeof·downloaderIds)[number]),⏎······
| .filter((id) => !downloaderIds.includes(id as typeof downloaderIds[number])) | |
| .filter( | |
| (id) => !downloaderIds.includes(id as (typeof downloaderIds)[number]), | |
| ) |
| } | ||
|
|
||
| // Plugin enabled with menu template | ||
| const template = (predefinedTemplate[1] as Electron.MenuItemConstructorOptions).submenu; |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace predefinedTemplate[1]·as·Electron.MenuItemConstructorOptions with ⏎········predefinedTemplate[1]·as·Electron.MenuItemConstructorOptions⏎······
| const template = (predefinedTemplate[1] as Electron.MenuItemConstructorOptions).submenu; | |
| const template = ( | |
| predefinedTemplate[1] as Electron.MenuItemConstructorOptions | |
| ).submenu; |
| } | ||
|
|
||
| // Plugin enabled with menu template | ||
| const template = (predefinedTemplate[1] as Electron.MenuItemConstructorOptions).submenu; |
There was a problem hiding this comment.
🚫 [eslint] <@typescript-eslint/no-unnecessary-type-assertion> reported by reviewdog 🐶
This assertion is unnecessary since it does not change the type of the expression.
| const template = (predefinedTemplate[1] as Electron.MenuItemConstructorOptions).submenu; | |
| const template = (predefinedTemplate[1]).submenu; |
| const validDownloaderSubmenus: Electron.MenuItemConstructorOptions[] = downloaderSubmenus.filter( | ||
| (s): s is NonNullable<typeof s> => s !== null, | ||
| ); |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ·downloaderSubmenus.filter(⏎····(s):·s·is·NonNullable<typeof·s>·=>·s·!==·null,⏎·· with ⏎····downloaderSubmenus.filter((s):·s·is·NonNullable<typeof·s>·=>·s·!==·null
| const validDownloaderSubmenus: Electron.MenuItemConstructorOptions[] = downloaderSubmenus.filter( | |
| (s): s is NonNullable<typeof s> => s !== null, | |
| ); | |
| const validDownloaderSubmenus: Electron.MenuItemConstructorOptions[] = | |
| downloaderSubmenus.filter((s): s is NonNullable<typeof s> => s !== null); |
| const insertIdx = pluginMenus.findIndex((item) => { | ||
| const itemLabel = | ||
| typeof item === 'object' && 'label' in item | ||
| ? (item as Electron.MenuItemConstructorOptions).label ?? '' |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace item·as·Electron.MenuItemConstructorOptions).label·??·'' with (item·as·Electron.MenuItemConstructorOptions).label·??·'')
| ? (item as Electron.MenuItemConstructorOptions).label ?? '' | |
| ? ((item as Electron.MenuItemConstructorOptions).label ?? '') |
| }, | ||
| ), | ||
| detail: t( | ||
| { playlistTitle }, |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace {·playlistTitle·} with ··playlistTitle
| { playlistTitle }, | |
| playlistTitle, |
| ), | ||
| detail: t( | ||
| { playlistTitle }, | ||
| ) + ' ' + t( |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ····)·+·'·'·+·t( with ······})·+⏎······'·'·+
| ) + ' ' + t( | |
| }) + | |
| ' ' + |
| }, | ||
| ), | ||
| }); | ||
| { playlistSize: items.length }, |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace {·playlistSize:·items.length·} with ··playlistSize:·items.length
| { playlistSize: items.length }, | |
| playlistSize: items.length, |
| ), | ||
| }); | ||
| { playlistSize: items.length }, | ||
| ); |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Insert ··}
| ); | |
| }); |
| ); | ||
| win.webContents.send('downloader-error-toast', { | ||
| message: playlistMessage, | ||
| title: t('plugins.downloader.backend.dialog.start-download-playlist.title'), |
There was a problem hiding this comment.
🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace 'plugins.downloader.backend.dialog.start-download-playlist.title' with ⏎········'plugins.downloader.backend.dialog.start-download-playlist.title',⏎······
| title: t('plugins.downloader.backend.dialog.start-download-playlist.title'), | |
| title: t( | |
| 'plugins.downloader.backend.dialog.start-download-playlist.title', | |
| ), |
There was a problem hiding this comment.
🚫 [eslint] <@typescript-eslint/await-thenable> reported by reviewdog 🐶
Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.
Lines 132 to 159 in c72522a
There was a problem hiding this comment.
Pull request overview
This PR bundles multiple feature additions across the plugin system: a unified downloader menu (combining the existing downloader + a new yt-dlp-based downloader), a Windows-only taskbar widget with an optional audio visualizer, and restoration of an adblocker plugin, alongside the required i18n/menu/workflow updates.
Changes:
- Adds a Windows 11 taskbar mini-player widget plus a companion visualizer window and IPC audio pipeline.
- Introduces a new
downloader-ytdlpplugin and replaces some blocking downloader dialogs with renderer-driven toast notifications. - Restores/introduces an adblocker plugin using Ghostery/Cliqz adblocker components and injector logic; updates menu structure and translations.
Reviewed changes
Copilot reviewed 66 out of 69 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/plugins/taskbar-widget/main.ts | Implements the taskbar widget + visualizer windows, positioning/z-order recovery, IPC, and album-art color extraction. |
| src/plugins/taskbar-widget/index.ts | Defines taskbar-widget plugin config + menus; captures audio FFT data in the renderer and forwards to main via IPC. |
| src/plugins/downloader/renderer.tsx | Adds non-blocking toast UI in the existing downloader renderer. |
| src/plugins/downloader/main/index.ts | Replaces blocking dialogs with IPC-driven toast notifications for errors/playlist start; minor formatting changes. |
| src/plugins/downloader-ytdlp/types.ts | Adds yt-dlp plugin type definitions and format/preset lists. |
| src/plugins/downloader-ytdlp/templates/download.tsx | Adds the yt-dlp downloader menu button template. |
| src/plugins/downloader-ytdlp/style.css | Adds styling for the yt-dlp downloader menu item. |
| src/plugins/downloader-ytdlp/renderer.tsx | Implements yt-dlp downloader renderer integration + toast notifications. |
| src/plugins/downloader-ytdlp/menu.ts | Adds yt-dlp downloader menu entries (folder selection, yt-dlp path prompt, presets, etc.). |
| src/plugins/downloader-ytdlp/main/utils.ts | Adds helper utilities (feedback IPC, badge, artwork crop). |
| src/plugins/downloader-ytdlp/main/index.ts | Implements yt-dlp downloading logic (song + playlist) and progress/feedback/error reporting. |
| src/plugins/downloader-ytdlp/index.ts | Registers the yt-dlp downloader plugin and its config defaults. |
| src/plugins/adblocker/types/index.ts | Defines adblocker mode labels/constants. |
| src/plugins/adblocker/injectors/inject.js | Adds in-page injector logic (ported from external source) to prune ad fields / trap properties. |
| src/plugins/adblocker/injectors/inject.d.ts | Adds TS typings for injector exports. |
| src/plugins/adblocker/injectors/inject-cliqz-preload.ts | Loads Ghostery/Cliqz preload integration. |
| src/plugins/adblocker/index.ts | Registers adblocker plugin and wires up blocker selection + preload injection. |
| src/plugins/adblocker/blocker.ts | Adds Ghostery ElectronBlocker setup, list loading, and session enable/disable. |
| src/plugins/adblocker/adSpeedup.ts | Adds “ad speedup” mode logic (mute + speed up + auto-skip). |
| src/plugins/adblocker/.gitignore | Ignores a generated adblocker engine binary artifact. |
| src/menu.ts | Groups downloader plugins under a unified “Downloader” menu entry with submenus. |
| src/i18n/resources/en.json | Adds new strings for yt-dlp path menu items and taskbar-widget/visualizer menus. |
| src/i18n/resources/ar.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/bg.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/bn.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ca.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/cs.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/de.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/el.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/es.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/fa.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/fi.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/fil.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/fr.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/hi.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/hr.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/hu.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/id.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/is.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/it.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ja.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ko.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/lt.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/lv.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ms.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/nb.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ne.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/nl.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/pl.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/pt.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/pt-BR.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ro.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ru.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/sk.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/sl.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/sr.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/sv.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/ta.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/th.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/tr.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/uk.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/vi.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/zh-CN.json | Adds downloader-ytdlp name/description translations. |
| src/i18n/resources/zh-TW.json | Adds downloader-ytdlp name/description translations. |
| package.json | Adds chalk and @types/chalk dependencies. |
| pnpm-lock.yaml | Locks chalk and @types/chalk versions (and updates transitive lock entries). |
| README.md | Adds fork-specific documentation at the top of the README. |
| .gitignore | Ignores package-lock.json. |
| .github/workflows/build.yml | Adds workflow concurrency configuration. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| // Assume 44100 Hz sample rate; fftSize = dataLen * 2 | ||
| const sampleRate = 44100; | ||
| const fftSize = dataLen * 2; |
There was a problem hiding this comment.
The visualizer FFT-to-frequency mapping hardcodes sampleRate = 44100, but WebAudio AudioContext.sampleRate is often 48000 on many systems. This will skew frequencyPerBin and therefore the band mapping. Pass the real sampleRate/fftSize from the renderer (or send it once via IPC) instead of assuming 44.1 kHz.
| onPlayerApiReady(_, { ipc }) { | ||
| document.addEventListener( | ||
| 'peard:audio-can-play', | ||
| (e: Event) => { | ||
| const detail = (e as CustomEvent).detail as { | ||
| audioContext: AudioContext; | ||
| audioSource: MediaElementAudioSourceNode; | ||
| }; | ||
| this.audioContext = detail.audioContext; | ||
| this.audioSource = detail.audioSource; | ||
| this.startAnalysis(ipc.send); | ||
| }, | ||
| { passive: true }, | ||
| ); | ||
| }, | ||
|
|
||
| startAnalysis( | ||
| this: { | ||
| audioContext: AudioContext | null; | ||
| audioSource: MediaElementAudioSourceNode | null; | ||
| analyser: AnalyserNode | null; | ||
| animationFrame: number | null; | ||
| ipcSend: ((channel: string, ...args: unknown[]) => void) | null; | ||
| }, | ||
| send: (channel: string, ...args: unknown[]) => void, | ||
| ) { | ||
| if (!this.audioContext || !this.audioSource) return; | ||
|
|
||
| // Clean up any previous analyser | ||
| if (this.animationFrame) { | ||
| clearInterval(this.animationFrame); | ||
| this.animationFrame = null; | ||
| } | ||
|
|
||
| this.analyser = this.audioContext.createAnalyser(); | ||
| this.analyser.fftSize = 1024; | ||
| this.analyser.smoothingTimeConstant = 0; | ||
| this.audioSource.connect(this.analyser); | ||
|
|
||
| const dataArray = new Uint8Array(this.analyser.frequencyBinCount); | ||
| const analyserRef = this.analyser; | ||
|
|
||
| // Use setInterval instead of requestAnimationFrame so that audio | ||
| // data continues to be captured and forwarded even when the main | ||
| // BrowserWindow is minimized or hidden (rAF pauses for hidden windows). | ||
| const intervalId = setInterval(() => { | ||
| analyserRef.getByteFrequencyData(dataArray); | ||
| send('taskbar-widget:audio-data', Array.from(dataArray)); | ||
| }, 33); // ~30 fps | ||
|
|
||
| // Store the interval ID so we can clean it up later. | ||
| // We repurpose animationFrame to hold this (it's just a number ID). | ||
| this.animationFrame = intervalId as unknown as number; | ||
| }, | ||
|
|
||
| stop() { | ||
| if (this.animationFrame) { | ||
| clearInterval(this.animationFrame); | ||
| this.animationFrame = null; | ||
| } | ||
| this.analyser = null; | ||
| this.audioContext = null; | ||
| this.audioSource = null; | ||
| }, |
There was a problem hiding this comment.
onPlayerApiReady adds a peard:audio-can-play listener but stop() never removes it. If the plugin is disabled/re-enabled without restarting immediately (restart dialog can be deferred), this can accumulate listeners and analyzers. Store the handler and removeEventListener in stop(), and also disconnect the analyser node from the audio graph when stopping.
| // Enhanced args with higher quality and metadata embedding (NO separate thumbnail downloads) | ||
| const args = [ | ||
| '-x', | ||
| '--audio-format', | ||
| 'mp3', | ||
| '--audio-quality', | ||
| '320K', // Higher bitrate | ||
| '--embed-thumbnail', // Embed album art into the MP3 | ||
| '--embed-metadata', // Embed ID3 tags | ||
| '--add-metadata', // Additional metadata | ||
| '--convert-thumbnails', | ||
| 'jpg', // Convert to standard format (for embedded only, no separate files) | ||
|
|
||
| '-o', | ||
| outTemplate, | ||
| url, | ||
| ]; |
There was a problem hiding this comment.
The yt-dlp implementation currently hardcodes MP3/320K in args and doesn't use the plugin's preset-related config (selectedPreset, customPresetSetting, DefaultPresetList) exposed in the menu. This makes the Presets UI misleading. Wire the selected preset into the yt-dlp/ffmpeg arguments (or remove the preset menu/config until supported).
| // IDs that will be merged into a single "Downloader" parent menu | ||
| const downloaderIds = ['downloader', 'downloader-ytdlp'] as const; | ||
| const downloaderLabels: Record<string, string> = { | ||
| 'downloader': 'youtube.js (built-in)', | ||
| 'downloader-ytdlp': 'ytdlp (external exe)', | ||
| }; |
There was a problem hiding this comment.
The unified downloader submenu labels are hardcoded English strings (youtube.js (built-in), ytdlp (external exe)). Menu labels elsewhere are localized via t(...), so this introduces non-translated UI. Consider adding i18n keys for these labels or deriving them from the plugin name()/description() to stay consistent.
|
|
||
| // Restore normal background throttling | ||
| if (mainWindowRef && !mainWindowRef.isDestroyed()) { | ||
| mainWindowRef.webContents.setBackgroundThrottling(true); |
There was a problem hiding this comment.
cleanup() unconditionally calls mainWindowRef.webContents.setBackgroundThrottling(true). If something else in the app had disabled throttling for the main window, this will override it. Track whether this plugin changed the setting (and only restore when needed), or store/restore the previous value.
| // High-DPI canvas scaling | ||
| const resize = () => { | ||
| const dpr = window.devicePixelRatio || 1; | ||
| const rect = canvas.getBoundingClientRect(); | ||
| canvas.width = rect.width * dpr; | ||
| canvas.height = rect.height * dpr; | ||
| ctx.scale(dpr, dpr); |
There was a problem hiding this comment.
In the visualizer canvas resize() handler, calling ctx.scale(dpr, dpr) on every resize will accumulate transforms and eventually distort drawing (bars get smaller/larger each resize). Reset the transform first (e.g., ctx.setTransform(1,0,0,1,0,0)), then apply the DPR scale.
| const dataArray = new Uint8Array(this.analyser.frequencyBinCount); | ||
| const analyserRef = this.analyser; | ||
|
|
||
| // Use setInterval instead of requestAnimationFrame so that audio | ||
| // data continues to be captured and forwarded even when the main | ||
| // BrowserWindow is minimized or hidden (rAF pauses for hidden windows). | ||
| const intervalId = setInterval(() => { | ||
| analyserRef.getByteFrequencyData(dataArray); | ||
| send('taskbar-widget:audio-data', Array.from(dataArray)); | ||
| }, 33); // ~30 fps |
There was a problem hiding this comment.
The audio analysis loop allocates a fresh JS array every ~33ms via Array.from(dataArray) before sending over IPC. This will create steady GC pressure. Consider sending the Uint8Array/Buffer directly (or transferring an ArrayBuffer) and decoding on the other side to reduce allocations.
| const playlistId = | ||
| getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl)); | ||
|
|
There was a problem hiding this comment.
downloadPlaylist() does getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl)). If playingUrl hasn't been set yet (e.g., user triggers playlist download before ytmd:video-src-changed fires), new URL(playingUrl) will throw and break the flow. Guard playingUrl (and/or wrap in try/catch) before constructing a URL.
| "@playwright/test": "1.58.2", | ||
| "@stylistic/eslint-plugin": "5.7.1", | ||
| "@total-typescript/ts-reset": "0.6.1", | ||
| "@types/chalk": "^2.2.4", |
There was a problem hiding this comment.
@types/chalk is deprecated (chalk v5 ships its own types) and adds an unnecessary dependency. Also, chalk itself doesn't appear to be used in the repo. Remove @types/chalk (and possibly chalk) unless there is a concrete usage added in this PR.
| # Fork Information | ||
|
|
||
| ## Purpose of This Fork | ||
|
|
||
| This fork adds back the **adblocker plugin** and implements a new **ytdlp-based downloader plugin** that provides more reliable downloading functionality compared to the previous youtube.js implementation. | ||
|
|
||
| ### Key Changes | ||
| - **Adblocker Plugin**: Restores ad-blocking capabilities using Cliqz adblocker | ||
| - **Downloader (ytdlp)**: New implementation using yt-dlp for robust video/audio downloads | ||
|
|
||
| ## Keeping Your Feature Branch Updated | ||
|
|
||
| **To keep your feature branch updated with upstream changes:** | ||
| ```bash | ||
| git checkout master | ||
| git fetch upstream | ||
| git merge upstream/master | ||
| git push origin master | ||
| git checkout feature/adblocker-and-downloader-plugins | ||
| git rebase master # Cleanly applies your changes on top of latest master | ||
| git push --force-with-lease origin feature/adblocker-and-downloader-plugins | ||
| ``` | ||
|
|
||
| **When ready to create a PR to upstream:** | ||
| 1. Make sure your feature branch is rebased on latest upstream/master (steps above) | ||
| 2. Push to your fork: `git push origin feature/adblocker-and-downloader-plugins` | ||
| 3. Go to https://github.com/pear-devs/pear-desktop and create a PR from your fork's branch | ||
|
|
||
| Using `rebase` keeps a clean, linear history which is preferred for PRs. | ||
|
|
||
| --- | ||
|
|
There was a problem hiding this comment.
The new README header contains fork-specific instructions and links to a different repository (pear-devs/pear-desktop). This looks unrelated to the features in this PR and could confuse users of this repo. Consider moving fork maintenance notes to a separate document (or removing them) so README stays project-focused.
| # Fork Information | |
| ## Purpose of This Fork | |
| This fork adds back the **adblocker plugin** and implements a new **ytdlp-based downloader plugin** that provides more reliable downloading functionality compared to the previous youtube.js implementation. | |
| ### Key Changes | |
| - **Adblocker Plugin**: Restores ad-blocking capabilities using Cliqz adblocker | |
| - **Downloader (ytdlp)**: New implementation using yt-dlp for robust video/audio downloads | |
| ## Keeping Your Feature Branch Updated | |
| **To keep your feature branch updated with upstream changes:** | |
| ```bash | |
| git checkout master | |
| git fetch upstream | |
| git merge upstream/master | |
| git push origin master | |
| git checkout feature/adblocker-and-downloader-plugins | |
| git rebase master # Cleanly applies your changes on top of latest master | |
| git push --force-with-lease origin feature/adblocker-and-downloader-plugins | |
| ``` | |
| **When ready to create a PR to upstream:** | |
| 1. Make sure your feature branch is rebased on latest upstream/master (steps above) | |
| 2. Push to your fork: `git push origin feature/adblocker-and-downloader-plugins` | |
| 3. Go to https://github.com/pear-devs/pear-desktop and create a PR from your fork's branch | |
| Using `rebase` keeps a clean, linear history which is preferred for PRs. | |
| --- |
|
Nobody wants to review 5K lines of AI generated code. |
🎯 Overview
This branch combines three major feature sets: a unified downloader experience, a Windows 11 taskbar mini-player widget, and a real-time audio visualizer rendered directly above the taskbar. Built on top of

feature/combined-downloader-widget(PR #7).📦 Feature 1: Unified Downloader Implementation
Key Changes
Files Changed
src/menu.ts- Menu restructuring (73 lines added)src/plugins/downloader/- Toast notification implementationsrc/plugins/adblocker/- Complete adblocker implementation (~550 lines)src/plugins/downloader-ytdlp/- New ytdlp downloader (~2,100 lines)🎨 Feature 2: Windows 11 Taskbar Widget
Key Features
Files Added/Modified
src/plugins/taskbar-widget/index.ts- Plugin configurationsrc/plugins/taskbar-widget/main.ts- Core widget implementation🔊 Feature 3: Taskbar Audio Visualizer (NEW)
Key Features
Visualizer.csfrequency processing algorithm1 + progress × 75smoothingTimeConstant=0for raw FFT datasetIntervalinstead ofrequestAnimationFrameand disableswebContents.setBackgroundThrottlingto maintain smooth ~30fps even when the main window is minimizedVisualizer Commits
8e7cd36feat: add taskbar audio visualizer with configurable bars, sensitivity, and positioning0c86df5fix: improve visualizer frequency mapping, strip artist from title, add year display, add blur opacity and visualizer width config options5b2e209feat: port FluentFlyout's visualizer algorithm for accurate bar distribution7c7daecfix: update visualizer defaults, add range labels, fix freeze on minimizec72522afix: disable background throttling when visualizer is activeFiles Changed (Visualizer-specific)
src/plugins/taskbar-widget/index.ts(+368 lines) - Config types, menu entries, renderer audio analysissrc/plugins/taskbar-widget/main.ts(+567 lines) - Visualizer BrowserWindow, FluentFlyout algorithm, IPCsrc/i18n/resources/en.json(+20 lines) - Visualizer menu strings with range labels📊 Combined Statistics
🧪 Testing
Unified Downloader
Taskbar Widget
Audio Visualizer
🚀 Usage
Taskbar Audio Visualizer
None — all features are additive and can be enabled/disabled via plugin settings.