feat(plugins): add Slowed playback plugin#4400
feat(plugins): add Slowed playback plugin#4400The-Kryz wants to merge 8 commits intopear-devs:masterfrom
Conversation
Maybe we should introduce a unified way for all plugins to use the audio context, w/o having conflicts. |
|
PS: If you need help with the solidjs stuff, I can help. |
|
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. |
|
And I fixed some bugs BTW xD |
ArjixWasTaken
left a comment
There was a problem hiding this comment.
Some more stuff I could point out using my phone
|
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! |
|
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! |
ArjixWasTaken
left a comment
There was a problem hiding this comment.
Other than this, everything looks good to me.
|
All done!! thks again @ArjixWasTaken! |
|
Great, I'll check this out on my computer once I have the time, to see if there are any bugs left. |
|
Just a note: this is an electron app, aka chromium, we can't use Firefox/safari specific stuff. |
|
Fixed! |
Is it really problem in your local environment? I get same error when building. And even Actions in this repo get it xd |
|
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 While the plugin isn't in the main, I'm using it like this to listen to my music xD |
|
Plugin works fine and I don't see any obvious bugs. @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. |
|
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. |
| // @ts-ignore | ||
| video.webkitPreservesPitch = true; |
| // @ts-ignore - Prefixos para garantir funcionamento no Electron/Chromium | ||
| video.webkitPreservesPitch = keepPitch(); |
There was a problem hiding this comment.
This statement is unnecessary.
| const [keepPitch, setKeepPitch] = createSignal(safeConfig.keepPitch ?? false); | ||
| const [collapsed, setCollapsed] = createSignal(false); | ||
|
|
||
| const getVideo = () => document.querySelector<HTMLVideoElement>('video'); |
There was a problem hiding this comment.
do not use document.querySelector<HTMLVideoElement>('video') in start
| cleanup: null as (() => void) | null, | ||
|
|
||
| start({ config, setConfig }) { | ||
| const safeConfig = config || DEFAULT_CONFIG; |
|
|
||
| const getVideo = () => document.querySelector<HTMLVideoElement>('video'); | ||
|
|
||
| document.getElementById('sr-panel')?.remove(); |
There was a problem hiding this comment.
What is the purpose of this statement?
There was a problem hiding this comment.
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> |
| // @ts-ignore | ||
| video.webkitPreservesPitch = p; |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughA 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
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/plugins/slowed/index.tsxsrc/plugins/slowed/readme.mdsrc/plugins/slowed/style.css
| 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); | ||
|
|
There was a problem hiding this comment.
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.
| 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;"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/plugins/slowed/index.tsx | head -100Repository: pear-devs/pear-desktop
Length of output: 4262
🏁 Script executed:
cat -n src/plugins/slowed/index.tsx | tail -20Repository: 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.
| <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)} /> |
There was a problem hiding this comment.
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.
| if (video.preservesPitch !== keepPitch()) { | ||
| video.preservesPitch = keepPitch(); | ||
| // @ts-ignore - Prefixos para garantir funcionamento no Electron/Chromium | ||
| video.webkitPreservesPitch = keepPitch(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure and read the relevant file
wc -l src/plugins/slowed/index.tsxRepository: 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 -30Repository: 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 typescriptRepository: 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 -1Repository: 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 -40Repository: 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.jsonRepository: 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 -5Repository: 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/nullRepository: pear-devs/pear-desktop
Length of output: 85
🏁 Script executed:
# Check the ESLint flat config file
cat -n eslint.config.mjsRepository: 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.
| const doCleanup = () => { | ||
| clearInterval(interval); | ||
| dispose(); | ||
| panel.remove(); | ||
| const video = getVideo(); | ||
| if (video) { | ||
| video.playbackRate = 1.0; | ||
| video.preservesPitch = true; | ||
| // @ts-ignore | ||
| video.webkitPreservesPitch = true; | ||
| } |
There was a problem hiding this comment.
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.
| createEffect(() => { | ||
| setConfig({ speed: speed(), keepPitch: keepPitch() }); | ||
| }); |
There was a problem hiding this comment.
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.
| 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).
| ## 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 |
There was a problem hiding this comment.
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.
| .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; |
There was a problem hiding this comment.
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.
|
Thank you to everyone who rejected my code, I will fix it. |
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! |
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 bycreateMediaElementSourceonce, doing so would causeInvalidStateErrorconflicts with existing plugins like the native Equalizer or Crossfade.By relying solely on
HTMLMediaElement.playbackRateandpreservesPitch, this plugin is 100% compatible with the rest of the ecosystem and won't break other audio plugins.Summary by CodeRabbit
New Features
Documentation