Skip to content

feat: Unified Downloader + Taskbar Widget + Audio Visualizer#4350

Open
digitalnomad91 wants to merge 44 commits intopear-devs:masterfrom
digitalnomad91:feature/taskbar-visualizer
Open

feat: Unified Downloader + Taskbar Widget + Audio Visualizer#4350
digitalnomad91 wants to merge 44 commits intopear-devs:masterfrom
digitalnomad91:feature/taskbar-visualizer

Conversation

@digitalnomad91
Copy link

@digitalnomad91 digitalnomad91 commented Feb 28, 2026

🎯 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).
image

📦 Feature 1: Unified Downloader Implementation

Key Changes

  • Consolidated Menu Entry: Groups existing downloader plugins under a single "Downloader" menu entry with labeled submenus
  • Toast Notifications: Replaces blocking dialog boxes with non-intrusive in-app toast notifications for download errors and playlist events
  • Ad Blocker Plugin Restoration: Re-implements ad-blocking capabilities using Cliqz adblocker
  • ytdlp-based Downloader: New implementation using yt-dlp for more reliable downloads

Files Changed

  • src/menu.ts - Menu restructuring (73 lines added)
  • src/plugins/downloader/ - Toast notification implementation
  • src/plugins/adblocker/ - Complete adblocker implementation (~550 lines)
  • src/plugins/downloader-ytdlp/ - New ytdlp downloader (~2,100 lines)
  • Translation strings added across 48 language files

🎨 Feature 2: Windows 11 Taskbar Widget

Key Features

  • Native Taskbar Integration: Embeds mini player directly into Windows 11 taskbar
  • Dynamic Album Art: Displays current track artwork with adaptive blur effects
  • Full Playback Controls: Play/pause, previous, next track buttons
  • Smart Window Management: Sophisticated z-order handling for Start menu compatibility
  • Auto-Hide Taskbar Support: Works seamlessly with Windows auto-hide feature
  • Multi-Monitor Support: Configure which display shows the widget
  • Click-to-Focus: Clicking widget brings main app to foreground

Files Added/Modified

  • src/plugins/taskbar-widget/index.ts - Plugin configuration
  • src/plugins/taskbar-widget/main.ts - Core widget implementation
  • Translation strings and configuration options

🔊 Feature 3: Taskbar Audio Visualizer (NEW)

Key Features

  • Real-Time Audio Bars: Renders a configurable bar visualizer directly above the taskbar widget
  • FluentFlyout Algorithm Port: Faithful port of FluentFlyout's Visualizer.cs frequency processing algorithm
    • Logarithmic frequency binning (40–8000 Hz)
    • Max amplitude per band
    • Linear boost: 1 + progress × 75
    • dB conversion with configurable min/max range
    • Asymmetric smoothing: instant attack, 0.8/0.2 weighted decay
  • Web Audio API Integration: AnalyserNode with fftSize=1024, smoothingTimeConstant=0 for raw FFT data
  • IPC Audio Pipeline: Renderer (setInterval@33ms) → Main Process → Visualizer BrowserWindow
  • Background-Safe Rendering: Uses setInterval instead of requestAnimationFrame and disables webContents.setBackgroundThrottling to maintain smooth ~30fps even when the main window is minimized
  • Configurable Options:
    • Bar count (4–64, default: 64)
    • Audio sensitivity (0.01–1.0, default: 0.01)
    • Audio peak threshold (0.1–1.0, default: 1.0)
    • Visualizer width (40–300px, default: 84px)
    • Blur opacity (0.1–1.0, default: 0.5)
    • Centered bars, baseline toggle, bar gap, bar radius, colors
  • Title Deduplication: Strips duplicate artist name from track title display
  • Year Display: Shows release year after artist name

Visualizer Commits

  • 8e7cd36 feat: add taskbar audio visualizer with configurable bars, sensitivity, and positioning
  • 0c86df5 fix: improve visualizer frequency mapping, strip artist from title, add year display, add blur opacity and visualizer width config options
  • 5b2e209 feat: port FluentFlyout's visualizer algorithm for accurate bar distribution
  • 7c7daec fix: update visualizer defaults, add range labels, fix freeze on minimize
  • c72522a fix: disable background throttling when visualizer is active

Files Changed (Visualizer-specific)

  • src/plugins/taskbar-widget/index.ts (+368 lines) - Config types, menu entries, renderer audio analysis
  • src/plugins/taskbar-widget/main.ts (+567 lines) - Visualizer BrowserWindow, FluentFlyout algorithm, IPC
  • src/i18n/resources/en.json (+20 lines) - Visualizer menu strings with range labels

📊 Combined Statistics

  • Files Changed: 70+ files
  • Total Additions: ~5,200+ lines (4,280 from base + 915 visualizer)
  • New Plugins: 3 (adblocker, downloader-ytdlp, taskbar-widget)
  • Commits: 30+ across all branches
  • Languages Updated: 48 translation files

🧪 Testing

Unified Downloader

  • ✅ Toast notification display
  • ✅ Menu consolidation and navigation
  • ✅ Ad blocking functionality
  • ✅ ytdlp download reliability

Taskbar Widget

  • ✅ Start menu interaction (z-order management)
  • ✅ Auto-hide taskbar compatibility
  • ✅ Multi-monitor support
  • ✅ Album art and blur effects
  • ✅ Playback control responsiveness
  • ✅ Click-to-focus functionality

Audio Visualizer

  • ✅ Smooth rendering at ~30fps
  • ✅ Accurate frequency distribution (FluentFlyout algorithm)
  • ✅ Smooth playback when main window minimized (background throttling disabled)
  • ✅ All config options functional via menu
  • ✅ Proper cleanup on disable/exit

🚀 Usage

Taskbar Audio Visualizer

  1. Enable "Taskbar Widget" plugin in settings
  2. Enable "Enable Visualizer" in the taskbar widget submenu
  3. Adjust bar count, sensitivity, width, colors etc. via submenu
  4. Visualizer renders above the taskbar widget in real-time

⚠️ Breaking Changes

None — all features are additive and can be enabled/disabled via plugin settings.


🍐 Vibe Coded with GitHub Copilot

digitalnomad91 and others added 30 commits January 3, 2026 21:18
…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>
Copilot AI and others added 12 commits February 28, 2026 05:51
…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.
Copilot AI review requested due to automatic review settings February 28, 2026 08:58

const pluginMenus = await Promise.all(
availablePlugins
.filter((id) => !downloaderIds.includes(id as typeof downloaderIds[number]))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace (id)·=>·!downloaderIds.includes(id·as·typeof·downloaderIds[number]) with ⏎········(id)·=>·!downloaderIds.includes(id·as·(typeof·downloaderIds)[number]),⏎······

Suggested change
.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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace predefinedTemplate[1]·as·Electron.MenuItemConstructorOptions with ⏎········predefinedTemplate[1]·as·Electron.MenuItemConstructorOptions⏎······

Suggested change
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/no-unnecessary-type-assertion> reported by reviewdog 🐶
This assertion is unnecessary since it does not change the type of the expression.

Suggested change
const template = (predefinedTemplate[1] as Electron.MenuItemConstructorOptions).submenu;
const template = (predefinedTemplate[1]).submenu;

Comment on lines +201 to +203
const validDownloaderSubmenus: Electron.MenuItemConstructorOptions[] = downloaderSubmenus.filter(
(s): s is NonNullable<typeof s> => s !== null,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [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

Suggested change
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 ?? ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace item·as·Electron.MenuItemConstructorOptions).label·??·'' with (item·as·Electron.MenuItemConstructorOptions).label·??·'')

Suggested change
? (item as Electron.MenuItemConstructorOptions).label ?? ''
? ((item as Electron.MenuItemConstructorOptions).label ?? '')

},
),
detail: t(
{ playlistTitle },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace {·playlistTitle·} with ··playlistTitle

Suggested change
{ playlistTitle },
playlistTitle,

),
detail: t(
{ playlistTitle },
) + ' ' + t(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace ····)·+·'·'·+·t( with ······})·+⏎······'·'·+

Suggested change
) + ' ' + t(
}) +
' ' +

},
),
});
{ playlistSize: items.length },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace {·playlistSize:·items.length·} with ··playlistSize:·items.length

Suggested change
{ playlistSize: items.length },
playlistSize: items.length,

),
});
{ playlistSize: items.length },
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Insert ··}

Suggested change
);
});

);
win.webContents.send('downloader-error-toast', {
message: playlistMessage,
title: t('plugins.downloader.backend.dialog.start-download-playlist.title'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐶
Replace 'plugins.downloader.backend.dialog.start-download-playlist.title' with ⏎········'plugins.downloader.backend.dialog.start-download-playlist.title',⏎······

Suggested change
title: t('plugins.downloader.backend.dialog.start-download-playlist.title'),
title: t(
'plugins.downloader.backend.dialog.start-download-playlist.title',
),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/await-thenable> reported by reviewdog 🐶
Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.

pear-desktop/src/menu.ts

Lines 132 to 159 in c72522a

availablePlugins
.filter((id) => !downloaderIds.includes(id as typeof downloaderIds[number]))
.sort((a, b) => {
const aPluginLabel = allPluginsStubs[a]?.name?.() ?? a;
const bPluginLabel = allPluginsStubs[b]?.name?.() ?? b;
return aPluginLabel.localeCompare(bPluginLabel);
})
.map((id) => {
const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1];
const plugin = allPluginsStubs[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
return pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
isNew,
true,
innerRefreshMenu,
);
}),

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-ytdlp plugin 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.

Comment on lines +592 to +595
} else {
// Assume 44100 Hz sample rate; fftSize = dataLen * 2
const sampleRate = 44100;
const fftSize = dataLen * 2;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +403 to +466
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;
},
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +464 to +480
// 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,
];
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +129
// 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)',
};
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1348 to +1351

// Restore normal background throttling
if (mainWindowRef && !mainWindowRef.isDestroyed()) {
mainWindowRef.webContents.setBackgroundThrottling(true);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +521 to +527
// 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);
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +451
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
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +616 to +618
const playlistId =
getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl));

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 146 to +149
"@playwright/test": "1.58.2",
"@stylistic/eslint-plugin": "5.7.1",
"@total-typescript/ts-reset": "0.6.1",
"@types/chalk": "^2.2.4",
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +32
# 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.

---

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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.
---

Copilot uses AI. Check for mistakes.
@ArjixWasTaken
Copy link
Member

Nobody wants to review 5K lines of AI generated code.
Either split the code into small feature branches and open separate PRs, or accept that there is no guarantee this will ever be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants