Skip to content

Ported Lyrics, WatchVideo, Selector, ActionsMenu, CollectionSelector to uhtml#297

Merged
n-ce merged 4 commits into
mainfrom
uhtml
May 17, 2025
Merged

Ported Lyrics, WatchVideo, Selector, ActionsMenu, CollectionSelector to uhtml#297
n-ce merged 4 commits into
mainfrom
uhtml

Conversation

@n-ce
Copy link
Copy Markdown
Owner

@n-ce n-ce commented May 16, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a new Selector component for customizable dropdown selections.
    • Added a rich, dialog-based Actions Menu for media item interactions.
    • Enhanced lyrics display with synchronized scrolling and real-time highlighting in a modal dialog.
    • Added a video watching dialog with synchronized audio/video playback, codec-based stream selection, and quality controls.
  • Bug Fixes

    • Improved error handling and fallback mechanisms for lyrics and video streaming dialogs.
  • Refactor

    • Replaced Solid.js-based lyrics, video, and actions menu components with modular dialog-based implementations using dynamic imports.
    • Streamlined dialog rendering for update prompts and media dialogs.
    • Migrated collection management UI to use state arrays instead of direct DOM manipulation.
    • Updated modal handling to dynamically create dialogs instead of relying on static elements.
    • Removed static actions menu dialog from HTML and replaced with dynamic dialog creation and rendering.
    • Adjusted event handling and navigation history management for dialogs to improve UX consistency.
  • Chores

    • Updated a development dependency for Node.js type definitions.
    • Updated a runtime dependency for Solid.js.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2025

Walkthrough

The changes refactor the modal component system by replacing Solid.js-based implementations with asynchronous functions that render content into HTMLDialogElements using the uhtml library or direct DOM manipulation. Lyrics and video watching dialogs are now dynamically imported and initialized with dialogs, and a new Selector component is introduced using uhtml. The update prompt and player logic are adapted accordingly.

Changes

File(s) Change Summary
package.json Updated @types/node devDependency version from ^22.15.17 to ^22.15.18.
src/components/ActionsMenu.tsx, src/lib/player.ts, src/modules/start.ts, src/main.ts Refactored modal dialog logic: replaced Solid.js lazy/render usage with creation of <dialog> elements and dynamic module imports, invoking new async functions with dialogs as arguments. Removed Solid.js imports.
src/components/Lyrics.ts, src/components/WatchVideo.ts, src/components/Selector.ts Added new modules: Lyrics and WatchVideo as async functions that render into dialogs, and a new Selector component using uhtml.
src/components/Lyrics.tsx, src/components/WatchVideo.tsx Deleted Solid.js-based Lyrics and WatchVideo components.
src/components/UpdatePrompt.ts Changed UpdatePrompt to an async function that renders directly into a dialog element using uhtml's render. Adjusted signature and imports.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ActionsMenu
    participant Dialog
    participant Lyrics/WatchVideo Module
    participant Store/API

    User->>ActionsMenu: Click "Show Lyrics" or "Watch Video"
    ActionsMenu->>Dialog: Create <dialog> element and append to body
    ActionsMenu->>Lyrics/WatchVideo Module: Dynamically import module
    Lyrics/WatchVideo Module->>Dialog: Initialize and render content
    Lyrics/WatchVideo Module->>Store/API: Fetch data (lyrics/video streams)
    Store/API-->>Lyrics/WatchVideo Module: Return data
    Lyrics/WatchVideo Module->>Dialog: Update dialog content
    User->>Dialog: Interact (close, select options, etc.)
    Dialog->>Lyrics/WatchVideo Module: Trigger cleanup on close
Loading

Poem

A dialog pops, a lyric flows,
Videos play where the rabbit goes.
Solid.js hops away, uhtml hops in—
With selectors and prompts, let the fun begin!
Refactored and neat, with code so spry,
The bunny’s new modals are ready to fly! 🐇✨

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 36d3766 and 985cc36.

📒 Files selected for processing (1)
  • package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@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: 6

🧹 Nitpick comments (5)
src/components/Selector.ts (2)

20-20: Missing TranslationKeys type import

The code uses TranslationKeys type but it's not explicitly imported. This type appears to be globally available, but explicitly importing it would improve code clarity and type safety.

import { Hole, html } from "uhtml";
import { i18n } from "../lib/utils";
+import type { TranslationKeys } from "../types"; // Add explicit import

23-29: Optimize onmount callback check

The _.onmount callback is checked twice - once before creating the Promise and again inside the Promise's then handler. This creates unnecessary conditional logic.

      <select
        ref=${(s: HTMLSelectElement) => {
-      if (_.onmount)
        Promise.resolve().then(() => {
          if (s.isConnected && _.onmount)
            _.onmount(s)
        });
    }}
src/components/ActionsMenu.tsx (1)

129-135: Prefer showModal() over setting open = true

dialog.open = true produces a non-modal dialog.
Because WatchVideo renders interactive media, accidental clicks on the page underneath are undesirable. Calling dialog.showModal() yields proper modality and a backdrop without extra CSS.

-const dialog = document.createElement('dialog') as HTMLDialogElement;
-dialog.open = true;
+const dialog = document.createElement('dialog') as HTMLDialogElement;
+dialog.showModal();
src/components/UpdatePrompt.ts (1)

20-22: Open external links safely

open(commitsLink); opens the changelog in the current browsing context, which can unexpectedly replace the app.
Use window.open(url, '_blank', 'noopener') to create a new tab and prevent window.opener-based attacks.

-open(commitsLink);
+window.open(commitsLink, '_blank', 'noopener');
src/components/Lyrics.ts (1)

36-55: Guard against missed DOM look-ups

dialog.firstElementChild!.children assumes rendering succeeded and the DOM is still intact.
If the template fails to render (e.g. uhtml throws) firstElementChild will be null and the exclamation mark suppresses TypeScript’s helpful warning, leading to a runtime crash on the first lrcSync call.

Instead:

-const p = dialog.firstElementChild!.children;
+const section = dialog.firstElementChild;
+if (!section) return;
+const p = section.children;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9cbe8ef and f35ab5f.

📒 Files selected for processing (11)
  • package.json (1 hunks)
  • src/components/ActionsMenu.tsx (3 hunks)
  • src/components/Lyrics.ts (1 hunks)
  • src/components/Lyrics.tsx (0 hunks)
  • src/components/Selector.ts (1 hunks)
  • src/components/UpdatePrompt.ts (1 hunks)
  • src/components/WatchVideo.ts (1 hunks)
  • src/components/WatchVideo.tsx (0 hunks)
  • src/lib/player.ts (1 hunks)
  • src/main.ts (1 hunks)
  • src/modules/start.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • src/components/Lyrics.tsx
  • src/components/WatchVideo.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/components/UpdatePrompt.ts (1)
src/lib/utils.ts (1)
  • i18n (40-45)
src/components/ActionsMenu.tsx (1)
src/lib/store.ts (1)
  • store (5-97)
src/components/Lyrics.ts (3)
src/lib/dom.ts (1)
  • loadingScreen (41-41)
src/lib/store.ts (1)
  • store (5-97)
src/lib/utils.ts (1)
  • notify (69-77)
🔇 Additional comments (7)
package.json (1)

18-18: Upgrade @types/node patch version to ^22.15.18
This is a safe patch-level bump for your Node.js type definitions. Please ensure your TypeScript build and any Netlify edge‐function types still compile cleanly with the updated defs.

src/components/Selector.ts (3)

1-3: Appropriate imports for uhtml-based component

The imports look good, including both the necessary Hole and html from uhtml, and the i18n utility for internationalization.


4-15: Well-typed props interface

The component function has a well-structured props interface with appropriate types:

  • id for element identification
  • label for user-facing text
  • handler with precise event typing
  • children using uhtml's Hole type
  • Optional onmount callback for post-connection operations

17-34: Well-structured uhtml template implementation

The template structure follows best practices:

  • Label properly connected to select via for attribute
  • Internationalization applied to label text
  • Event handler properly bound with @change
  • Children properly rendered inside the select element
  • Asynchronous onmount pattern ensures DOM connection before callback execution
src/main.ts (1)

44-45: Refactor to dialog-based component approach

This change aligns with the overall refactoring strategy - replacing direct uhtml rendering with passing a dialog element to the component function. The implementation correctly maintains the dialog event handling while simplifying the rendering approach.

src/modules/start.ts (1)

63-69: Refactored to use dialog-based WatchVideo component

The implementation properly:

  1. Creates a dialog element with TypeScript casting
  2. Sets the dialog's open state and class name
  3. Appends it to document body
  4. Dynamically imports the WatchVideo component
  5. Passes the dialog to the component function

This approach is consistent with the overall refactoring from Solid.js rendering to dialog-based components using uhtml.

src/lib/player.ts (1)

14-20: Consistent implementation of dialog-based WatchVideo component

This implementation mirrors the approach used in start.ts, creating and configuring a dialog element before passing it to the WatchVideo component. The change aligns with the project's move from Solid.js rendering to dialog-based components using uhtml.

Comment thread src/components/ActionsMenu.tsx Outdated
Comment on lines +106 to +115
const dialog = document.createElement('dialog') as HTMLDialogElement;
dialog.className = 'displayer';
dialog.addEventListener('click', () => {
dialog.close();
dialog.remove();
store.lrcSync = () => '';
});
document.body.appendChild(dialog);
import('./Lyrics.ts')
.then(async mod => await mod.default(dialog))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add defensive error handling when the dynamic import fails

If the Lyrics.ts module fails to load (network hiccup, 404 after future refactor, etc.) the dialog node you just appended will sit in the DOM forever and the user receives no feedback. Wrap the import in a try/catch and close / remove the dialog in the error branch, optionally notify()-ing the user.

-import('./Lyrics.ts')
-  .then(async mod => await mod.default(dialog))
+try {
+  const mod = await import('./Lyrics.ts');
+  await mod.default(dialog);
+} catch (err) {
+  console.error('Failed to load Lyrics module', err);
+  dialog.close();
+  dialog.remove();
+  notify('Unable to load lyrics. Please try again later.');
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/ActionsMenu.tsx around lines 106 to 115, the dynamic import of
'Lyrics.ts' lacks error handling, causing the dialog to remain in the DOM
indefinitely if the import fails. Wrap the import statement in a try/catch
block; in the catch block, close and remove the dialog element and optionally
notify the user of the failure to load the module.

Comment thread src/components/Lyrics.ts
Comment on lines +10 to +16
const data = await fetch(
`https://lrclib.net/api/get?track_name=${store.actionsMenu.title}&artist_name=${store.actionsMenu.author.slice(0, -8)}`,
{
headers: {
'Lrclib-Client': `ytify ${Build} (https://github.com/n-ce/ytify)`
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Encode user-generated strings in the request URL

Track titles or artist names containing spaces, &, ?, etc. will break the LRCLib request.
Wrap both parameters with encodeURIComponent.

-`https://lrclib.net/api/get?track_name=${store.actionsMenu.title}&artist_name=${store.actionsMenu.author.slice(0, -8)}`
+`https://lrclib.net/api/get?track_name=${encodeURIComponent(store.actionsMenu.title)}&artist_name=${encodeURIComponent(store.actionsMenu.author.slice(0, -8))}`

While touching this code, double-check that the global Build variable is always defined in production builds; otherwise a reference error will occur in development mode.

📝 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
const data = await fetch(
`https://lrclib.net/api/get?track_name=${store.actionsMenu.title}&artist_name=${store.actionsMenu.author.slice(0, -8)}`,
{
headers: {
'Lrclib-Client': `ytify ${Build} (https://github.com/n-ce/ytify)`
}
})
const data = await fetch(
`https://lrclib.net/api/get?track_name=${encodeURIComponent(store.actionsMenu.title)}&artist_name=${encodeURIComponent(store.actionsMenu.author.slice(0, -8))}`,
{
headers: {
'Lrclib-Client': `ytify ${Build} (https://github.com/n-ce/ytify)`
}
}
)
🤖 Prompt for AI Agents
In src/components/Lyrics.ts around lines 10 to 16, the track_name and
artist_name parameters in the fetch URL are not URL-encoded, which can cause the
request to break if they contain special characters. Fix this by wrapping
store.actionsMenu.title and the sliced store.actionsMenu.author with
encodeURIComponent before inserting them into the URL string. Also, verify that
the global Build variable is defined in all environments to prevent reference
errors.

Comment on lines +192 to +194
audio.src = proxyHandler(audioArray[0].url);
audio.currentTime = video.currentTime;
loadingScreen.close();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid out-of-bounds access when audioArray is empty

audioArray[0] will throw if no compatible audio stream is returned.
Add a guard and surface the error to the user:

-if (audioArray.length) {
-  audio.src = proxyHandler(audioArray[0].url);
-  audio.currentTime = video.currentTime;
-} else {
-  loadingScreen.close();
-  notify('No playable audio stream found.');
-  return;
-}
+if (!audioArray.length) {
+  loadingScreen.close();
+  notify('No playable audio stream found.');
+  return;
+}
+
+audio.src = proxyHandler(audioArray[0].url);
+audio.currentTime = video.currentTime;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/WatchVideo.ts around lines 192 to 194, the code accesses
audioArray[0] without checking if audioArray is empty, which can cause an
out-of-bounds error. Add a guard condition to check if audioArray has at least
one element before accessing audioArray[0]. If it is empty, handle the error
gracefully by showing an appropriate message to the user instead of proceeding
with the assignment.

Comment on lines +152 to +179
<div>

<button @click=${close}>Close</button>

${media.video.length ?
Selector({
id: 'videoCodecSelector',
label: '',
handler: (_) => {
video.src = proxyHandler(_.target.value);
video.currentTime = audio.currentTime;
if (savedQ)
save('watchMode', _.target.selectedOptions[0].textContent as string);
},
children: html`
<option>Video</option>
${media.video.map(f => html`
<option
value=${f[1]}
selected=${f[0] === savedQ}
>${f[0]}</option>
`)
}`,
onmount: (_) => {
if (savedQ)
video.src = proxyHandler(_.value);
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Set an initial video.src to guarantee playback

When no previous quality preference is saved, the selector’s onmount hook might not execute and video.src remains empty, so the player stays black while audio plays.
Assign the first available stream as a safe default:

@@
-        onmount: (_) => {
-          if (savedQ)
-            video.src = proxyHandler(_.value);
-        }
+        onmount: (_) => {
+          const chosen = savedQ ? _.value : media.video[0]?.[1];
+          if (chosen) video.src = proxyHandler(chosen);
+        }
📝 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
<div>
<button @click=${close}>Close</button>
${media.video.length ?
Selector({
id: 'videoCodecSelector',
label: '',
handler: (_) => {
video.src = proxyHandler(_.target.value);
video.currentTime = audio.currentTime;
if (savedQ)
save('watchMode', _.target.selectedOptions[0].textContent as string);
},
children: html`
<option>Video</option>
${media.video.map(f => html`
<option
value=${f[1]}
selected=${f[0] === savedQ}
>${f[0]}</option>
`)
}`,
onmount: (_) => {
if (savedQ)
video.src = proxyHandler(_.value);
}
})
<div>
<button @click=${close}>Close</button>
${media.video.length ?
Selector({
id: 'videoCodecSelector',
label: '',
handler: (_) => {
video.src = proxyHandler(_.target.value);
video.currentTime = audio.currentTime;
if (savedQ)
save('watchMode', _.target.selectedOptions[0].textContent as string);
},
children: html`
<option>Video</option>
${media.video.map(f => html`
<option
value=${f[1]}
selected=${f[0] === savedQ}
>${f[0]}</option>
`)
}`,
onmount: (_) => {
const chosen = savedQ ? _.value : media.video[0]?.[1];
if (chosen) video.src = proxyHandler(chosen);
}
})
🤖 Prompt for AI Agents
In src/components/WatchVideo.ts between lines 152 and 179, the video element's
src is not set initially if there is no saved quality preference, causing the
player to remain black while audio plays. To fix this, assign the first
available video stream URL to video.src as a default before rendering the
selector, ensuring the video source is always set for playback even when no
saved preference exists.

Comment on lines +52 to +60
.filter(f => {
const av1 = hasAv1 && supportsAv1 && f.type.includes('av01');
if (av1) return true;
const vp9 = !hasAv1 && f.type.includes('vp9');
if (vp9) return true;
const avc = !hasVp9 && f.type.includes('avc1');
if (avc) return true;
})
.map(f => ([f.resolution, f.url]));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Codec-selection filter can drop all streams when AV1 is available but unsupported

If the API returns AV1 streams and the browser doesn’t support AV1, hasAv1 is truthy, supportsAv1 is false – the first clause rejects the item, but vp9 and avc branches are also skipped because of the !hasAv1 / !hasVp9 checks.
Result: media.video becomes empty ⇒ no video rendered, silent failure.

Refactor the filter to test each codec independently and short-circuit only when a match is found:

-media.video = data.videoStreams
-  .filter(f => {
-    const av1 = hasAv1 && supportsAv1 && f.type.includes('av01');
-    if (av1) return true;
-    const vp9 = !hasAv1 && f.type.includes('vp9');
-    if (vp9) return true;
-    const avc = !hasVp9 && f.type.includes('avc1');
-    if (avc) return true;
-  })
+media.video = data.videoStreams.filter(f => {
+  if (supportsAv1 && f.type.includes('av01')) return true;      // prefer AV1
+  if (f.type.includes('vp9')) return true;                       // else VP9
+  return f.type.includes('avc1');                                // fallback AVC
+})
📝 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
.filter(f => {
const av1 = hasAv1 && supportsAv1 && f.type.includes('av01');
if (av1) return true;
const vp9 = !hasAv1 && f.type.includes('vp9');
if (vp9) return true;
const avc = !hasVp9 && f.type.includes('avc1');
if (avc) return true;
})
.map(f => ([f.resolution, f.url]));
media.video = data.videoStreams.filter(f => {
if (supportsAv1 && f.type.includes('av01')) return true; // prefer AV1
if (f.type.includes('vp9')) return true; // else VP9
return f.type.includes('avc1'); // fallback AVC
})
.map(f => ([f.resolution, f.url]));
🤖 Prompt for AI Agents
In src/components/WatchVideo.ts around lines 52 to 60, the filter logic
incorrectly excludes all streams when AV1 streams exist but are unsupported by
the browser, due to conditions that skip VP9 and AVC checks if AV1 is present.
Refactor the filter to independently check each codec without mutually exclusive
conditions, returning true as soon as a matching supported codec is found,
ensuring that unsupported AV1 streams do not cause all streams to be dropped.

Comment thread src/components/WatchVideo.ts
@n-ce n-ce changed the title Ported Lyrics, WatchVideo, Selector to uhtml Ported Lyrics, WatchVideo, Selector, ActionsMenu, CollectionSelector to uhtml May 17, 2025
Copy link
Copy Markdown
Contributor

@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: 4

🧹 Nitpick comments (3)
src/scripts/router.ts (1)

125-127: Consider using optional chaining for null safety

The dynamic dialog element retrieval is a good improvement that aligns with the modal component refactoring. However, you should use optional chaining to safely check if the element exists before accessing its properties.

- const actionsMenu = document.getElementById('actionsMenu') as HTMLDialogElement | null;
- if (actionsMenu && actionsMenu.open) {
-   actionsMenu.click();
+ const actionsMenu = document.getElementById('actionsMenu') as HTMLDialogElement | null;
+ if (actionsMenu?.open) {
+   actionsMenu.click();
🧰 Tools
🪛 Biome (1.9.4)

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/lib/utils.ts (1)

69-77: Prevent multiple snackbars piling up

Changing the clear predicate to el.isConnected no longer removes any previous snackbar. Two notify() calls within 8 s now produce stacked toasts. Consider explicitly removing an existing snackbar before appending a new one:

 const el = $('p');
+// Ensure only one snackbar is visible at a time
+document.querySelector('.snackbar')?.remove();
 const clear = () => el.isConnected && el.remove();

This preserves the new robustness while restoring the former single-toast behaviour.

src/components/CollectionSelector.ts (1)

24-43: Tighten typing & improve accessibility of the <select>

  1. The custom cast on e.target works but bypasses the DOM typings. A safer, self-documenting alternative:
const select = e.target as HTMLSelectElement;
const { value } = select;
...
select.selectedIndex = 0;
  1. The control has no accessible label. Adding aria-label="${i18n('collection_selector_add_to')}" (or linking a <label for="collectionSelector">) will help screen-reader users.

  2. Hard-coding tabindex="2" risks conflicting when some list items are conditionally removed. Consider relying on natural tab order or calculating the index dynamically.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between f35ab5f and 80ca39f.

📒 Files selected for processing (19)
  • index.html (0 hunks)
  • src/components/ActionsMenu.ts (1 hunks)
  • src/components/ActionsMenu.tsx (0 hunks)
  • src/components/CollectionSelector.ts (1 hunks)
  • src/components/CollectionSelector.tsx (0 hunks)
  • src/components/Lyrics.ts (1 hunks)
  • src/components/Selector.ts (1 hunks)
  • src/components/UpdatePrompt.ts (1 hunks)
  • src/components/WatchVideo.ts (1 hunks)
  • src/lib/dom.ts (0 hunks)
  • src/lib/libraryUtils.ts (1 hunks)
  • src/lib/player.ts (1 hunks)
  • src/lib/store.ts (2 hunks)
  • src/lib/utils.ts (3 hunks)
  • src/main.ts (2 hunks)
  • src/modules/setMetadata.ts (2 hunks)
  • src/modules/start.ts (1 hunks)
  • src/scripts/list.ts (3 hunks)
  • src/scripts/router.ts (2 hunks)
💤 Files with no reviewable changes (4)
  • index.html
  • src/lib/dom.ts
  • src/components/CollectionSelector.tsx
  • src/components/ActionsMenu.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main.ts
  • src/modules/start.ts
  • src/components/UpdatePrompt.ts
  • src/lib/player.ts
  • src/components/WatchVideo.ts
  • src/components/Lyrics.ts
  • src/components/Selector.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/lib/libraryUtils.ts (2)
src/lib/store.ts (1)
  • store (5-100)
src/lib/utils.ts (1)
  • notify (69-77)
src/components/CollectionSelector.ts (3)
src/lib/libraryUtils.ts (4)
  • getDB (7-7)
  • reservedCollections (5-5)
  • createCollection (74-83)
  • addToCollection (46-57)
src/lib/store.ts (1)
  • store (5-100)
src/lib/utils.ts (1)
  • i18n (40-45)
src/scripts/list.ts (1)
src/lib/store.ts (1)
  • store (5-100)
🪛 Biome (1.9.4)
src/scripts/router.ts

[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (5)
src/lib/store.ts (1)

36-36: Good refactoring to centralize collection options state!

Moving the collection options from DOM elements to a centralized array in the store is a good architectural improvement. This change properly separates state management from DOM manipulation, making the code more maintainable and easier to test.

Also applies to: 98-98

src/scripts/router.ts (1)

1-1: LGTM: Improved import statement

Removing the direct actionsMenu import aligns with the modal refactoring to use dynamic DOM queries instead of static imports.

src/scripts/list.ts (1)

34-34: LGTM: Good refactoring from DOM to state management

The use of the store array for managing collection options is consistent with the refactoring goals. Using indexOf to find the collection and updating the array at the found index for renaming are good practices.

Also applies to: 79-79

src/lib/libraryUtils.ts (1)

78-79: LGTM: Well-implemented state management refactoring

The changes to use store.addToCollectionOptions instead of DOM queries for both checking existence and adding new collections are well-implemented. This properly centralizes the collection options state and moves away from direct DOM manipulation, making the code more maintainable.

Also applies to: 82-82

src/components/ActionsMenu.ts (1)

120-137: Verify dialog opening strategy for the “Watch on …” flow

You pre-set dialog.open = true but rely on the lazily-loaded WatchVideo module to finalise rendering. If that module internally calls showModal() an InvalidStateError will be thrown because the dialog is already open.

Please double-check WatchVideo.default(); if it calls showModal(), remove the open = true line and let the module decide. Otherwise, prefer dialog.showModal() here for consistency with the other dialog invocations.

Comment thread src/scripts/list.ts Outdated
Comment on lines 51 to 57
more = function() {
store.actionsMenu = data;
actionsMenu.showModal();
history.pushState({}, '', '#');
const dialog = document.createElement('dialog') as HTMLDialogElement;
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make sure the temporary <dialog> node is always cleaned-up (Esc key, close() call, etc.)

ActionsMenu removes the node via its own close() helper, but if the user dismisses the dialog with the browser default “Esc” shortcut the element stays in the DOM, leaking one detached node per open.
Add a cancel/close listener immediately after creating the element:

 const dialog = document.createElement('dialog') as HTMLDialogElement;
+// Remove the element even when it is dismissed with <Esc> or via `dialog.close()`.
+dialog.addEventListener('cancel', () => dialog.remove());
+dialog.addEventListener('close',  () => dialog.remove());
 document.body.appendChild(dialog);

This keeps the DOM tidy and avoids an ever-growing pile of hidden dialogs.

📝 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
more = function() {
store.actionsMenu = data;
actionsMenu.showModal();
history.pushState({}, '', '#');
const dialog = document.createElement('dialog') as HTMLDialogElement;
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));
}
more = function() {
store.actionsMenu = data;
const dialog = document.createElement('dialog') as HTMLDialogElement;
// Remove the element even when it is dismissed with <Esc> or via `dialog.close()`.
dialog.addEventListener('cancel', () => dialog.remove());
dialog.addEventListener('close', () => dialog.remove());
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));
}
🤖 Prompt for AI Agents
In src/modules/setMetadata.ts around lines 51 to 57, the temporary <dialog>
element created is not always removed from the DOM if the user dismisses it with
the Esc key, causing a memory leak. To fix this, add an event listener for the
'cancel' or 'close' event on the dialog immediately after creating it, and in
the handler, remove the dialog element from the DOM to ensure it is always
cleaned up regardless of how it is closed.

Comment thread src/lib/utils.ts
Comment on lines +210 to +214
const dialog = $('dialog') as HTMLDialogElement;
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Attach a cancel/close handler to the ad-hoc dialog as well

superClick mirrors the logic in setMetadata, so the same leak can occur here. Right after creating the dialog:

 const dialog = $('dialog') as HTMLDialogElement;
+dialog.addEventListener('cancel', () => dialog.remove());
+dialog.addEventListener('close',  () => dialog.remove());
 document.body.appendChild(dialog);
📝 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
const dialog = $('dialog') as HTMLDialogElement;
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));
const dialog = $('dialog') as HTMLDialogElement;
dialog.addEventListener('cancel', () => dialog.remove());
dialog.addEventListener('close', () => dialog.remove());
document.body.appendChild(dialog);
import('../components/ActionsMenu.ts')
.then(mod => mod.default(dialog));
🤖 Prompt for AI Agents
In src/lib/utils.ts around lines 210 to 214, the dialog created and appended to
the body lacks a cancel or close event handler, which can cause a memory leak
similar to the one in setMetadata. To fix this, add an event listener for the
dialog's cancel or close event immediately after appending it, and ensure the
handler properly cleans up or removes the dialog to prevent leaks.

Comment on lines +15 to +25
function close() {
dialog.close();
history.back();
dialog.remove();
}

dialog.id = 'actionsMenu';
dialog.onclick = close;
dialog.showModal();
history.pushState({}, '', '#');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle cancel / close events to avoid orphaned dialogs & stale history

Pressing Esc closes the dialog implicitly but never calls close(), leaving the node in the DOM and the extra history.pushState() entry intact. Tie both lifecycle events together:

 dialog.id = 'actionsMenu';
 dialog.onclick = close;
+dialog.addEventListener('cancel', () => close());
+dialog.addEventListener('close',  () => dialog.remove()); // fallback safety
 dialog.showModal();
 history.pushState({}, '', '#');
📝 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
function close() {
dialog.close();
history.back();
dialog.remove();
}
dialog.id = 'actionsMenu';
dialog.onclick = close;
dialog.showModal();
history.pushState({}, '', '#');
function close() {
dialog.close();
history.back();
dialog.remove();
}
dialog.id = 'actionsMenu';
dialog.onclick = close;
dialog.addEventListener('cancel', () => close());
dialog.addEventListener('close', () => dialog.remove()); // fallback safety
dialog.showModal();
history.pushState({}, '', '#');
🤖 Prompt for AI Agents
In src/components/ActionsMenu.ts around lines 15 to 25, the dialog's implicit
close via Esc key does not trigger the close() function, causing the dialog
element to remain in the DOM and the history state to be stale. Fix this by
adding event listeners for the dialog's 'cancel' and 'close' events that call
the close() function, ensuring proper cleanup of the dialog element and history
state in all close scenarios.

n-ce and others added 2 commits May 17, 2025 10:32
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@n-ce n-ce merged commit d5f6e4f into main May 17, 2025
2 checks passed
@n-ce n-ce deleted the uhtml branch May 17, 2025 05:55
@coderabbitai coderabbitai Bot mentioned this pull request Feb 20, 2026
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.

1 participant