Skip to content

feat(plugins): add Slowed playback plugin#4400

Draft
The-Kryz wants to merge 8 commits intopear-devs:masterfrom
The-Kryz:plugin-slowed-oficial
Draft

feat(plugins): add Slowed playback plugin#4400
The-Kryz wants to merge 8 commits intopear-devs:masterfrom
The-Kryz:plugin-slowed-oficial

Conversation

@The-Kryz
Copy link
Copy Markdown

@The-Kryz The-Kryz commented Apr 1, 2026

Description:
This PR introduces a lightweight "Slowed" plugin that adds a floating UI to control playback rate and pitch preservation, allowing users to easily simulate Slowed or Nightcore effects directly in the app.

Technical Decisions:
I intentionally avoided using the Web Audio API (AudioContext, BiquadFilterNode) for Reverb or Bass effects. Since the browser only allows the audio stream to be captured by createMediaElementSource once, doing so would cause InvalidStateError conflicts with existing plugins like the native Equalizer or Crossfade.

By relying solely on HTMLMediaElement.playbackRate and preservesPitch, this plugin is 100% compatible with the rest of the ecosystem and won't break other audio plugins.

Summary by CodeRabbit

  • New Features

    • Added a "Slowed & Nightcore" plugin with an interactive floating control panel
    • Playback speed slider (0.5x–1.5x), pitch preservation toggle, and preset buttons for Slowed, Nightcore, and Reset effects
    • Settings persist across track changes
  • Documentation

    • Added plugin documentation describing core capabilities and compatibility

@ArjixWasTaken
Copy link
Copy Markdown
Member

ArjixWasTaken commented Apr 1, 2026

By relying solely on HTMLMediaElement.playbackRate and preservesPitch, this plugin is 100% compatible with the rest of the ecosystem and won't break other audio plugins.

Maybe we should introduce a unified way for all plugins to use the audio context, w/o having conflicts.
Just noting this as a future improvement.

Copy link
Copy Markdown
Member

@ArjixWasTaken ArjixWasTaken left a comment

Choose a reason for hiding this comment

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

In the past, I'd say LGTM, but since we moved strictly to using solidjs a while ago, I unfortunately have to ask you to make changes.

Otherwise, nice job

Comment thread src/plugins/slowed/index.ts Outdated
Comment thread src/plugins/slowed/index.ts Outdated
Comment thread src/plugins/slowed/index.ts Outdated
@ArjixWasTaken
Copy link
Copy Markdown
Member

ArjixWasTaken commented Apr 1, 2026

PS: If you need help with the solidjs stuff, I can help.

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

Done! I've fully refactored the plugin to follow the SolidJS standards:

Replaced manual DOM injection with Solid's render and JSX.

Implemented createSignal and createEffect for reactive state management.

Switched to the native stylesheets option for CSS.

Added a proper stop() method and onCleanup to ensure all intervals and UI elements are destroyed when the plugin is disabled.

Thanks for the guidance, @ArjixWasTaken! Let me know if there's anything else to adjust.

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

And I fixed some bugs BTW xD

Copy link
Copy Markdown
Member

@ArjixWasTaken ArjixWasTaken left a comment

Choose a reason for hiding this comment

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

Some more stuff I could point out using my phone

Comment thread package.json Outdated
Comment thread src/plugins/slowed/index.tsx Outdated
@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

Sorry about the changes to package.json! I was encountering a TypeError: Cannot read properties of undefined (reading 'date') error in my local Windows environment when trying to compile the application, and adding ajv-formats was a workaround I tried to fix the problem.

I am now reverting package.json and pnpm-lock.yaml to their original states, as I realized it was a problem in my local environment that shouldn't affect the main repository. Thank you for noticing this!

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

All set! I've replaced the conditional rendering with the component to ensure reactivity works as intended in SolidJS. I also double-checked the cleanup logic and it's working smoothly now.

Everything should be in line with the project's standards now. Thanks, @ArjixWasTaken!

Copy link
Copy Markdown
Member

@ArjixWasTaken ArjixWasTaken left a comment

Choose a reason for hiding this comment

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

Other than this, everything looks good to me.

Comment thread src/plugins/slowed/index.tsx Outdated
@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

All done!! thks again @ArjixWasTaken!

@ArjixWasTaken
Copy link
Copy Markdown
Member

Great, I'll check this out on my computer once I have the time, to see if there are any bugs left.

@ArjixWasTaken
Copy link
Copy Markdown
Member

Just a note: this is an electron app, aka chromium, we can't use Firefox/safari specific stuff.

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

Fixed!

@ImMALWARE
Copy link
Copy Markdown

I realized it was a problem in my local environment

Is it really problem in your local environment? I get same error when building. And even Actions in this repo get it xd
https://github.com/pear-devs/pear-desktop/actions/runs/23715577687/job/69082005570

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 1, 2026

Honestly, I'm relieved to see that the compilation error is also happening in the Actions. I spent a lot of time thinking the problem was my Windows environment! 😂

Apparently it seems to be a problem across the entire repository; I reverted all my workarounds in package.json to keep this PR clean. I've officially given up on fixing the build environment for now and am now fully committed to pnpm start! It works on my machine, and that's where the magic happens.

While the plugin isn't in the main, I'm using it like this to listen to my music xD

Copy link
Copy Markdown
Author

@The-Kryz The-Kryz left a comment

Choose a reason for hiding this comment

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

Finished

@ArjixWasTaken
Copy link
Copy Markdown
Member

ArjixWasTaken commented Apr 8, 2026

Plugin works fine and I don't see any obvious bugs.
But I am not a fan of the floating control

@The-Kryz would it be ok to you if I rework the UI?

PS: The reason I am not a fan of it, is because it is too present, if other plugins also did that, it'd make a mess of the screen real estate. It'd be better if it was integrated with the player controls.

@The-Kryz
Copy link
Copy Markdown
Author

The-Kryz commented Apr 8, 2026

Hey @ArjixWasTaken! Sure, you can go ahead and rework the UI, no problem at all. I totally understand your point about the screen real estate, and integrating it into the player controls makes a lot of sense. Glad the core logic is working fine! Feel free to make any necessary changes.

@JellyBrick JellyBrick added the enhancement New feature or request label Apr 11, 2026
Comment on lines +108 to +109
// @ts-ignore
video.webkitPreservesPitch = true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[ditto]

Comment on lines +95 to +96
// @ts-ignore - Prefixos para garantir funcionamento no Electron/Chromium
video.webkitPreservesPitch = keepPitch();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This statement is unnecessary.

const [keepPitch, setKeepPitch] = createSignal(safeConfig.keepPitch ?? false);
const [collapsed, setCollapsed] = createSignal(false);

const getVideo = () => document.querySelector<HTMLVideoElement>('video');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do not use document.querySelector<HTMLVideoElement>('video') in start

cleanup: null as (() => void) | null,

start({ config, setConfig }) {
const safeConfig = config || DEFAULT_CONFIG;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

config is always non-null.


const getVideo = () => document.querySelector<HTMLVideoElement>('video');

document.getElementById('sr-panel')?.remove();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the purpose of this statement?

Copy link
Copy Markdown
Member

@ArjixWasTaken ArjixWasTaken Apr 12, 2026

Choose a reason for hiding this comment

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

I'll rework the UI either way, so it's pointless to review it.
ig this should be marked as a draft

</div>

<div class="sr-row sr-pitch-row">
<span class="sr-label">Keep pitch</span>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i18n

Comment on lines +123 to +124
// @ts-ignore
video.webkitPreservesPitch = p;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is this file?

@JellyBrick
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

A new "Slowed" plugin is added enabling video playback speed control (0.5–1.5x) and pitch preservation through a floating control panel. The plugin initializes reactive state, injects a collapsible UI panel into the DOM, polls for video elements, synchronizes playback properties, and persists settings across track changes.

Changes

Cohort / File(s) Summary
Slowed Plugin Implementation
src/plugins/slowed/index.tsx, src/plugins/slowed/style.css, src/plugins/slowed/readme.md
New SolidJS-based plugin with reactive state for speed and pitch settings. Registers plugin with inline CSS, injects a floating control panel with preset buttons, speed slider (0.5–1.5), pitch toggle, and header collapse functionality. Polling interval (500ms) locates video elements and updates playbackRate and preservesPitch properties. Effects synchronize video properties reactively and persist configuration. Cleanup removes panel, disposes render, and resets video settings. CSS provides styled panel UI with fixed positioning, gradient bars, slider with fill variable, and custom toggle switch. Readme documents plugin capabilities and Web Audio API avoidance.

Sequence Diagram

sequenceDiagram
    participant Plugin as Slowed Plugin
    participant SolidJS as SolidJS Framework
    participant DOM as Document/DOM
    participant Video as Video Element
    participant Config as Config System

    rect rgba(100, 150, 200, 0.5)
        Note over Plugin,Config: Plugin Initialization
        Plugin->>Plugin: createPlugin() registration
        Plugin->>SolidJS: createSignal() for speed & keepPitch
        Plugin->>DOM: Remove existing `#sr-panel`
        Plugin->>DOM: Create & inject new `#sr-panel`
        Plugin->>SolidJS: createEffect() for reactivity
    end

    rect rgba(150, 200, 100, 0.5)
        Note over Plugin,Video: Runtime - Polling Loop (500ms)
        Plugin->>DOM: Query first video element
        SolidJS->>Video: Read speed signal
        SolidJS->>Video: Read keepPitch signal
        Video->>Video: Update playbackRate
        Video->>Video: Update preservesPitch property
    end

    rect rgba(200, 150, 100, 0.5)
        Note over Plugin,Config: Persistence & Sync
        SolidJS->>SolidJS: Detect speed/pitch changes
        SolidJS->>Config: Call setConfig() with new values
        Config->>Config: Persist settings
    end

    rect rgba(150, 100, 200, 0.5)
        Note over Plugin,DOM: Cleanup on Stop
        Plugin->>Plugin: Clear polling interval
        Plugin->>SolidJS: Dispose Solid render
        Plugin->>DOM: Remove `#sr-panel`
        Plugin->>Video: Reset playbackRate to 1.0
        Plugin->>Video: Reset preservesPitch to true
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 wiggles nose at the floating panel
A slowed plugin bounds into place,
With speed and pitch controls to embrace,
Preset buttons and a slider so fine,
Video playback bends to your design!
From rabbit's burrow to screen so bright,
We've crafted effects pure and right! 🎬✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(plugins): add Slowed playback plugin' accurately and concisely describes the main change: introducing a new plugin for slowed/nightcore playback effects.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/slowed/index.tsx`:
- Around line 100-110: doCleanup is force-resetting video playback properties to
hardcoded defaults; instead capture the video's original state when you first
modify it (e.g., originalPlaybackRate, originalPreservesPitch,
originalWebkitPreservesPitch stored on the video element or in a Map keyed by
the element) and in doCleanup (where you call getVideo(),
clearInterval(interval), dispose(), panel.remove()) restore those saved values
only if they exist; ensure you only touch the same properties you changed and
safely handle missing/removed video elements.
- Around line 29-43: The start() function currently only removes the old DOM
node ('sr-panel') but leaves the previous Solid root, intervals and mutations
running; modify start() to first detect and tear down any existing plugin
instance by invoking the plugin's stop() (or calling the existing teardown
function) before proceeding: ensure any running interval timers are cleared, any
Solid root or reactive roots are disposed, and any event listeners or mutations
set up by start()/getVideo are removed; then create the new panel and initialize
signals as before so multiple start() calls don't leak background work.
- Around line 128-130: The effect currently calls setConfig on every speed()
change which can queue many async writes during slider drags; debounce updates
and handle the returned Promise: replace the immediate createEffect -> setConfig
call with a debounced updater that waits e.g. 100–250ms after the last
speed()/keepPitch() change before calling setConfig({ speed: speed(), keepPitch:
keepPitch() }), await the Promise and catch/log any rejection, and ensure rapid
intermediate ticks cancel the pending debounce so only the final value is
persisted (refer to createEffect, speed, keepPitch, and setConfig to locate the
code).
- Line 46: Replace the non-semantic <div class="sr-header"> click target with a
real <button> that calls setCollapsed(!collapsed()) (same toggle logic), add
aria-expanded={!collapsed()} to expose state to AT, and either add
id="sr-panel-body" to the <div class="sr-body"> or remove aria-controls from the
button (aria-expanded is required, aria-controls is optional) so the toggle is
keyboard-accessible and references a real ID if used.
- Around line 59-76: The range and checkbox inputs lack accessible associations
with their visible text; add explicit labels by giving the slider and checkbox
unique ids (e.g., "speed-slider", "keep-pitch-checkbox") and reference the
corresponding visible text spans using aria-labelledby (pointing to the ids of
the "Speed" sr-label span and the "Keep pitch" sr-label span), or alternatively
wrap each input in a <label> that contains the visible text; update the JSX
where speed(), setSpeed, keepPitch(), and setKeepPitch() are used so the inputs
include the new id and aria-labelledby attributes to link them to the existing
sr-label elements.
- Around line 93-96: Replace the three usages of "// `@ts-ignore`" by adding a
small typed helper that exposes webkitPreservesPitch on media elements (e.g.,
declare an interface HTMLMediaElementWithWebkit { webkitPreservesPitch?: boolean
} or a helper function setWebkitPreservesPitch(el: HTMLMediaElement, val:
boolean)) and use that helper where you currently set video.webkitPreservesPitch
in src/plugins/slowed/index.tsx (the blocks that check keepPitch() and the later
occurrences around lines 108–109 and 123–124); ensure you cast the
HTMLMediaElement to the typed interface or call the helper so the assignment is
type-safe and remove the ts-ignore comments.

In `@src/plugins/slowed/readme.md`:
- Around line 11-12: Replace the absolute claim about "100% compatibility" in
the README sentence that references HTML5 <video> element properties
(`playbackRate` and `preservesPitch`) with a softer statement: state that the
plugin "avoids using the Web Audio API and is designed to minimize conflicts
with other audio-hijacking plugins" or similar, and remove or reword the phrase
promising guaranteed compatibility so it no longer asserts complete
compatibility with every plugin.

In `@src/plugins/slowed/style.css`:
- Around line 140-206: The slider removes the native outline and the hidden
checkbox for the switch has no visible replacement, breaking keyboard focus
visibility; restore accessible focus indicators by adding focus styles (use
:focus-visible) for .sr-slider to show an outline or glow (e.g., box-shadow or
outline) and add focus-visible/on-focus rules that target the switch’s visual
thumb (e.g., .sr-switch input:focus-visible + .sr-thumb or
.sr-switch:focus-within .sr-thumb) to draw a clear focus ring/border when the
invisible input receives keyboard focus; ensure the focus styles are subtle,
high-contrast, and animate consistently with existing transitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33ca0430-2530-41b2-aee2-f8003f8d6781

📥 Commits

Reviewing files that changed from the base of the PR and between e2eba63 and 2326edd.

📒 Files selected for processing (3)
  • src/plugins/slowed/index.tsx
  • src/plugins/slowed/readme.md
  • src/plugins/slowed/style.css

Comment on lines +29 to +43
start({ config, setConfig }) {
const safeConfig = config || DEFAULT_CONFIG;

const [speed, setSpeed] = createSignal(safeConfig.speed ?? 1.0);
const [keepPitch, setKeepPitch] = createSignal(safeConfig.keepPitch ?? false);
const [collapsed, setCollapsed] = createSignal(false);

const getVideo = () => document.querySelector<HTMLVideoElement>('video');

document.getElementById('sr-panel')?.remove();

const panel = document.createElement('div');
panel.id = 'sr-panel';
document.body.appendChild(panel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tear down the previous plugin instance before mounting a new one.

Line 38 only removes the old panel node. If start() runs again without stop(), the previous interval and Solid root keep running and continue mutating the player in the background.

Suggested guard at the top of start()
     start({ config, setConfig }) {
+      this.cleanup?.();
+      this.cleanup = null;
+
       const safeConfig = config || DEFAULT_CONFIG;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
start({ config, setConfig }) {
const safeConfig = config || DEFAULT_CONFIG;
const [speed, setSpeed] = createSignal(safeConfig.speed ?? 1.0);
const [keepPitch, setKeepPitch] = createSignal(safeConfig.keepPitch ?? false);
const [collapsed, setCollapsed] = createSignal(false);
const getVideo = () => document.querySelector<HTMLVideoElement>('video');
document.getElementById('sr-panel')?.remove();
const panel = document.createElement('div');
panel.id = 'sr-panel';
document.body.appendChild(panel);
start({ config, setConfig }) {
this.cleanup?.();
this.cleanup = null;
const safeConfig = config || DEFAULT_CONFIG;
const [speed, setSpeed] = createSignal(safeConfig.speed ?? 1.0);
const [keepPitch, setKeepPitch] = createSignal(safeConfig.keepPitch ?? false);
const [collapsed, setCollapsed] = createSignal(false);
const getVideo = () => document.querySelector<HTMLVideoElement>('video');
document.getElementById('sr-panel')?.remove();
const panel = document.createElement('div');
panel.id = 'sr-panel';
document.body.appendChild(panel);
🧰 Tools
🪛 ESLint

[error] 31-31: Delete ······

(prettier/prettier)


[error] 33-33: Replace safeConfig.keepPitch·??·false with ⏎········safeConfig.keepPitch·??·false,⏎······

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` around lines 29 - 43, The start() function
currently only removes the old DOM node ('sr-panel') but leaves the previous
Solid root, intervals and mutations running; modify start() to first detect and
tear down any existing plugin instance by invoking the plugin's stop() (or
calling the existing teardown function) before proceeding: ensure any running
interval timers are cleared, any Solid root or reactive roots are disposed, and
any event listeners or mutations set up by start()/getVideo are removed; then
create the new panel and initialize signals as before so multiple start() calls
don't leak background work.


const dispose = render(() => (
<div class="sr-container">
<div class="sr-header" onClick={() => setCollapsed(!collapsed())} style="cursor: pointer;">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/plugins/slowed/index.tsx | head -100

Repository: pear-devs/pear-desktop

Length of output: 4262


🏁 Script executed:

cat -n src/plugins/slowed/index.tsx | tail -20

Repository: pear-devs/pear-desktop

Length of output: 585


Use a real button for the collapse toggle with proper ARIA attributes.

The header is currently a non-semantic <div> with an onClick handler, which is not keyboard-operable and doesn't expose the expanded/collapsed state to assistive technologies.

Replace it with a <button> element and add aria-expanded={!collapsed()} to expose the state. However, the suggested aria-controls="sr-panel-body" references an ID that doesn't exist—the panel body (<div class="sr-body">) has no ID. Either add id="sr-panel-body" to the body element or remove the aria-controls attribute (it's optional and aria-expanded is the critical attribute for keyboard accessibility).

🧰 Tools
🪛 ESLint

[error] 46-46: Replace <div·class="sr-header"·onClick={()·=>·setCollapsed(!collapsed())}·style="cursor:·pointer;" with ··<div⏎··············class="sr-header"⏎··············onClick={()·=>·setCollapsed(!collapsed())}⏎··············style="cursor:·pointer;"⏎············

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` at line 46, Replace the non-semantic <div
class="sr-header"> click target with a real <button> that calls
setCollapsed(!collapsed()) (same toggle logic), add aria-expanded={!collapsed()}
to expose state to AT, and either add id="sr-panel-body" to the <div
class="sr-body"> or remove aria-controls from the button (aria-expanded is
required, aria-controls is optional) so the toggle is keyboard-accessible and
references a real ID if used.

Comment on lines +59 to +76
<div class="sr-row">
<div class="sr-label-row">
<span class="sr-label">Speed</span>
<span class="sr-val">{speed().toFixed(2)}x</span>
</div>
<input
class="sr-slider"
type="range" min="0.5" max="1.5" step="0.01"
value={speed()}
onInput={(e) => setSpeed(parseFloat(e.currentTarget.value))}
style={{ '--fill': `${((speed() - 0.5) / (1.5 - 0.5)) * 100}%` }}
/>
</div>

<div class="sr-row sr-pitch-row">
<span class="sr-label">Keep pitch</span>
<label class="sr-switch">
<input type="checkbox" checked={keepPitch()} onChange={(e) => setKeepPitch(e.currentTarget.checked)} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Associate the visible labels with the slider and checkbox.

Right now assistive tech will see an unlabeled range input and an unlabeled checkbox, because the visible text lives in separate <span> elements.

Suggested association via aria-labelledby
-                  <span class="sr-label">Speed</span>
+                  <span id="sr-speed-label" class="sr-label">Speed</span>
@@
-                <input 
+                <input
+                  aria-labelledby="sr-speed-label"
                   class="sr-slider" 
                   type="range" min="0.5" max="1.5" step="0.01" 
                   value={speed()} 
@@
-                <span class="sr-label">Keep pitch</span>
+                <span id="sr-pitch-label" class="sr-label">Keep pitch</span>
                 <label class="sr-switch">
-                  <input type="checkbox" checked={keepPitch()} onChange={(e) => setKeepPitch(e.currentTarget.checked)} />
+                  <input
+                    type="checkbox"
+                    aria-labelledby="sr-pitch-label"
+                    checked={keepPitch()}
+                    onChange={(e) => setKeepPitch(e.currentTarget.checked)}
+                  />
🧰 Tools
🪛 ESLint

[error] 59-59: Insert ⏎··

(prettier/prettier)


[error] 60-60: Insert ··

(prettier/prettier)


[error] 61-61: Insert ··

(prettier/prettier)


[error] 62-62: Insert ··

(prettier/prettier)


[error] 63-63: Replace ················ with ··················

(prettier/prettier)


[error] 64-68: Replace <input·⏎··················class="sr-slider"·⏎··················type="range"·min="0.5"·max="1.5"·step="0.01"·⏎··················value={speed()}·⏎ with ··<input⏎····················class="sr-slider"⏎····················type="range"⏎····················min="0.5"⏎····················max="1.5"⏎····················step="0.01"⏎····················value={speed()}⏎··

(prettier/prettier)


[error] 66-66: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 66-66: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 66-66: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 68-68: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 69-69: Replace style={{·'--fill':·${((speed()·-·0.5)·/·(1.5·-·0.5))··100}%·}} with ··style={{⏎······················'--fill':·${((speed()·-·0.5)·/·(1.5·-·0.5))··100}%,

(prettier/prettier)


[error] 69-69: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 70-70: Replace /> with ····}}

(prettier/prettier)


[error] 71-71: Replace </div with ····/

(prettier/prettier)


[error] 72-72: Insert ··</div>

(prettier/prettier)


[error] 73-73: Insert ⏎··

(prettier/prettier)


[error] 74-74: Insert ··

(prettier/prettier)


[error] 75-75: Insert ··

(prettier/prettier)


[error] 76-76: Replace <input·type="checkbox"·checked={keepPitch()}·onChange={(e)·=>·setKeepPitch(e.currentTarget.checked)} with ··<input⏎······················type="checkbox"⏎······················checked={keepPitch()}⏎······················onChange={(e)·=>·setKeepPitch(e.currentTarget.checked)}⏎···················

(prettier/prettier)


[error] 76-76: Props should be sorted alphabetically

(stylistic/jsx-sort-props)


[error] 76-76: Props should be sorted alphabetically

(stylistic/jsx-sort-props)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` around lines 59 - 76, The range and checkbox
inputs lack accessible associations with their visible text; add explicit labels
by giving the slider and checkbox unique ids (e.g., "speed-slider",
"keep-pitch-checkbox") and reference the corresponding visible text spans using
aria-labelledby (pointing to the ids of the "Speed" sr-label span and the "Keep
pitch" sr-label span), or alternatively wrap each input in a <label> that
contains the visible text; update the JSX where speed(), setSpeed, keepPitch(),
and setKeepPitch() are used so the inputs include the new id and aria-labelledby
attributes to link them to the existing sr-label elements.

Comment on lines +93 to +96
if (video.preservesPitch !== keepPitch()) {
video.preservesPitch = keepPitch();
// @ts-ignore - Prefixos para garantir funcionamento no Electron/Chromium
video.webkitPreservesPitch = keepPitch();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and read the relevant file
wc -l src/plugins/slowed/index.tsx

Repository: pear-devs/pear-desktop

Length of output: 98


🏁 Script executed:

# Read the file to see the code at the mentioned lines
cat -n src/plugins/slowed/index.tsx | sed -n '85,130p'

Repository: pear-devs/pear-desktop

Length of output: 1654


🏁 Script executed:

# Check the TypeScript configuration to understand lib setup
cat -n tsconfig.json | head -30

Repository: pear-devs/pear-desktop

Length of output: 1076


🏁 Script executed:

# Also check what TypeScript version is being used
cat package.json | grep -A 5 -B 5 typescript

Repository: pear-devs/pear-desktop

Length of output: 1148


🏁 Script executed:

# Check TypeScript's DOM lib definitions for HTMLVideoElement
node -e "
const ts = require('typescript');
const sourceFile = ts.createSourceFile('lib.dom.d.ts', '', ts.ScriptTarget.Latest, true);
// Try to find HTMLVideoElement definition
const libPath = require.resolve('typescript');
const libDir = require('path').dirname(libPath);
const domLib = require('path').join(libDir, '../lib/lib.dom.d.ts');
const fs = require('fs');
const content = fs.readFileSync(domLib, 'utf-8');
// Extract HTMLVideoElement interface
const match = content.match(/interface HTMLVideoElement[^}]*preservesPitch[^}]*}/s);
if (match) console.log(match[0].substring(0, 2000));
" 2>/dev/null || echo "Need different approach"

Repository: pear-devs/pear-desktop

Length of output: 48


🏁 Script executed:

# Alternative: search for HTMLVideoElement typing in node_modules
find node_modules/typescript -name "lib.dom.d.ts" 2>/dev/null | head -1

Repository: pear-devs/pear-desktop

Length of output: 48


🏁 Script executed:

# Look at TypeScript definitions
grep -A 20 "interface HTMLVideoElement" node_modules/typescript/lib/lib.dom.d.ts 2>/dev/null | head -40

Repository: pear-devs/pear-desktop

Length of output: 1855


🏁 Script executed:

# Search for preservesPitch in DOM lib
grep -n "preservesPitch" node_modules/typescript/lib/lib.dom.d.ts 2>/dev/null || echo "Not found in standard DOM lib"

Repository: pear-devs/pear-desktop

Length of output: 438


🏁 Script executed:

# Search for webkitPreservesPitch in DOM lib
grep -n "webkitPreservesPitch" node_modules/typescript/lib/lib.dom.d.ts 2>/dev/null || echo "Not found in standard DOM lib"

Repository: pear-devs/pear-desktop

Length of output: 95


🏁 Script executed:

# Check ESLint configuration
cat -n .eslintrc.json 2>/dev/null || cat -n .eslintrc.js 2>/dev/null || echo "No explicit eslintrc found"

Repository: pear-devs/pear-desktop

Length of output: 92


🏁 Script executed:

# Look for ESLint config in package.json
grep -A 10 '"eslintConfig"' package.json

Repository: pear-devs/pear-desktop

Length of output: 48


🏁 Script executed:

# Check for TypeScript ESLint rules about `@ts-ignore`
grep -r "@ts-ignore\|ts-ignore" .eslintrc* 2>/dev/null || echo "No explicit rules found"

Repository: pear-devs/pear-desktop

Length of output: 89


🏁 Script executed:

# Check typescript-eslint version and common rules
npm ls typescript-eslint 2>/dev/null | grep typescript-eslint | head -5

Repository: pear-devs/pear-desktop

Length of output: 1620


🏁 Script executed:

# Check for eslint.config.js (flat config)
ls -la eslint.config.* 2>/dev/null || echo "No flat config found"

Repository: pear-devs/pear-desktop

Length of output: 132


🏁 Script executed:

# Look in src or root for any eslint config
find . -maxdepth 2 -name "eslint*" -type f 2>/dev/null

Repository: pear-devs/pear-desktop

Length of output: 85


🏁 Script executed:

# Check the ESLint flat config file
cat -n eslint.config.mjs

Repository: pear-devs/pear-desktop

Length of output: 4803


Replace @ts-ignore comments with a typed helper for webkitPreservesPitch.

The preservesPitch property is already typed on HTMLMediaElement, but webkitPreservesPitch is not in the standard DOM lib and requires a vendor type extension. Create a small typed helper and reuse it across lines 93–96, 108–109, and 123–124 to avoid suppressing type checking:

Suggested typed helper
+type PitchAwareVideo = HTMLVideoElement & {
+  webkitPreservesPitch?: boolean;
+};
+
+const setPitchPreservation = (video: PitchAwareVideo, enabled: boolean) => {
+  video.preservesPitch = enabled;
+  if ('webkitPreservesPitch' in video) {
+    video.webkitPreservesPitch = enabled;
+  }
+};
@@
-        if (video.preservesPitch !== keepPitch()) {
-          video.preservesPitch = keepPitch();
-          // `@ts-ignore` - Prefixos para garantir funcionamento no Electron/Chromium
-          video.webkitPreservesPitch = keepPitch();
-        }
+        if (video.preservesPitch !== keepPitch()) {
+          setPitchPreservation(video as PitchAwareVideo, keepPitch());
+        }
🧰 Tools
🪛 ESLint

[error] 95-95: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` around lines 93 - 96, Replace the three usages
of "// `@ts-ignore`" by adding a small typed helper that exposes
webkitPreservesPitch on media elements (e.g., declare an interface
HTMLMediaElementWithWebkit { webkitPreservesPitch?: boolean } or a helper
function setWebkitPreservesPitch(el: HTMLMediaElement, val: boolean)) and use
that helper where you currently set video.webkitPreservesPitch in
src/plugins/slowed/index.tsx (the blocks that check keepPitch() and the later
occurrences around lines 108–109 and 123–124); ensure you cast the
HTMLMediaElement to the typed interface or call the helper so the assignment is
type-safe and remove the ts-ignore comments.

Comment on lines +100 to +110
const doCleanup = () => {
clearInterval(interval);
dispose();
panel.remove();
const video = getVideo();
if (video) {
video.playbackRate = 1.0;
video.preservesPitch = true;
// @ts-ignore
video.webkitPreservesPitch = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hard-reset the media element to 1.0/true on cleanup.

Stopping the plugin currently overwrites any pre-existing playback settings, including user-selected speed or another plugin's values. Capture the previous state for the video you modify and restore that instead of forcing defaults.

🧰 Tools
🪛 ESLint

[error] 108-108: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` around lines 100 - 110, doCleanup is
force-resetting video playback properties to hardcoded defaults; instead capture
the video's original state when you first modify it (e.g., originalPlaybackRate,
originalPreservesPitch, originalWebkitPreservesPitch stored on the video element
or in a Map keyed by the element) and in doCleanup (where you call getVideo(),
clearInterval(interval), dispose(), panel.remove()) restore those saved values
only if they exist; ensure you only touch the same properties you changed and
safely handle missing/removed video elements.

Comment on lines +128 to +130
createEffect(() => {
setConfig({ speed: speed(), keepPitch: keepPitch() });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't persist every slider tick via raw setConfig().

createEffect runs for every speed() change, and setConfig can be async. Dragging the slider will queue a burst of writes and can silently drop rejections.

Suggested debounce and Promise handling
+      let persistTimer: number | undefined;
+
       createEffect(() => {
-        setConfig({ speed: speed(), keepPitch: keepPitch() });
+        clearTimeout(persistTimer);
+        const next = { speed: speed(), keepPitch: keepPitch() };
+        persistTimer = window.setTimeout(() => {
+          void Promise.resolve(setConfig(next)).catch(console.error);
+        }, 150);
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
createEffect(() => {
setConfig({ speed: speed(), keepPitch: keepPitch() });
});
let persistTimer: number | undefined;
createEffect(() => {
clearTimeout(persistTimer);
const next = { speed: speed(), keepPitch: keepPitch() };
persistTimer = window.setTimeout(() => {
void Promise.resolve(setConfig(next)).catch(console.error);
}, 150);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/index.tsx` around lines 128 - 130, The effect currently
calls setConfig on every speed() change which can queue many async writes during
slider drags; debounce updates and handle the returned Promise: replace the
immediate createEffect -> setConfig call with a debounced updater that waits
e.g. 100–250ms after the last speed()/keepPitch() change before calling
setConfig({ speed: speed(), keepPitch: keepPitch() }), await the Promise and
catch/log any rejection, and ensure rapid intermediate ticks cancel the pending
debounce so only the final value is persisted (refer to createEffect, speed,
keepPitch, and setConfig to locate the code).

Comment on lines +11 to +12
## Compatibility Note
This plugin operates strictly via the HTML5 `<video>` element properties (`playbackRate` and `preservesPitch`). It explicitly avoids the Web Audio API to guarantee 100% compatibility with other audio-hijacking plugins (like native Equalizers or Crossfade). No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Soften the 100% compatibility claim.

The implementation still polls and mutates shared video state, so this reads stronger than the code can guarantee. I'd describe it as "avoids Web Audio API conflicts" rather than promising full compatibility with every plugin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/readme.md` around lines 11 - 12, Replace the absolute
claim about "100% compatibility" in the README sentence that references HTML5
<video> element properties (`playbackRate` and `preservesPitch`) with a softer
statement: state that the plugin "avoids using the Web Audio API and is designed
to minimize conflicts with other audio-hijacking plugins" or similar, and remove
or reword the phrase promising guaranteed compatibility so it no longer asserts
complete compatibility with every plugin.

Comment on lines +140 to +206
.sr-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: linear-gradient(
to right,
#7c3aed var(--fill, 30%),
#3b1a6e var(--fill, 30%)
);
outline: none;
cursor: pointer;
transition: background 0.1s;
}

.sr-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #c084fc, #7c3aed);
border: 2px solid #a855f7;
box-shadow: 0 0 6px rgba(168, 85, 247, 0.7);
cursor: pointer;
transition: box-shadow 0.15s, transform 0.1s;
}

.sr-slider:hover::-webkit-slider-thumb {
box-shadow: 0 0 12px rgba(168, 85, 247, 0.9);
transform: scale(1.15);
}

.sr-slider:active::-webkit-slider-thumb {
transform: scale(0.95);
}

.sr-pitch-row {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-top: 4px;
}

.sr-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
cursor: pointer;
}

.sr-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}

.sr-thumb {
position: absolute;
inset: 0;
border-radius: 20px;
background: #2d1b69;
border: 1px solid #5b21b6;
transition: background 0.2s, border-color 0.2s;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore visible focus styles for the slider and switch.

.sr-slider removes the browser outline, and the hidden checkbox never gets a replacement focus ring. That makes keyboard navigation hard to track on the two main controls.

Suggested CSS fix
 .sr-slider {
   -webkit-appearance: none;
   appearance: none;
   width: 100%;
   height: 4px;
   border-radius: 2px;
@@
-  outline: none;
   cursor: pointer;
   transition: background 0.1s;
 }
+
+.sr-slider:focus-visible {
+  outline: 2px solid `#c084fc`;
+  outline-offset: 4px;
+}
@@
 .sr-switch input {
   opacity: 0;
   width: 0;
   height: 0;
   position: absolute;
 }
+
+.sr-switch input:focus-visible + .sr-thumb {
+  box-shadow:
+    0 0 0 2px `#1a0a2e`,
+    0 0 0 4px `#c084fc`;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/slowed/style.css` around lines 140 - 206, The slider removes the
native outline and the hidden checkbox for the switch has no visible
replacement, breaking keyboard focus visibility; restore accessible focus
indicators by adding focus styles (use :focus-visible) for .sr-slider to show an
outline or glow (e.g., box-shadow or outline) and add focus-visible/on-focus
rules that target the switch’s visual thumb (e.g., .sr-switch
input:focus-visible + .sr-thumb or .sr-switch:focus-within .sr-thumb) to draw a
clear focus ring/border when the invisible input receives keyboard focus; ensure
the focus styles are subtle, high-contrast, and animate consistently with
existing transitions.

@ArjixWasTaken ArjixWasTaken marked this pull request as draft April 12, 2026 15:22
@The-Kryz
Copy link
Copy Markdown
Author

Thank you to everyone who rejected my code, I will fix it.
But at the moment I am facing depression problems and I am unable to program. As soon as I get better I will apply the corrections.

@ArjixWasTaken
Copy link
Copy Markdown
Member

Thank you to everyone who rejected my code, I will fix it.
But at the moment I am facing depression problems and I am unable to program. As soon as I get better I will apply the corrections.

Don't worry man, I am also suffering from depression, and finding the motivation to work on stuff is hard, nobody here judges you.

Heck, I have some PRs that have been left unattended for a year or two.

Hope you get well!

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants