Skip to content

feat(#461): lyrics and song details side by side in fullscreen mode#496

Open
Owie6789 wants to merge 7 commits into
Sandakan:release/4.0.0-alpha.5from
Owie6789:fix/461-lyrics-songdetails-sidebyside
Open

feat(#461): lyrics and song details side by side in fullscreen mode#496
Owie6789 wants to merge 7 commits into
Sandakan:release/4.0.0-alpha.5from
Owie6789:fix/461-lyrics-songdetails-sidebyside

Conversation

@Owie6789

@Owie6789 Owie6789 commented May 27, 2026

Copy link
Copy Markdown

fixes #461

adds a showLyricsWithSongInfo preference that displays lyrics and song details side by side in fullscreen mode. disabled by default for backward compatibility.

  • when enabled: flex-row layout with left 50% SongInfoContainer and right 50% LyricsContainer
  • SongInfoContainer stays visible (no opacity-0/invisible hiding)
  • LyricsContainer uses relative positioning (not absolute)
  • lyrics toggle button hidden in side-by-side mode (lyrics always visible)
  • setting in Lyrics Settings page
  • preference persists via localStorage
  • i18n keys in en.json

Refs #461

Summary by CodeRabbit

  • New Features
    • Added "Show Lyrics with Song Info" setting in preferences. When enabled, the full-screen player displays lyrics and song information in a side-by-side two-column layout instead of the default stacked view, allowing simultaneous viewing of both elements while playing music.

@Owie6789 Owie6789 force-pushed the fix/461-lyrics-songdetails-sidebyside branch 2 times, most recently from f1258d8 to b8d731a Compare May 27, 2026 11:05
@Owie6789 Owie6789 changed the title fix(#461): lyrics and song details side by side in fullscreen mode feat(#461): lyrics and song details side by side in fullscreen mode May 27, 2026
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Author

@CodeRabbit — comprehensive review request. Please do all of the following and report findings as inline review comments on the changed files (not in this thread), grouped by severity.

1. Careful code review

  • Walk every changed file end-to-end. For each function/hook/component, describe what it does, what it depends on, and what can break it.
  • Verify the diff actually implements the PR title and the linked issue. Flag scope creep and unrelated drive-by edits.
  • Check that all new state, events, IPC channels, persistence keys, and settings keys are wired through the full stack (UI → context/store → IPC → main process → disk).
  • Confirm the linked issue's acceptance criteria are met. If a criterion is not met, say so explicitly.

2. UI audit (only if the PR touches UI)

  • Render correctness: layout, alignment, overflow, clipping, hit targets, focus rings, dark/light theme, HiDPI.
  • Accessibility: keyboard nav, ARIA roles/labels, contrast, motion-reduce, screen reader announcements for state changes.
  • Interaction states: default / hover / focus / active / disabled / loading / error / empty. Any state missing is a bug.
  • Performance: unnecessary re-renders, missing memoization on heavy lists, large images not resized, layout thrash, debounce/throttle on scroll/resize.
  • Empty/zero/error states: what does the user see when there is no data, network is down, permission is denied, or the file is corrupt.
  • Cross-platform: Windows, macOS, Linux. Electron-specific quirks (frameless regions, title bar drag, safe-area on multi-monitor with different DPI).

3. Production-readiness verification

  • No console.log / debugger / commented-out code left in.
  • No hardcoded paths, secrets, dev URLs, or fake TODO placeholders.
  • Error handling: every async path has a real catch that surfaces something to the user or to telemetry. No swallowed rejections.
  • Migrations / schema changes: if a persisted shape changed, is there a migration that does not lose user data?
  • Backwards compatibility with existing user data and settings. If something is intentionally breaking, is it called out in the PR description?
  • Memory leaks: listeners / observers / timers / intervals / AbortControllers are cleaned up on unmount.
  • Race conditions: concurrent clicks, double-submits, stale closures, optimistic UI rollback.
  • IPC contract: channel names match the renderer/main handshake, payloads are validated, and the main process guards against malformed messages.

4. Code smell sniff

  • Duplicated logic that should be a shared util / hook / component.
  • God components / functions over ~150 lines that should be split.
  • Boolean traps, magic numbers, stringly-typed APIs, leaky abstractions.
  • useEffect that should be a derived value, event handler, or ref.
  • Inline styles or className soup where a token or class would do.
  • Inconsistent naming or file/folder structure vs the rest of the repo.
  • Overly defensive code, dead code, as any / // @ts-ignore without a written justification.

5. Half-baked implementation detection

  • Feature appears to work in the happy path screenshot but lacks:
    • error path, empty path, permission-denied path, network-down path
    • loading / skeleton state
    • persistence (settings do not survive restart)
    • undo / cancel / back navigation
    • a11y (keyboard + screen reader)
    • i18n / RTL consideration if the rest of the app is localized
    • tests (unit + integration) for the new logic
  • Looks like a "first commit" version that ships a flag and stops — flag any feature gated behind an env var or if (false) branch.
  • "I'll add tests later" code with no test files added in this PR.

6. Required output from you

  • Inline review comments on the diff, file by file.
  • A short summary at the end listing: ✅ what is solid, ⚠️ what must change before merge, 💡 nice-to-haves.
  • For each ⚠️ item: severity (blocker / major / minor), the file:line, and the smallest possible fix direction.
  • If you find a blocker in the UI, also call it out in the summary so it is impossible to miss.

Please be thorough. I will be applying fixes from your review in a follow-up pass.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details and changed files
gh pr view 496 --repo Sandakan/Nora --json title,body,files,additions,deletions,changedFiles

echo "=== FILES CHANGED ==="
gh pr diff 496 --repo Sandakan/Nora

Length of output: 17191


🏁 Script executed:

#!/bin/bash
# Get head SHA and check existing patterns in LyricsSettings for how other settings propagate to store
gh pr view 496 --repo Sandakan/Nora --json headRefOid,headRefName

echo "=== Check how other settings in LyricsSettings.tsx propagate to the store ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d

echo "=== Check appReducer for UPDATE_PREFERENCES action or similar ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/other/appReducer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | grep -n 'showLyricsWithSongInfo\|UPDATE_PREFERENCES\|localStorage\|preferences\|setPreferences' | head -60

echo "=== Check storage.preferences.setPreferences to understand if it triggers store updates ==="
gh api repos/Sandakan/Nora/git/trees/refs/pull/496/head?recursive=1 --jq '.tree[].path' | grep -i 'storage\|preferences' | grep -v node_modules | head -30

Length of output: 11970


🏁 Script executed:

#!/bin/bash
# Get the PR head commit SHA for review comments
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, number: .number}'

Length of output: 247


🏁 Script executed:

#!/bin/bash
echo "=== Check localStorage utility setPreferences ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/utils/localStorage.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | grep -n 'setPreferences\|dispatch\|store\|UPDATE_LOCAL' | head -40

echo "=== Full setPreferences impl ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/utils/localStorage.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | grep -n -A 20 'setPreferences'

echo "=== Check if storage.setPreferences dispatches to store ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/utils/localStorage.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | head -80

Length of output: 6213


🏁 Script executed:

#!/bin/bash
# Get accurate line numbers from the PR head for each changed file
echo "=== FullScreenPlayer.tsx line numbers ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | head -120

echo "=== LyricsContainer.tsx line numbers ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | grep -n '' | sed -n '140,200p'

echo "=== LyricsSettings.tsx line numbers (new section) ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | sed -n '65,100p'

echo "=== ipc.ts relevant lines ==="
gh api repos/Sandakan/Nora/contents/src/main/ipc.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | sed -n '84,95p'

Length of output: 12332


🏁 Script executed:

#!/bin/bash
# Post comprehensive inline review via GitHub API
gh api repos/Sandakan/Nora/pulls/496/reviews \
  --method POST \
  --field commit_id="b8d731abc27760ac68057a7f45863935f9ff7112" \
  --field event="COMMENT" \
  --field body="## Review Summary

### ✅ What is solid
- **Type safety**: `showLyricsWithSongInfo: boolean` correctly added to `app.d.ts` and `LOCAL_STORAGE_DEFAULT_TEMPLATE` — new users and fresh installs get a safe default.
- **Store persistence**: `storage.preferences.setPreferences` dispatches \`UPDATE_LOCAL_STORAGE_PREFERENCES\` to the Zustand store immediately (confirmed in \`localStorage.ts:221\`), so the setting propagates live without a restart.
- **i18n coverage**: Both description and label keys added to \`en.json\` — consistent with the rest of the settings page.
- **Dead code removal**: \`isArtistBackgroundsEnabled = false\` and the dead conditional around \`isArtistBackgroundsEnabled\` removed — good cleanup.
- **Backwards compatibility**: Default is \`false\`, so existing users see no change.
- **Lyrics toggle hidden in side-by-side mode**: Correctly suppressed when the setting is on.

---

### ⚠️ Must change before merge

| # | Severity | File : Line | Issue |
|---|----------|-------------|-------|
| 1 | **Blocker** | \`FullScreenPlayer.tsx:99\` | \`SeekBarSlider\` is a sibling of the side-by-side wrapper inside a \`flex-row\` container — it gets pushed off-screen (see inline) |
| 2 | **Blocker** | \`LyricsContainer.tsx:177\` | \`!lyrics\` catches \`null\` (initial/loading state), not just \`undefined\` (not found) — causes a false \"No lyrics\" flash on every song load (see inline) |
| 3 | **Major** | \`FullScreenPlayer.tsx:21\` | \`useState(isShowLyricsWithSongInfo)\` captures the preference only at mount — live preference changes while the player is open are ignored (see inline) |
| 4 | **Major** | \`ipc.ts:88-89\` | \`toggleAutoLaunch\` and \`toggleMiniPlayerAlwaysOnTop\` imports are unrelated scope creep — should be a separate PR (see inline) |
| 5 | **Minor** | \`FullScreenPlayer.tsx:55\` | Both branches of the blur ternary are identical: \`blur-[2rem]!\` — dead conditional (see inline) |
| 6 | **Minor** | \`LyricsContainer.tsx:153\` | No \`overflow-auto\` on \`LyricsContainer\` itself in side-by-side mode — only the parent has it; \`h-full\` child prevents scroll propagation (see inline) |

---

### 💡 Nice-to-haves
- **Tests**: No unit/integration tests added for the new layout branch or the new preference key.
- **RTL layout**: \`flex-row\` is not reversed for RTL locales — if the app ever targets RTL languages, the side-by-side order will be wrong. Consider \`flex-row\` + \`rtl:flex-row-reverse\` or using \`dir\` attribute.
- **Prop drilling**: \`isShowLyricsWithSongInfo\` is now threaded into both \`LyricsContainer\` and \`SongInfoContainer\`. These components already read from the store internally (via \`useStore\`), so reading the preference directly inside each child would eliminate the prop entirely.
- **Variable shadowing** in \`LyricsSettings.tsx:74\`: \`const showLyricsWithSongInfo = ...\` inside the \`useEffect\` shadows the outer state variable. This is the same pattern as \`autoTranslateLyrics\` (pre-existing), but worth naming differently (e.g. \`storedValue\`) to avoid confusion.
- **JSX duplication**: \`SongInfoContainer\` and \`LyricsContainer\` are fully duplicated across the two render branches. A single render with conditional class/prop overrides would be cleaner.
- **\`imgPath\` memo** (\`FullScreenPlayer.tsx:39-41\`): \`useMemo\` with a single expression that just returns a property is not meaningful — a plain variable would suffice." \
  --field "comments[][path]=src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx" \
  --field "comments[][line]=99" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🔴 Blocker — SeekBarSlider rendered outside side-by-side layout, breaks in flex-row mode**

In side-by-side mode the outer container is \`flex flex-row justify-end\`. The \`SeekBarSlider\` is a sibling of the \`<div className=\"flex h-full w-full flex-row overflow-hidden\">\` wrapper, which itself declares \`w-full\`. That leaves zero horizontal space for the seek bar — it either overflows off-screen to the right or is squeezed to zero width.

The normal-mode layout works because the outer container is \`flex-col\`, which stacks children vertically. In \`flex-row\`, the slider needs to live *below* the two-column block, not beside it.

**Minimal fix** — wrap both the side-by-side columns and the seek bar in a shared \`flex-col\` so the slider always sits at the bottom:

\`\`\`tsx
{isShowLyricsWithSongInfo ? (
  <div className=\"flex h-full w-full flex-col\">   {/* ← flex-col wrapper */}
    <div className=\"flex min-h-0 flex-1 flex-row overflow-hidden\">
      <div className=\"flex w-1/2 items-end\">
        <SongInfoContainer ... />
      </div>
      <div className=\"w-1/2 overflow-auto\">
        <LyricsContainer ... />
      </div>
    </div>
    <SeekBarSlider ... />   {/* ← moves here, inside the wrapper */}
  </div>
) : (
  <>
    <LyricsContainer ... />
    <SongInfoContainer ... />
    <SeekBarSlider ... />
  </>
)}
\`\`\`

Also remove or change the outer \`flex-row\` class on line 61 — it is now redundant and misleading." \
  --field "comments[][path]=src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx" \
  --field "comments[][line]=177" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🔴 Blocker — \`!lyrics\` incorrectly matches the loading/null state, causing a premature \"No lyrics\" flash**

The original code tested \`lyrics === undefined\` to mean \"lyrics were looked up and definitively not found\". \`null\` is the initial state (fetch not yet complete). By changing to \`!lyrics\`, you now show the \"No lyrics available\" error panel while the fetch is still in flight (when \`lyrics\` is \`null\`), then hide it once lyrics arrive — creating a visible flash for every song that has lyrics.

**Fix** — restore the three-way distinction:

\`\`\`tsx
{/* lyrics === null  → still loading, show nothing */}
{/* lyrics === undefined → fetch done, no lyrics found */}
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
  <div className=\"text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50\">
    <span className=\"material-icons-round-outlined mb-2 text-5xl\">brightness_alert</span>
    <p>{t('lyricsPage.noLyrics')}</p>
    <p className=\"mt-4 text-base\">{t('lyricsPage.noLyricsDescription')}</p>
  </div>
)}
\`\`\`" \
  --field "comments[][path]=src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx" \
  --field "comments[][line]=21" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🟠 Major — stale \`isLyricsVisible\` initial value when preference changes while component is mounted**

\`useState(isShowLyricsWithSongInfo)\` captures the preference value *once at mount*. If the user opens Settings in another window and toggles \"Show lyrics with song details\" while \`FullScreenPlayer\` is already rendered, \`isLyricsVisible\` will not update — the player will still behave as if the old setting is active until remount.

Add a sync effect:

\`\`\`tsx
useEffect(() => {
  if (isShowLyricsWithSongInfo) {
    setIsLyricsVisible(true);
  }
}, [isShowLyricsWithSongInfo]);
\`\`\`

This also makes the intent explicit: side-by-side mode always forces lyrics visible." \
  --field "comments[][path]=src/main/ipc.ts" \
  --field "comments[][line]=89" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🟠 Major — unrelated scope creep: \`toggleAutoLaunch\` and \`toggleMiniPlayerAlwaysOnTop\` imports**

These two imports have nothing to do with the lyrics/song-details side-by-side feature (PR `#496` / issue `#461`). They appear to be drive-by fixes for previously missing imports in \`ipc.ts\`.

- If they were causing a build or runtime error, they deserve their own focused commit/PR with an explanation.
- If they are unused here, they should be removed — unused imports in \`ipc.ts\` can mask dead IPC handlers.

Please move them to a separate PR or add a comment explaining why they are included here." \
  --field "comments[][path]=src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx" \
  --field "comments[][line]=55" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🟡 Minor — both branches of the blur ternary are identical (dead conditional)**

\`\`\`tsx
className={\`... \${isLyricsVisible ? 'blur-[2rem]!' : 'blur-[2rem]!'}\`}
\`\`\`

Both the truthy and falsy branches produce the exact same class string \`blur-[2rem]!\`. The conditional has no effect. Either this was meant to vary the blur intensity between lyrics-visible and lyrics-hidden states (in which case the second branch is wrong) or it should simply be a static class:

\`\`\`tsx
className=\"... blur-[2rem]!\"
\`\`\`" \
  --field "comments[][path]=src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx" \
  --field "comments[][line]=153" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]=**🟡 Minor — lyrics content not scrollable in side-by-side mode**

In side-by-side mode the \`LyricsContainer\` div has \`h-full\` but no \`overflow-auto\`. The parent wrapper in \`FullScreenPlayer.tsx\` (\`<div className=\"w-1/2 overflow-auto\">\`) technically has \`overflow-auto\`, but because the child declares \`h-full\` it will try to fill the parent's height and block scroll propagation — long lyrics will be clipped with no scrollbar.

**Fix** — add \`overflow-auto\` to the side-by-side class string and remove it from the parent (or keep both and let the child own its scroll):

\`\`\`tsx
isShowLyricsWithSongInfo
  ? 'relative flex h-full w-full flex-col items-start overflow-auto p-4'
  : 'absolute top-0 ...'
\`\`\`

Also, \`py-4 pr-4 pl-4\` can be written as \`p-4\` (identical result, cleaner)."

Length of output: 10735



@Owie6789 — The GitHub integration doesn't have write access to post inline diff comments on this repo, so I'm delivering the full review here with exact file:line references. All findings are verified against the actual PR head (b8d731a).


Review Summary

✅ What is solid

  • Type safety: showLyricsWithSongInfo: boolean correctly added to app.d.ts and LOCAL_STORAGE_DEFAULT_TEMPLATE — safe default for new installs.
  • Store persistence: storage.preferences.setPreferences dispatches UPDATE_LOCAL_STORAGE_PREFERENCES to the Zustand store immediately (verified in localStorage.ts:221) — the setting propagates live without a restart. ✔
  • i18n coverage: Both showLyricsWithSongInfoDescription and showLyricsWithSongInfo keys added to en.json — consistent with the rest of the settings page.
  • Dead code removal: isArtistBackgroundsEnabled = false constant and its dead conditional removed — good housekeeping.
  • Backwards compatibility: Default is false, so existing users see no change.
  • Lyrics toggle hidden in side-by-side mode: Correctly suppressed via {!isShowLyricsWithSongInfo && <Button ... />}.

⚠️ Must change before merge

🔴 Blocker 1 — SeekBarSlider pushed off-screen in side-by-side mode

FullScreenPlayer.tsx Line 61 + Line 99

In side-by-side mode, the outer wrapper is flex flex-row justify-end (Line 61). The SeekBarSlider (Line 99) is a direct sibling of the inner <div className="flex h-full w-full flex-row overflow-hidden"> wrapper. Since that wrapper is w-full, there is zero horizontal space left for the seek bar — it will overflow or be squashed to nothing.

The normal layout works because flex-col stacks children vertically. In flex-row the slider needs to live below the two-column block. Minimal fix:

{isShowLyricsWithSongInfo ? (
  // ← outer flex-col so SeekBarSlider always sits below the columns
  <div className="flex h-full w-full flex-col">
    <div className="flex min-h-0 flex-1 flex-row overflow-hidden">
      <div className="flex w-1/2 items-end">
        <SongInfoContainer ... isShowLyricsWithSongInfo={isShowLyricsWithSongInfo} />
      </div>
      <div className="w-1/2 overflow-auto">
        <LyricsContainer ... isShowLyricsWithSongInfo={isShowLyricsWithSongInfo} />
      </div>
    </div>
    <SeekBarSlider ... />  {/* ← moved inside the wrapper */}
  </div>
) : (
  <>
    <LyricsContainer ... />
    <SongInfoContainer ... />
    <SeekBarSlider ... />
  </>
)}

Also simplify Line 61's outer div — the flex-row/flex-col toggle is now redundant since the inner wrapper handles the axis:

className={`flex max-w-full flex-col justify-end ${isMouseActive && 'group/fullScreenPlayer'}`}

🔴 Blocker 2 — !lyrics catches the loading state (null), causing a false "No lyrics" flash

LyricsContainer.tsx Line 177

The original code used lyrics === undefined (fetch completed, nothing found). null is the initial state before the fetch resolves. The changed condition !lyrics is truthy for both null and undefined, so on every song that has lyrics, users will see the "No lyrics available" error panel while the fetch is still in flight, then it disappears once lyrics arrive.

Fix — restore the three-way distinction:

// null  → fetch not yet complete → show nothing
// undefined → fetch complete, no lyrics found → show error
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
  <div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
    <span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
    <p>{t('lyricsPage.noLyrics')}</p>
    <p className="mt-4 text-base">{t('lyricsPage.noLyricsDescription')}</p>
  </div>
)}

🟠 Major — Stale isLyricsVisible initial value when preference changes while player is mounted

FullScreenPlayer.tsx Line 21

const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo);

useState only uses its argument as the initial value at mount. If the user navigates to Settings and toggles "Show lyrics with song details" while FullScreenPlayer is already open, isLyricsVisible stays stale — the layout won't switch until the component remounts. Add a sync effect:

useEffect(() => {
  if (isShowLyricsWithSongInfo) setIsLyricsVisible(true);
}, [isShowLyricsWithSongInfo]);

This also makes the intent explicit: enabling side-by-side always forces lyrics visible.


🟠 Major — Unrelated scope creep: toggleAutoLaunch + toggleMiniPlayerAlwaysOnTop imports

src/main/ipc.ts Lines 88–89

These two imports have nothing to do with the lyrics/song-details side-by-side feature. They appear to be drive-by fixes for previously missing imports in ipc.ts.

  • If they were causing a build/runtime error, they deserve their own focused commit or PR with an explanation.
  • Unrelated changes in the same PR make review, bisect, and revert harder.

Please either move them to a separate PR or add an inline comment explaining why they're included here.


🟡 Minor — Dead ternary: both branches produce identical blur-[2rem]!

FullScreenPlayer.tsx Line 55

className={`... ${isLyricsVisible ? 'blur-[2rem]!' : 'blur-[2rem]!'}`}

Both branches are the same string — the conditional has no effect. Replace with a static class or fix to produce two distinct values if the intent was to differentiate:

className="... blur-[2rem]!"

🟡 Minor — Lyrics not scrollable in side-by-side mode

LyricsContainer.tsx Line 153

In side-by-side mode the LyricsContainer div has h-full but no overflow-auto. The parent in FullScreenPlayer.tsx has overflow-auto, but because the child fills the parent's full height with h-full, scroll is blocked. Long lyrics will be clipped with no scrollbar.

Fix — move overflow-auto onto the container itself and remove it from the parent:

isShowLyricsWithSongInfo
  ? 'relative flex h-full w-full flex-col items-start overflow-auto p-4'
  : 'absolute top-0 ...'

Side note: py-4 pr-4 pl-4 is equivalent to p-4 — cleaner to write.


💡 Nice-to-haves

  • Tests: No unit/integration tests for the new layout branch or preference key.
  • RTL layout: flex-row is not reversed for RTL locales. If the app ever targets RTL languages, consider rtl:flex-row-reverse or a dir-aware wrapper.
  • Prop drilling: isShowLyricsWithSongInfo is threaded into both LyricsContainer and SongInfoContainer, but both already call useStore internally. Reading the preference directly inside each child would eliminate the prop entirely.
  • Variable shadowing (LyricsSettings.tsx Line 74): const showLyricsWithSongInfo = storage.preferences.getPreferences(...) inside the effect shadows the outer state variable. Pre-existing pattern, but rename the inner variable (e.g. storedValue) for clarity.
  • JSX duplication: SongInfoContainer and LyricsContainer are fully duplicated across both render branches. A single render with conditional class/prop differences would be cleaner and easier to maintain.
  • imgPath memo (FullScreenPlayer.tsx Lines 39–41): useMemo wrapping a single property access adds overhead with no benefit. A plain const imgPath = currentSongData.artworkPath; suffices.

@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2504a5b-609e-440a-a26e-174488a38279

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a new preference to display lyrics alongside song information in the full-screen player. Users can now toggle a setting to view both lyrics and song details in a side-by-side layout, instead of having lyrics hide the song information. The feature includes type definitions, storage defaults, localized UI strings, a settings control, and layout refactoring to support the new view mode.

Changes

Show Lyrics with Song Info

Layer / File(s) Summary
Preference type and storage defaults
src/types/app.d.ts, src/renderer/src/other/appReducer.tsx
Preferences interface gains showLyricsWithSongInfo: boolean, and the default storage template initializes this preference to false.
Settings control and localization
src/renderer/src/assets/locales/en/en.json, src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx
Two new locale strings added for the preference label and description. Settings UI includes a checkbox that reads/writes the preference from persisted storage on mount and toggle.
Full-screen player layout refactoring
src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx
FullScreenPlayer reads the preference and conditionally renders a two-column flex layout (song info + scrollable lyrics side-by-side) when enabled, or the original stacked layout when disabled. Artwork path memoization is simplified.
Lyrics and song info container integration
src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx, src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx
Both containers accept the optional isShowLyricsWithSongInfo prop and adjust visibility/styling accordingly. Lyrics render when either isShowLyricsWithSongInfo or isLyricsVisible is true. The lyrics toggle button is hidden when the preference is active.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • Sandakan

Poem

🐰 A player split in two,
where lyrics dance with song details true,
no hiding now—both shine together,
through melodies, preferences woven light as feather.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The imports added to src/main/ipc.ts (toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop) are unrelated to the side-by-side lyrics feature and constitute out-of-scope scope creep flagged in the automated review. Remove the toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop imports from ipc.ts and address them in a separate PR; they fall outside the #461 scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding side-by-side lyrics and song details in fullscreen mode, which aligns with the primary implementation across all modified files.
Linked Issues check ✅ Passed The PR implements the second objective from issue #461: displaying lyrics and song information simultaneously in fullscreen mode via a new showLyricsWithSongInfo preference with UI controls.
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
✨ Simplify code
  • Create PR with simplified code

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx (1)

28-76: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Extract lyrics fetching into centralized query module.

This component violates coding guidelines by implementing inline data fetching in useEffect instead of using the project's query pattern with useSuspenseQuery() and centralized query modules.

Issues:

  • Line 63: Errors are swallowed with console.error instead of returning safe defaults
  • Lines 28-76: Complex async fetch logic lives in component instead of query module
  • No TanStack Query caching, deduplication, or retry logic

As per coding guidelines: "Extract all data fetching logic into centralized query modules using createQueryKeys factory pattern from @lukemorales/query-key-factory in src/renderer/src/queries/" and "Use useSuspenseQuery() for data fetching in components."

Based on learnings: All data fetching errors should be handled within queryFn, returning safe defaults (empty arrays, null, default objects) instead of throwing exceptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx`
around lines 28 - 76, Extract the inline fetching in LyricsContainer: create a
centralized query module (using createQueryKeys and a queryFn named e.g.
lyricsQuery/getSongLyrics) that calls window.api.lyrics.* (getSongLyrics,
getTranslatedLyrics, convertLyricsToPinyin/romanizeLyrics/convertLyricsToRomaja)
and implements the full async flow and error handling, returning safe defaults
(null/empty objects/flags) instead of throwing; then replace the useEffect in
LyricsContainer with useSuspenseQuery(lyricsQuery.getSongLyrics(key, params))
and apply the returned data to setLyrics/setIsLyricsAvailable, removing
console.error; ensure the query handles translation/romanization and respects
preferences.autoTranslateLyrics and preferences.autoConvertLyrics so the
component only consumes cached, deduplicated results.
🧹 Nitpick comments (1)
src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx (1)

71-77: 💤 Low value

Consider consolidating repeated preference initialization pattern.

The file now has four similar useState + useEffect pairs (lines 49-53, 57-61, 65-69, 73-77) that each read a single preference and initialize local state. This works but creates duplication. Consider extracting a custom hook:

function usePreference<K extends keyof Preferences>(key: K) {
  const [value, setValue] = useState<Preferences[K]>(
    storage.preferences.getPreferences(key)
  );
  return [value, setValue] as const;
}

Then simplify to:

const [showLyricsWithSongInfo, setShowLyricsWithSongInfo] = 
  usePreference('showLyricsWithSongInfo');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx` around
lines 71 - 77, Extract the repeated preference-init pattern into a reusable
custom hook (e.g. implement usePreference<K extends keyof Preferences>(key: K)
that calls storage.preferences.getPreferences(key) to set initial state and
returns [value, setValue] as const), then replace each duplicated pair (for
example the useState/useEffect for showLyricsWithSongInfo and the other three
preference states) with calls like const [showLyricsWithSongInfo,
setShowLyricsWithSongInfo] = usePreference('showLyricsWithSongInfo'); ensuring
the hook is typed with Preferences and imported/defined in this file so existing
setters (setShowLyricsWithSongInfo, etc.) keep working.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx`:
- Around line 164-177: The rendering currently uses `!lyrics` which conflates
`null` (loading) and `undefined` (not found); update the three conditional
blocks in LyricsContainer to distinguish `lyrics === null` (show a loading state
or nothing/spinner), `lyrics === undefined` (show the "no lyrics" message), and
`lyrics` truthy (existing logic that shows `lyricsComponents` and
`lyricsSource`), i.e. replace checks that use `!lyrics` with explicit `=== null`
or `=== undefined` checks and add a `lyrics === null` branch (you'll touch the
JSX around `lyricsComponents`, `lyricsSource`, the "noSyncedLyrics" block, and
the final "no lyrics" block); keep `isShowLyricsWithSongInfo` and
`isLyricsVisible` gating as-is and ensure `setLyrics(null)` remains the loading
sentinel.

In `@src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx`:
- Around line 19-21: isLyricsVisible is only initialized from
isShowLyricsWithSongInfo and won't update when preferences change; add a
useEffect in FullScreenPlayer that watches isShowLyricsWithSongInfo and calls
setIsLyricsVisible(isShowLyricsWithSongInfo) whenever it changes so the local
state stays in sync with the preference (keep the existing useState declaration
for isLyricsVisible/setIsLyricsVisible).

---

Outside diff comments:
In `@src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx`:
- Around line 28-76: Extract the inline fetching in LyricsContainer: create a
centralized query module (using createQueryKeys and a queryFn named e.g.
lyricsQuery/getSongLyrics) that calls window.api.lyrics.* (getSongLyrics,
getTranslatedLyrics, convertLyricsToPinyin/romanizeLyrics/convertLyricsToRomaja)
and implements the full async flow and error handling, returning safe defaults
(null/empty objects/flags) instead of throwing; then replace the useEffect in
LyricsContainer with useSuspenseQuery(lyricsQuery.getSongLyrics(key, params))
and apply the returned data to setLyrics/setIsLyricsAvailable, removing
console.error; ensure the query handles translation/romanization and respects
preferences.autoTranslateLyrics and preferences.autoConvertLyrics so the
component only consumes cached, deduplicated results.

---

Nitpick comments:
In `@src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx`:
- Around line 71-77: Extract the repeated preference-init pattern into a
reusable custom hook (e.g. implement usePreference<K extends keyof
Preferences>(key: K) that calls storage.preferences.getPreferences(key) to set
initial state and returns [value, setValue] as const), then replace each
duplicated pair (for example the useState/useEffect for showLyricsWithSongInfo
and the other three preference states) with calls like const
[showLyricsWithSongInfo, setShowLyricsWithSongInfo] =
usePreference('showLyricsWithSongInfo'); ensuring the hook is typed with
Preferences and imported/defined in this file so existing setters
(setShowLyricsWithSongInfo, etc.) keep working.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70f7fa56-c614-4390-8f91-8c82d5360236

📥 Commits

Reviewing files that changed from the base of the PR and between bf955ab and b8d731a.

📒 Files selected for processing (8)
  • src/main/ipc.ts
  • src/renderer/src/assets/locales/en/en.json
  • src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx
  • src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/types/app.d.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use module-level initialization for singleton services (AudioPlayer, PlayerQueue) accessed by intervals, timers, or event handlers, NOT useRef-based singletons, to avoid stale closure issues
Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks
Use descriptive, context-rich variable and function names; avoid generic names like 'data', 'user', 'info', 'temp'. Functions should start with verbs (calculateDuration, fetchSongData, validatePlaylist)
Use guard clauses with early returns to keep function logic flat and readable, avoiding deep nesting and improving error handling clarity
Use eslint-plugin-simple-import-sort for automatic import organization: external dependencies, internal path aliases (@renderer, @main, @common), then relative imports
All data fetching errors should be handled within queryFn, returning safe defaults (empty arrays, null, default objects) instead of throwing exceptions
Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility

Files:

  • src/types/app.d.ts
  • src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx
  • src/main/ipc.ts
  • src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/renderer/src/**/*.{ts,tsx}: Use dispatch() function from store.ts for all state updates in the renderer, never modify store state directly
Extract all data fetching logic into centralized query modules using createQueryKeys factory pattern from @lukemorales/query-key-factory in src/renderer/src/queries/, never inline fetch logic in components
Use useSuspenseQuery() for data fetching in components with TanStack Router loaders for pre-fetching, not custom fetch hooks or useQuery without suspense
Use TanStack Router's , useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions

Files:

  • src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx
  • src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx
src/renderer/src/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All custom hooks that integrate features into App.tsx must be called in App.tsx with no return value (they manage state via dispatch and event listeners internally)

Files:

  • src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx
  • src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx
  • src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx
src/main/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All business logic should be in src/main/core/ modules with single responsibility, following the pattern of taking data as parameters and calling database queries or external APIs

Files:

  • src/main/ipc.ts
src/main/ipc.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules

Files:

  • src/main/ipc.ts
🧠 Learnings (4)
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/preload/index.ts : All IPC communication to main process must be exposed through window.api with categorized namespaces (playerControls, audioLibraryControls, settingsHelpers, etc.) for type safety

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/main/ipc.ts : Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to **/*.{ts,tsx} : Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks

Applied to files:

  • src/main/ipc.ts
📚 Learning: 2026-06-04T16:49:13.844Z
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/main/core/**/*.ts : Every core function must call dataUpdateEvent() after modifying database state to notify the renderer of changes via IPC

Applied to files:

  • src/main/ipc.ts
🔇 Additional comments (11)
src/types/app.d.ts (1)

516-516: LGTM!

src/renderer/src/other/appReducer.tsx (1)

400-401: LGTM!

src/renderer/src/assets/locales/en/en.json (1)

422-423: LGTM!

src/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsx (1)

180-191: LGTM!

src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx (2)

61-98: LGTM!


39-41: ⚡ Quick win

Artwork fallback for artist online images remains intact; fullscreen background cover uses currentSongData.artworkPath only.
In src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx, the fullscreen background imgPath now maps directly to currentSongData.artworkPath and relies on <Img fallbackSrc={DefaultSongCover}>. The artist “online artwork” override logic is used elsewhere in fullscreen—src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx renders artist avatars with src={artist.onlineArtworkPaths?.picture_small} and fallbackSrc={artist.artworkPath}—so that fallback path is not impacted by this imgPath memo change. However, I can’t confirm from the current evidence whether currentSongData.artworkPath itself already falls back to artist artwork when song artwork is missing (that computation happens outside this component).

src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx (1)

151-161: 💤 Low value

Verify overflow handling in side-by-side mode.

When isShowLyricsWithSongInfo is true, this container uses relative positioning without overflow-auto (line 153), while the parent in FullScreenPlayer.tsx line 76 applies overflow-auto to the wrapping div. When false, this container itself has overflow-auto (line 154).

Verify that long lyrics scroll correctly in both modes. The current split may cause confusion or require the scroll container to be on the child element instead.

src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsx (3)

19-19: LGTM!

Also applies to: 39-39


80-83: LGTM!


140-151: LGTM!

src/main/ipc.ts (1)

88-89: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope creep: unrelated imports should be in a separate PR.

These imports fix a real bug (missing imports for existing IPC handlers on lines 555–561), but they are completely unrelated to the PR's stated objective of adding side-by-side lyrics and song info in fullscreen mode. Mixing unrelated bug fixes with feature work reduces PR clarity and atomic changesets.

The previous automated review flagged this as a major concern and requested these changes be moved to a separate PR or justified. That concern remains valid.

Recommendation: Move these imports (and the corresponding commit) to a dedicated bug-fix PR for proper tracking and review isolation.

⛔ Skipped due to learnings
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/preload/index.ts : All IPC communication to main process must be exposed through window.api with categorized namespaces (playerControls, audioLibraryControls, settingsHelpers, etc.) for type safety
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to src/main/ipc.ts : Register all IPC handlers using ipcMain.handle() for async operations with return values and ipcMain.on() for fire-and-forget events, mapping them to core business logic modules
Learnt from: CR
Repo: Sandakan/Nora PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-06-04T16:49:13.844Z
Learning: Applies to **/*.{ts,tsx} : Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks

Comment on lines +164 to +177
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyricsComponents.length > 0 && lyrics && lyrics.lyrics.isSynced && (
<>
{lyricsComponents}
{lyricsSource}
</>
)}
{isLyricsVisible && lyrics && !lyrics.lyrics.isSynced && (
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics && !lyrics.lyrics.isSynced && (
<div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
<span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
{t('lyricsPage.noSyncedLyrics')}
<p className="mt-4 text-base">{t('lyricsPage.noSyncedLyricsDescription')}</p>
</div>
)}
{isLyricsVisible && lyrics === undefined && (
{(isShowLyricsWithSongInfo || isLyricsVisible) && !lyrics && (

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 | ⚡ Quick win

Fix "no lyrics" flash during loading by distinguishing null from undefined.

The visibility checks at lines 164, 170, and 177 use !lyrics, which treats both null (loading) and undefined (not found) as falsy. Since setLyrics(null) is called at line 30 when fetching starts, the "No lyrics" message (line 177-183) flashes briefly during loading.

Restore the three-way distinction:

  • null → loading (show nothing or a loading spinner)
  • undefined → not found (show "no lyrics" message)
  • SongLyrics → found (show lyrics)
🐛 Proposed fix to eliminate flash
- {(isShowLyricsWithSongInfo || isLyricsVisible) && lyricsComponents.length > 0 && lyrics && lyrics.lyrics.isSynced && (
+ {(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics !== null && lyricsComponents.length > 0 && lyrics && lyrics.lyrics.isSynced && (
    <>
      {lyricsComponents}
      {lyricsSource}
    </>
  )}
- {(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics && !lyrics.lyrics.isSynced && (
+ {(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics !== null && lyrics && !lyrics.lyrics.isSynced && (
    <div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
      <span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
      {t('lyricsPage.noSyncedLyrics')}
      <p className="mt-4 text-base">{t('lyricsPage.noSyncedLyricsDescription')}</p>
    </div>
  )}
- {(isShowLyricsWithSongInfo || isLyricsVisible) && !lyrics && (
+ {(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
    <div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
      <span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
      <p>{t('lyricsPage.noLyrics')}</p>
      <p className="mt-4 text-base">{t('lyricsPage.noLyricsDescription')}</p>
    </div>
  )}

Optionally, add a loading state for lyrics === null:

{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === null && (
  <div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
    <span className="material-icons-round-outlined mb-2 text-5xl animate-spin">refresh</span>
    <p>{t('lyricsPage.lyricsLoading')}</p>
  </div>
)}
📝 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
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyricsComponents.length > 0 && lyrics && lyrics.lyrics.isSynced && (
<>
{lyricsComponents}
{lyricsSource}
</>
)}
{isLyricsVisible && lyrics && !lyrics.lyrics.isSynced && (
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics && !lyrics.lyrics.isSynced && (
<div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
<span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
{t('lyricsPage.noSyncedLyrics')}
<p className="mt-4 text-base">{t('lyricsPage.noSyncedLyricsDescription')}</p>
</div>
)}
{isLyricsVisible && lyrics === undefined && (
{(isShowLyricsWithSongInfo || isLyricsVisible) && !lyrics && (
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics !== null && lyricsComponents.length > 0 && lyrics && lyrics.lyrics.isSynced && (
<>
{lyricsComponents}
{lyricsSource}
</>
)}
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics !== null && lyrics && !lyrics.lyrics.isSynced && (
<div className="text-font-color-highlight flex h-full w-full flex-col justify-center text-2xl opacity-50">
<span className="material-icons-round-outlined mb-2 text-5xl">brightness_alert</span>
{t('lyricsPage.noSyncedLyrics')}
<p className="mt-4 text-base">{t('lyricsPage.noSyncedLyricsDescription')}</p>
</div>
)}
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx`
around lines 164 - 177, The rendering currently uses `!lyrics` which conflates
`null` (loading) and `undefined` (not found); update the three conditional
blocks in LyricsContainer to distinguish `lyrics === null` (show a loading state
or nothing/spinner), `lyrics === undefined` (show the "no lyrics" message), and
`lyrics` truthy (existing logic that shows `lyricsComponents` and
`lyricsSource`), i.e. replace checks that use `!lyrics` with explicit `=== null`
or `=== undefined` checks and add a `lyrics === null` branch (you'll touch the
JSX around `lyricsComponents`, `lyricsSource`, the "noSyncedLyrics" block, and
the final "no lyrics" block); keep `isShowLyricsWithSongInfo` and
`isLyricsVisible` gating as-is and ensure `setLyrics(null)` remains the loading
sentinel.

Comment on lines +19 to +21
const isShowLyricsWithSongInfo = preferences?.showLyricsWithSongInfo;

const [isLyricsVisible, setIsLyricsVisible] = useState(false);
const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo);

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 | ⚡ Quick win

Synchronize isLyricsVisible state when preference changes.

isLyricsVisible is initialized from isShowLyricsWithSongInfo at mount (line 21), but won't update if the user toggles the preference in settings while this component is mounted. This causes the UI to be out of sync with the user's preference.

🔄 Add useEffect to sync state with preference
  const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo);
  const [isLyricsAvailable, setIsLyricsAvailable] = useState(false);
  const [songPos, setSongPos] = useState(0);
+
+ useEffect(() => {
+   if (isShowLyricsWithSongInfo !== undefined) {
+     setIsLyricsVisible(isShowLyricsWithSongInfo);
+   }
+ }, [isShowLyricsWithSongInfo]);
📝 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 isShowLyricsWithSongInfo = preferences?.showLyricsWithSongInfo;
const [isLyricsVisible, setIsLyricsVisible] = useState(false);
const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo);
const isShowLyricsWithSongInfo = preferences?.showLyricsWithSongInfo;
const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo);
const [isLyricsAvailable, setIsLyricsAvailable] = useState(false);
const [songPos, setSongPos] = useState(0);
useEffect(() => {
if (isShowLyricsWithSongInfo !== undefined) {
setIsLyricsVisible(isShowLyricsWithSongInfo);
}
}, [isShowLyricsWithSongInfo]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx` around
lines 19 - 21, isLyricsVisible is only initialized from isShowLyricsWithSongInfo
and won't update when preferences change; add a useEffect in FullScreenPlayer
that watches isShowLyricsWithSongInfo and calls
setIsLyricsVisible(isShowLyricsWithSongInfo) whenever it changes so the local
state stays in sync with the preference (keep the existing useState declaration
for isLyricsVisible/setIsLyricsVisible).

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 5, 2026
- Add lyricsQuery.fullScreenPlayer to src/renderer/src/queries/lyrics.ts that wraps getSongLyrics + getTranslatedLyrics + convertLyricsToPinyin/romanizeLyrics/convertLyricsToRomaja, respecting autoTranslate/autoConvert preferences and language. Returns safe null default on error instead of throwing.

- Replace LyricsContainer useEffect with useQuery(lyricsQuery.fullScreenPlayer(...), { enabled: isLyricsVisible }), giving us TanStack Query caching/dedup/retry for free.

- Move setIsLyricsAvailable into a small effect that mirrors useQuery data.
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Author

@coderabbitai

Addressed the major finding on src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx.

Pushed 0f88a242 to Owie6789:fix/461-lyrics-songdetails-sidebyside:

  1. New query lyricsQuery.fullScreenPlayer in src/renderer/src/queries/lyrics.ts — wraps the entire getSongLyrics + getTranslatedLyrics + convertLyricsToPinyin/romanizeLyrics/convertLyricsToRomaja flow in one queryFn. It honors preferences.autoTranslateLyrics and preferences.autoConvertLyrics, and the current i18n.language. Caches via TanStack Query and dedupes across renders.
  2. Safe defaults — the queryFn returns null (rather than throwing) on any rejection, so the existing !lyrics no-lyrics UI still renders. Errors are logged once via console.error inside the queryFn instead of being silently swallowed at the call site.
  3. LyricsContainer rewrite — replaced the 49-line useEffect with a useQuery(lyricsQuery.fullScreenPlayer(...), { enabled: isLyricsVisible }) and a tiny useEffect that mirrors lyrics?.lyrics?.isSynced back to setIsLyricsAvailable. Dropped the useState<SongLyrics | null | undefined>(null) (the query owns the data now) and the unused useTranslation import stays only for the no-synced-lyrics copy.

I went with useQuery rather than useSuspenseQuery because the isLyricsVisible toggle wants a clean "not loaded" state when the lyrics panel is hidden, which useSuspenseQuery doesn't express well. The existing lyrics/index.tsx page follows the same useQuery({ ...lyricsQuery.single(...), staleTime: Infinity }) shape, so this stays consistent with the rest of the app.

Skipped (out of scope / per "keep changes minimal"):

  • LyricsSettings.tsx L71-77 nitpick (💤 Low value) — the four useState + useEffect pairs are duplicative but harmless; extracting a usePreference hook is a stylistic refactor that belongs in a separate PR.

Could you re-review? If anything else surfaces (e.g. a missing enabled: isLyricsVisible interaction or a cache-key collision with lyricsQuery.single), happy to address.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Commit 0f88a242 diff ==="
gh api repos/Sandakan/Nora/commits/0f88a242 --jq '.files[] | {filename: .filename, patch: .patch}'

echo "=== New queries/lyrics.ts ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/queries/lyrics.ts?ref=0f88a242 \
  --jq '.content' | base64 -d

echo "=== Updated LyricsContainer.tsx ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx?ref=0f88a242 \
  --jq '.content' | base64 -d | cat -n

Length of output: 17032



@Owie6789 — Good refactor overall. Inspected the new commit. The useQuery approach is clean, the queryFn is well-guarded, and there is no cache key collision with lyricsQuery.single (the factory namespaces them as ['lyrics', 'fullScreenPlayer', ...] vs ['lyrics', 'single', ...], so the fsp- prefix is harmless redundancy rather than a requirement). Two issues remain, one of which is the flash blocker in a slightly new form:


🔴 Blocker — enabled: isLyricsVisible never fires in side-by-side mode

LyricsContainer.tsx Line 40

enabled: isLyricsVisible   // ← isShowLyricsWithSongInfo is not considered

In side-by-side mode isShowLyricsWithSongInfo is true and lyrics should always load, but isLyricsVisible can be false (it's the useState that was only initialized from the preference at mount — the major finding still open). The query will never be triggered in side-by-side mode and the container will silently stay empty.

Fix:

enabled: isLyricsVisible || !!isShowLyricsWithSongInfo

🔴 Blocker — "No lyrics" flash is still present (sentinel shifted from null to undefined)

LyricsContainer.tsx Line 148

{(isShowLyricsWithSongInfo || isLyricsVisible) && !lyrics && (
  /* No lyrics available panel */
)}

With useQuery, data starts as undefined while the fetch is in flight (React Query's initial value before the queryFn resolves), then becomes null once the queryFn returns null (no lyrics / error). !lyrics is truthy for both states, so the "No lyrics" panel flashes for every song that has lyrics — same root problem as before, just with undefined as the loading sentinel instead of null.

Fix — destructure isPending and gate on it:

const { data: lyrics, isPending } = useQuery({
  ...lyricsQuery.fullScreenPlayer({ ... }),
  enabled: isLyricsVisible || !!isShowLyricsWithSongInfo
});

// …

{(isShowLyricsWithSongInfo || isLyricsVisible) && !isPending && !lyrics && (
  <div className="text-font-color-highlight …">
    {/* No lyrics available */}
  </div>
)}

isPending is true only while the query is actively fetching (and enabled is true), so this correctly suppresses the panel during the load and shows it only when the fetch is genuinely complete and returned nothing.


🟡 Minor — staleTime not set (unnecessary refetch on window focus)

LyricsContainer.tsx Line 27

The existing lyricsQuery.single usage elsewhere in the app passes staleTime: Infinity at the call site. Without it, TanStack Query's default staleTime: 0 marks the data stale immediately, so focusing the app window will trigger a refetch of the entire translate/convert pipeline. Lyrics don't change mid-song, so:

const { data: lyrics, isPending } = useQuery({
  ...lyricsQuery.fullScreenPlayer({ ... }),
  enabled: isLyricsVisible || !!isShowLyricsWithSongInfo,
  staleTime: Infinity
});

✅ Confirmed resolved by this commit

  • Cache key isolation: no collision with lyricsQuery.single — confirmed.
  • queryFn error handling: all async paths return null on failure; error is logged once — clean.
  • Translation/conversion pipeline: null-guarded at each step, isReset flag checked before chaining — correct.
  • setIsLyricsAvailable effect: correctly minimal, runs only when lyrics changes — correct.
  • useSkipLyricsLines(lyrics ?? null): the ?? null coercion correctly passes null when data is undefined — correct.

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 5, 2026
…lyrics panel on isPending, add staleTime Infinity
@Owie6789

Owie6789 commented Jun 5, 2026

Copy link
Copy Markdown
Author

@coderabbitai — final comprehensive re-review pass. Please re-review the latest commit on this PR head branch and answer four specific questions in one response.

Do not narrow scope to a checklist. Inspect the entire diff history of this branch (every commit between this branch and upstream/release/4.0.0-alpha.5) and walk the code in context.

What I want you to verify

1. Re-review the latest commit

Review every new change I just pushed against the full current state of the branch. Surface any new blocker, major, minor, or nit you find.

2. Original issue fulfillment

The PR's "Closes #N" issue defines the user-visible problem this branch is meant to fix. For each "Closes" issue, walk the issue text and tell me:

  • The exact behavior the issue describes
  • The exact behavior the PR's final code produces
  • Whether the user-visible problem is actually solved (e.g., the bug is gone, the feature works in the documented happy path and the obvious edge cases)
  • If you can't be sure, say so explicitly and say what you would need to confirm

3. Deviation from issue fix

Compare every commit on this branch to the original issue. Did any commit pull the scope in a direction the issue did not ask for? Examples to look for:

  • New abstractions, helpers, or files that the issue did not ask for
  • Refactors of unrelated code that ride along with the fix
  • Configuration changes the issue did not justify
  • Re-shaped public APIs or component contracts the issue did not require
  • Localization keys or i18n additions the issue did not require (these often balloon the diff)

If you find any, name the commit, the file, and the lines, and say whether you think the deviation is justified (e.g., "required for the fix to be sound") or removable.

4. Over-sieveing

Walk the whole diff and tell me if any of the changes are over-engineered for the original issue. Examples:

  • Helpers and utilities that are only used once inside this PR
  • Defensive null checks / type guards for values that the actual call sites can never produce
  • Generic abstractions over a single concrete case
  • Excessive error-handling layering for an error path that the existing module already covers
  • "While I'm here" cleanups of code adjacent to the fix

For each one, name it, and say whether it should be kept, narrowed, or removed.

What to skip

  • I am not asking you to re-verify the comments / formatting / naming-convention items you already approved in earlier passes
  • I am not asking you to re-litigate the very-nits you raised on the previous re-review for which I just pushed a fix in this round; only call them out if the new push regresses them
  • I am not asking for a fresh exhaustive lint pass

Output format

Reply with four clearly-labeled sections — ## 1. Re-review of latest commit, ## 2. Original issue fulfillment, ## 3. Deviation from issue fix, ## 4. Over-sieveing. In each, lead with the verdict (Pass / Concern), then a concise list of named items (file:line), then a short rationale. Keep the whole reply under ~120 lines so I can act on it.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Current PR head ==="
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, branch: .head.ref, base: .base.ref}'

echo "=== All commits on branch ==="
gh api "repos/Sandakan/Nora/pulls/496/commits" --jq '.[].sha + " " + .[].commit.message'

echo "=== Issue `#461` body ==="
gh api repos/Sandakan/Nora/issues/461 --jq '.body'

echo "=== Full PR diff (latest) ==="
gh pr diff 496 --repo Sandakan/Nora

Length of output: 30962



@Owie6789 — Analysis complete against HEAD c089c7e0. Four sections below, each verdict-first.


1. Re-review of latest commit (c089c7e0)

Verdict: One blocker still open; the three fixes from my last pass are correct.

✅ Fixed by this commit

  • enabled: isLyricsVisible || !!isShowLyricsWithSongInfo — query now fires in side-by-side mode regardless of the isLyricsVisible toggle state.
  • !isPending && !lyrics — "No lyrics" panel is suppressed while the fetch is in flight; flash is gone.
  • staleTime: Infinity — no spurious refetches on window focus; consistent with lyricsQuery.single usage.

🔴 Blocker — SeekBarSlider layout not fixed (FullScreenPlayer.tsx Lines 61 + 99)

The outer container is still conditionally flex-row when isShowLyricsWithSongInfo is true:

className={`flex max-w-full ${isShowLyricsWithSongInfo ? 'flex-row' : 'flex-col'} justify-end ...`}

SeekBarSlider (Line 99) is a sibling of the <div className="flex h-full w-full flex-row overflow-hidden"> two-column wrapper. That wrapper declares w-full, which in a flex-row parent consumes 100% of horizontal space — the seek bar has zero room and is pushed off-screen or squashed to nothing.

Additionally, the inner <div className="flex h-full w-full flex-row overflow-hidden"> declares h-full, but its parent (the outer wrapper) has no h-full, so the height-fill chain is broken: columns won't expand to fill the viewport either.

Minimal fix (same as previously recommended — still applies):

{isShowLyricsWithSongInfo ? (
  <div className="flex h-full w-full flex-col">        {/* ← flex-col, not flex-row */}
    <div className="flex min-h-0 flex-1 flex-row overflow-hidden">
      <div className="flex w-1/2 items-end">
        <SongInfoContainer ... />
      </div>
      <div className="w-1/2 overflow-auto">
        <LyricsContainer ... />
      </div>
    </div>
    <SeekBarSlider ... />    {/* ← lives inside the wrapper, below columns */}
  </div>
) : (
  <>
    <LyricsContainer ... />
    <SongInfoContainer ... />
    <SeekBarSlider ... />
  </>
)}

And simplify the outer div to always flex-col (it no longer needs to toggle its axis):

className={`flex max-w-full flex-col justify-end ${isMouseActive && 'group/fullScreenPlayer'}`}

🟡 Nit — py-4 pr-4 pl-4p-4 (LyricsContainer.tsx Line ~122)

py-4 pr-4 pl-4 is identical to p-4. Not blocking, just noise.


2. Original issue fulfillment

Verdict: Conditionally passes — happy path works; seek-bar blocker prevents it from being fully usable.

What issue #461 (point 2) describes:

"In fullscreen or 'Now Playing' mode, opening lyrics currently replaces the song details entirely. It would improve usability if both lyrics and song information (title, artist, artwork) could be displayed simultaneously."

What the PR's final code produces:

  • Adds an opt-in preference showLyricsWithSongInfo (off by default, persisted to localStorage).
  • When enabled: fullscreen switches to a 50/50 flex-row split — SongInfoContainer left, LyricsContainer right.
  • SongInfoContainer stays visible (no opacity-0/invisible hiding), lyrics toggle is hidden.
  • Lyrics are fetched automatically (no manual toggle needed).
  • Setting is accessible at Settings → Lyrics.

Is the problem solved?
The core ask — seeing both at once — is implemented and the preference survives restarts. However, with the SeekBarSlider blocker unresolved, the seek bar (and potentially playback controls) would be invisible or inaccessible in side-by-side mode, making this mode barely usable in practice. I can't confirm it fully passes until that layout bug is fixed.


3. Deviation from issue fix

Verdict: Two deviations; one unjustified, one borderline.

Commit File Lines Deviation Justified?
6f8476ee (ipc imports) src/main/ipc.ts 88–89 toggleAutoLaunch + toggleMiniPlayerAlwaysOnTop imports — entirely unrelated to lyrics or fullscreen layout. ❌ Not justified. Move to a separate PR.
0f88a242 (query refactor) src/renderer/src/queries/lyrics.ts 47–118 New lyricsQuery.fullScreenPlayer factory — replaces a component-local useEffect with a TanStack Query abstraction. The issue only asked for a side-by-side layout, not a query refactor. ⚠️ Borderline — it fixes the flash/null-sentinel bugs that the simple useEffect approach couldn't handle cleanly, so it's arguably required for the fix to be sound. Keeping it is defensible.

4. Over-engineering

Verdict: Two items worth noting; neither is a blocker.

  1. String-concatenated query keys in lyricsQuery.fullScreenPlayer (queries/lyrics.ts Lines 54–63)

    queryKey: [
      `fsp-title=${title}`,
      `fsp-artists=${artists.join(',')}`,
      ...
    ]

    TanStack Query serializes nested objects natively (e.g. ['lyrics', 'fullScreenPlayer', { title, artists, ... }]). The fsp- prefix strings are a manual collision-avoidance pattern that createQueryKeys already handles by namespacing. The fsp-artists=a,b,c string also loses array identity (an artist named "a,b" collides with ["a","b"]). Recommendation: narrow — use a structured object key instead:

    queryKey: [{ title, artists, album, path, duration, language, autoTranslate, autoConvert }]
  2. lyricsQuery.fullScreenPlayer as a single-use factory (queries/lyrics.ts Lines 47–118, used only in LyricsContainer.tsx)
    The translate → convert pipeline it contains duplicates logic that the original useEffect (now removed) and presumably lyricsQuery.single both implement. As a single-use abstraction it adds ~70 lines of new code for one call site. Recommendation: keep — the caching/dedup/retry benefits are real, and the error-handling is cleaner than the original useEffect. But consider whether lyricsQuery.single could be extended to accept autoTranslate/autoConvert params instead of maintaining two parallel pipelines. That consolidation is a follow-up, not a blocker.

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 6, 2026
…ined for lyrics loading, sync isLyricsVisible with preference
@Owie6789

Owie6789 commented Jun 6, 2026

Copy link
Copy Markdown
Author

@CodeRabbit, sorry i forgot to commit all my changes before asking that of you reapply and rereview that prompt i sad previously now that i have properly commited

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Current PR head ==="
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, branch: .head.ref, base: .base.ref}'

echo "=== All commits on branch (newest first) ==="
gh api "repos/Sandakan/Nora/pulls/496/commits" --jq '.[].sha + " " + .[].commit.message'

echo "=== Full PR diff ==="
gh pr diff 496 --repo Sandakan/Nora

echo "=== Issue `#461` body ==="
gh api repos/Sandakan/Nora/issues/461 --jq '.body'

Length of output: 34786



@Owie6789 — No worries! Here is the full four-section review against HEAD 41612701.


1. Re-review of latest commit

Verdict: Two blockers remain; two new unrelated changes introduced.

✅ Addressed since last pass

  • useEffect sync of isLyricsVisible with isShowLyricsWithSongInfo — correctly added with an !== undefined guard. ✅
  • enabled: isLyricsVisible || !!isShowLyricsWithSongInfo — query now fires in side-by-side mode. ✅
  • staleTime: Infinity — no spurious refetches. ✅
  • lyrics !== null guard on synced/unsynced lyrics blocks — prevents content flash during load. ✅

🔴 Blocker 1 — SeekBarSlider still off-screen in side-by-side mode

FullScreenPlayer.tsx Lines 67 + 108

The outer container still toggles flex-row when isShowLyricsWithSongInfo is true:

className={`flex max-w-full ${isShowLyricsWithSongInfo ? 'flex-row' : 'flex-col'} justify-end ...`}

The SeekBarSlider (Line 108) is a direct sibling of the inner <div className="flex h-full w-full flex-row overflow-hidden">, which has w-full. Inside a flex-row parent, that consumes 100% of horizontal space — the seek bar has zero room and is pushed off-screen or squashed to nothing. This fix was recommended in both previous passes and is still missing.

Minimal fix — wrap the two-column block and the seek bar together in a flex-col div:

{isShowLyricsWithSongInfo ? (
  <div className="flex h-full w-full flex-col">
    <div className="flex min-h-0 flex-1 flex-row overflow-hidden">
      <div className="flex w-1/2 items-end">
        <SongInfoContainer ... isShowLyricsWithSongInfo={isShowLyricsWithSongInfo} />
      </div>
      <div className="w-1/2 overflow-auto">
        <LyricsContainer ... isShowLyricsWithSongInfo={isShowLyricsWithSongInfo} />
      </div>
    </div>
    <SeekBarSlider ... />   {/* ← inside the wrapper, below columns */}
  </div>
) : (
  <>
    <LyricsContainer ... />
    <SongInfoContainer ... />
    <SeekBarSlider ... />
  </>
)}

And change the outer div to always flex-col (the inner wrapper owns the axis now):

className={`flex max-w-full flex-col justify-end ${isMouseActive && 'group/fullScreenPlayer'}`}

🔴 Blocker 2 — "No lyrics" flash still present; sentinel is wrong

LyricsContainer.tsx Line 148 (no-lyrics panel condition)

{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
  /* No lyrics panel */
)}

With TanStack Query, data is undefined during the pending/loading state (before the queryFn resolves). The queryFn returns null when lyrics are definitively not found. So lyrics === undefined fires while the fetch is still in flight — producing the same flash as before, just with undefined as the loading sentinel instead of null.

isPending is already destructured on the same line but never used in the render block (TypeScript will warn about the unused variable).

Fix — either gate on isPending or change the sentinel:

{(isShowLyricsWithSongInfo || isLyricsVisible) && !isPending && lyrics === null && (
  <div ...>no lyrics</div>
)}

lyrics === null is only true after the queryFn completes and definitively finds no lyrics. lyrics === undefined means "not fetched yet."


🟠 New: Unrelated scope creep — tabIndex changes

MainContainer.tsx Line 62 · SecondaryContainer.tsx Line 22

Both change tabIndex={focusable ? 1 : undefined}tabIndex={focusable ? 0 : undefined}. The fix is correct (tabIndex > 0 breaks natural tab order and is a known a11y anti-pattern), but it has nothing to do with lyrics side-by-side. Please move to a separate PR, or keep if you are batching a11y fixes intentionally — but call it out in the PR description.


2. Original issue fulfillment

Verdict: Conditionally passes — the concept is correct; SeekBar blocker and no-lyrics flash prevent a fully usable release.

What issue #461 point 2 describes:

"In fullscreen or 'Now Playing' mode, opening lyrics currently replaces the song details entirely. It would improve usability if both lyrics and song information (title, artist, artwork) could be displayed simultaneously."

What the PR produces (when both blockers are fixed):

  • Opt-in showLyricsWithSongInfo preference, off by default, persisted to localStorage and dispatched to the Zustand store immediately ✅
  • When on: fullscreen renders a 50/50 flex-row split with SongInfoContainer left and LyricsContainer right ✅
  • SongInfoContainer stays visible at full opacity; lyrics toggle button is hidden ✅
  • Lyrics are fetched automatically via useQuery without the user pressing the toggle ✅
  • Setting is accessible at Settings → Lyrics ✅

The core user ask is addressed. The two blockers must be fixed before this is usable — in side-by-side mode the seek bar (and playback control) would be unreachable, and every song briefly shows "No lyrics available" during load.


3. Deviation from issue fix

Verdict: Four deviations found; two unjustified.

Commit File Lines Deviation Justified?
6f8476ee src/main/ipc.ts 88–89 toggleAutoLaunch + toggleMiniPlayerAlwaysOnTop imports — unrelated to lyrics ❌ Move to separate PR
0f88a242 src/renderer/src/queries/lyrics.ts 47–118 lyricsQuery.fullScreenPlayer — query refactor not asked for by issue ⚠️ Borderline justified — needed to correctly distinguish the null/undefined loading sentinel and get caching; keeping is defensible
41612701 src/renderer/src/components/MainContainer.tsx 62 tabIndex 1 → 0 — correct a11y fix, unrelated to lyrics ❌ Should be a separate PR or at least called out in the description
41612701 src/renderer/src/components/SecondaryContainer.tsx 22 Same tabIndex fix ❌ Same as above

4. Over-engineering

Verdict: Two items; one should be narrowed, one is borderline keep.

  1. String-concatenated query keys (queries/lyrics.ts Lines 55–65) — narrow

    queryKey: [`fsp-title=${title}`, `fsp-artists=${artists.join(',')}`, ...]

    TanStack Query serializes nested objects natively. The fsp- prefix strings also introduce an artist-name collision bug: an artist named "a,b" produces the same key segment as ["a", "b"]. Use a structured object instead:

    queryKey: [{ scope: 'fullScreenPlayer', title, artists, album, path, duration, language, autoTranslate, autoConvert }]
  2. lyricsQuery.fullScreenPlayer as a single-use factory (queries/lyrics.ts Lines 47–118, called only in LyricsContainer.tsx) — keep (borderline)
    The fetch + translate + convert pipeline here duplicates logic from the old useEffect, but the caching, dedup, and retry are genuine wins. Worth noting as a follow-up: consider whether lyricsQuery.single could be extended to accept autoTranslate/autoConvert params to avoid two parallel pipelines long-term.


TL;DR for your next fix pass: The two items that must be resolved before merge are the SeekBarSlider layout (flex-row outer container + seek bar sibling) and the lyrics === undefined!isPending && lyrics === null sentinel swap in the no-lyrics panel condition.

( ˶ᵔᵕᵔ˶ ) 🐇

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 6, 2026
…o-lyrics flash, query keys

- Blocker 1: Fix SeekBarSlider layout in side-by-side mode by wrapping two-column block + seek bar in flex-col div inside isShowLyricsWithSongInfo branch, outer div always flex-col
- Blocker 2: Fix no-lyrics flash by changing sentinel from lyrics===undefined to !isPending && lyrics===null
- Over-engineering: Replace string-concatenated query keys with structured objects in lyricsQuery.single and lyricsQuery.fullScreenPlayer
@Owie6789

Owie6789 commented Jun 9, 2026

Copy link
Copy Markdown
Author

@coderabbitai rereview

Merge conflict with upstream/release/4.0.0-alpha.5 resolved:

  • Conflict: src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx — the upstream stacked LyricsContainer/SongInfoContainer layout conflicted with the fix branch's new conditional side-by-side layout (isShowLyricsWithSongInfo).
  • Resolution: Kept the fix branch's side-by-side layout structure (the purpose of this PR) while incorporating the upstream's before:max-w-full CSS addition to the seekbar slider in both layout branches.
  • SHA: 931a4a6b

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Current PR head ==="
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, branch: .head.ref}'

echo "=== All commits ==="
gh api "repos/Sandakan/Nora/pulls/496/commits" --jq '.[].sha + " " + .[].commit.message'

echo "=== Full PR diff ==="
gh pr diff 496 --repo Sandakan/Nora

echo "=== FullScreenPlayer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

echo "=== LyricsContainer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

Length of output: 56290



@Owie6789 — Good work resolving the merge conflict. The before:max-w-full is correctly applied to both SeekBarSlider instances. Here is the re-review against HEAD 931a4a6b.


✅ All previous blockers are resolved

Item Status
SeekBarSlider layout (flex-row outer → flex-col wrapper) ✅ Fixed — outer div is always flex-col; side-by-side branch wraps columns + slider in a shared flex-col div (Line 71). The absolute h-fit w-full on the slider positions it relative to the root .full-screen-player (which holds relative); the grid h-screen grid-rows-[auto_1fr] chain correctly gives h-full a defined size.
No-lyrics flash (!isPending && lyrics === null) ✅ Fixed — TanStack Query: isPending=true while fetching or when enabled:false; data=null only after queryFn returns null; "No lyrics" panel is correctly gated.
isLyricsVisible stale initial value ✅ Fixed — useEffect syncs on isShowLyricsWithSongInfo changes with an !== undefined guard.
enabled misses side-by-side mode ✅ Fixed — `enabled: isLyricsVisible
staleTime: Infinity ✅ Present.
String-concatenated query keys ✅ Replaced with structured objects in both lyricsQuery.single and lyricsQuery.fullScreenPlayer.
Merge conflict before:max-w-full ✅ Applied to both layout branches.

⚠️ Still open before merge

🟠 Major — src/main/ipc.ts Lines 88–89 (unresolved since commit 6f8476ee)

toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop imports are unrelated to this feature. Still present in this PR. Please move to a separate PR or add a clear inline comment explaining the dependency.


🟡 Minor / Nits (non-blocking)

  • FullScreenPlayer.tsx Line 61 — Dead ternary still present: ${isLyricsVisible ? 'blur-[2rem]!' : 'blur-[2rem]!'}. Both branches are identical; simplify to a static class blur-[2rem]!.
  • LyricsContainer.tsx Line 125py-4 pr-4 pl-4p-4 (same output, cleaner).
  • FullScreenPlayer.tsx Lines 45–47useMemo(() => currentSongData.artworkPath, [...]) wraps a single property access; a plain const is sufficient.

TL;DR: No blockers remain. The one item requiring attention before merge is the unrelated ipc.ts imports. The rest are nits you can address now or in a follow-up.

( ˶ᵔᵕᵔ˶ ) 🐇

@Owie6789

Copy link
Copy Markdown
Author

@coderabbitai rereview

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Current PR head ==="
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, branch: .head.ref}'

echo "=== All commits ==="
gh api "repos/Sandakan/Nora/pulls/496/commits" --jq '.[].sha + " " + .[].commit.message'

echo "=== Full PR diff ==="
gh pr diff 496 --repo Sandakan/Nora

echo "=== FullScreenPlayer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

echo "=== LyricsContainer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

echo "=== ipc.ts lines 85-95 ==="
gh api repos/Sandakan/Nora/contents/src/main/ipc.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | sed -n '84,96p'

Length of output: 57063



@Owie6789 — No new commits have landed since my last review: the PR head is still 931a4a6b, which is the same SHA I reviewed in the previous pass.

The current standing hasn't changed:

Status Item
✅ Resolved SeekBarSlider flex-col layout fix
✅ Resolved No-lyrics flash (!isPending && lyrics === null)
✅ Resolved isLyricsVisible sync effect
✅ Resolved enabled: isLyricsVisible || !!isShowLyricsWithSongInfo
✅ Resolved staleTime: Infinity
✅ Resolved Structured object query keys
🟠 Major — still open src/main/ipc.ts Lines 88–89: toggleAutoLaunch + toggleMiniPlayerAlwaysOnTop are unrelated scope creep
🟡 Minor nit FullScreenPlayer.tsx Line 61: dead ternary — both branches produce blur-[2rem]!, simplify to a static class
🟡 Minor nit LyricsContainer.tsx Line 125: py-4 pr-4 pl-4p-4
🟡 Minor nit FullScreenPlayer.tsx Lines 45–47: useMemo over a single property access — a plain const suffices

It looks like your intended commits may not have been pushed yet. Once you push, tag me again and I'll pick up the new SHA immediately.

( ˶ᵔᵕᵔ˶ ) 🐇

Owie6789 added a commit to Owie6789/Nora that referenced this pull request Jun 12, 2026
- Remove unnecessary useMemo wrapping a plain property access
- Remove dead ternary where both branches produced the same class
- Replace py-4 pr-4 pl-4 with equivalent p-4 in side-by-side layout
@Owie6789

Copy link
Copy Markdown
Author

@coderabbitai review my last commit and see if it satisfies what you asked and the original issue

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Current PR head ==="
gh api repos/Sandakan/Nora/pulls/496 --jq '{sha: .head.sha, branch: .head.ref}'

echo "=== All commits ==="
gh api "repos/Sandakan/Nora/pulls/496/commits" --jq '.[].sha + " " + .[].commit.message'

echo "=== Full PR diff ==="
gh pr diff 496 --repo Sandakan/Nora

echo "=== FullScreenPlayer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

echo "=== LyricsContainer.tsx at HEAD ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsx?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n

echo "=== ipc.ts lines 84-96 ==="
gh api repos/Sandakan/Nora/contents/src/main/ipc.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | sed -n '84,96p'

Length of output: 62559



@Owie6789 — Reviewed HEAD f967cc65. Here's the full accounting:


✅ Everything previously flagged is resolved

Item Status
SeekBarSlider off-screen in flex-row (Blocker) ✅ Fixed — outer div always flex-col; flex-col wrapper contains columns + slider
No-lyrics flash !isPending && lyrics === null (Blocker) ✅ Fixed — correct sentinel in place
isLyricsVisible stale initial value (Major) ✅ Fixed — useEffect sync with !== undefined guard
Query not enabled in side-by-side mode (Major) ✅ Fixed — enabled: isLyricsVisible || !!isShowLyricsWithSongInfo
staleTime: Infinity ✅ Present
Structured object query keys ✅ Both lyricsQuery.single and lyricsQuery.fullScreenPlayer updated
Dead ternary blur-[2rem]! (nit) ✅ Fixed — static class
py-4 pr-4 pl-4p-4 (nit) ✅ Fixed
useMemo over single property access (nit) ✅ Removed

⚠️ One item still open

🟠 src/main/ipc.ts Lines 88–89toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop imports are still present and unrelated to this feature. This is the sole remaining item before the PR is clean. Please either move it to a separate PR or add a brief inline comment explaining the dependency (e.g., these were missing and caused a runtime error that blocked this feature).


Original issue fulfillment

✅ Passes — Issue #461's request (lyrics and song details visible simultaneously in fullscreen/Now Playing mode) is fully addressed:

  • Opt-in showLyricsWithSongInfo preference, off by default, persisted to localStorage and dispatched live to the Zustand store ✅
  • 50/50 side-by-side layout with SongInfoContainer left and scrollable LyricsContainer right ✅
  • SongInfoContainer stays fully visible; lyrics toggle hidden in this mode ✅
  • Lyrics fetched automatically; no manual toggle required ✅
  • Setting accessible at Settings → Lyrics with i18n labels ✅

Fix the ipc.ts imports and this is ready for merge. 🐇

@Owie6789

Copy link
Copy Markdown
Author

@coderabbitai the ipc.ts has zero diff from release/4.0.0-alpha.5 — the imports in question already exist in the base branch. the commit 6f8476e that added them was redundant (already in base). can you recheck the diff between this PR's head and the base?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PR diff filtered to ipc.ts only ==="
gh pr diff 496 --repo Sandakan/Nora -- src/main/ipc.ts

echo "=== ipc.ts on base branch (release/4.0.0-alpha.5) lines 84-96 ==="
gh api repos/Sandakan/Nora/contents/src/main/ipc.ts?ref=release/4.0.0-alpha.5 \
  --jq '.content' | base64 -d | cat -n | sed -n '84,96p'

echo "=== ipc.ts on PR head lines 84-96 ==="
gh api repos/Sandakan/Nora/contents/src/main/ipc.ts?ref=refs/pull/496/head \
  --jq '.content' | base64 -d | cat -n | sed -n '84,96p'

Length of output: 1884



@Owie6789 — You are completely right. The diff confirms it: toggleAutoLaunch and toggleMiniPlayerAlwaysOnTop are present at Lines 88–89 in the base branch (release/4.0.0-alpha.5) and the PR head is byte-for-byte identical. ipc.ts has no diff in this PR.

I was wrong to keep flagging that as scope creep across multiple review passes — commit 6f8476ee's changes had already been incorporated into the base branch before this PR was opened, so nothing in ipc.ts originates from this PR.

With that corrected, this PR has no remaining open issues. All previous blockers and majors have been resolved, and the ipc.ts concern is withdrawn. The PR is clean for merge.

( ˶ᵔᵕᵔ˶ ) 🐇


✏️ Learnings added
Learnt from: Owie6789
URL: https://github.com/Sandakan/Nora/pull/496

Timestamp: 2026-06-12T15:52:22.894Z
Learning: In the Nora repository (GitHub: Sandakan/Nora), when reviewing PR `#496` (branch fix/461-lyrics-songdetails-sidebyside targeting release/4.0.0-alpha.5), the imports `toggleAutoLaunch` and `toggleMiniPlayerAlwaysOnTop` in `src/main/ipc.ts` Lines 88-89 already exist in the base branch `release/4.0.0-alpha.5`. These are NOT scope creep introduced by this PR. Do not flag them as unrelated changes in any future review of this file or PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Owie6789 added 6 commits June 12, 2026 18:24
- Add lyricsQuery.fullScreenPlayer to src/renderer/src/queries/lyrics.ts that wraps getSongLyrics + getTranslatedLyrics + convertLyricsToPinyin/romanizeLyrics/convertLyricsToRomaja, respecting autoTranslate/autoConvert preferences and language. Returns safe null default on error instead of throwing.

- Replace LyricsContainer useEffect with useQuery(lyricsQuery.fullScreenPlayer(...), { enabled: isLyricsVisible }), giving us TanStack Query caching/dedup/retry for free.

- Move setIsLyricsAvailable into a small effect that mirrors useQuery data.
…lyrics panel on isPending, add staleTime Infinity
…ined for lyrics loading, sync isLyricsVisible with preference
…o-lyrics flash, query keys

- Blocker 1: Fix SeekBarSlider layout in side-by-side mode by wrapping two-column block + seek bar in flex-col div inside isShowLyricsWithSongInfo branch, outer div always flex-col
- Blocker 2: Fix no-lyrics flash by changing sentinel from lyrics===undefined to !isPending && lyrics===null
- Over-engineering: Replace string-concatenated query keys with structured objects in lyricsQuery.single and lyricsQuery.fullScreenPlayer
- Remove unnecessary useMemo wrapping a plain property access
- Remove dead ternary where both branches produced the same class
- Replace py-4 pr-4 pl-4 with equivalent p-4 in side-by-side layout
@Owie6789 Owie6789 force-pushed the fix/461-lyrics-songdetails-sidebyside branch from f967cc6 to e3592b8 Compare June 12, 2026 17:26
@Owie6789

Copy link
Copy Markdown
Author

@Sandakan ready to merge. CR verdict: clean (latest commit f967cc6 + rebase pass). All flagged items resolved incl. the ipc.ts false-positive? Branch is MERGEABLE / CLEAN.

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