feat(#461): lyrics and song details side by side in fullscreen mode#496
feat(#461): lyrics and song details side by side in fullscreen mode#496Owie6789 wants to merge 7 commits into
Conversation
f1258d8 to
b8d731a
Compare
|
@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
2. UI audit (only if the PR touches UI)
3. Production-readiness verification
4. Code smell sniff
5. Half-baked implementation detection
6. Required output from you
Please be thorough. I will be applying fixes from your review in a follow-up pass. |
|
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/NoraLength 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 -30Length 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 -80Length 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
Review Summary✅ What is solid
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis 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. ChangesShow Lyrics with Song Info
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
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 liftExtract lyrics fetching into centralized query module.
This component violates coding guidelines by implementing inline data fetching in
useEffectinstead of using the project's query pattern withuseSuspenseQuery()and centralized query modules.Issues:
- Line 63: Errors are swallowed with
console.errorinstead 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-factoryin 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 valueConsider consolidating repeated preference initialization pattern.
The file now has four similar
useState+useEffectpairs (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
📒 Files selected for processing (8)
src/main/ipc.tssrc/renderer/src/assets/locales/en/en.jsonsrc/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsxsrc/renderer/src/components/FullScreenPlayer/containers/LyricsContainer.tsxsrc/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsxsrc/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsxsrc/renderer/src/other/appReducer.tsxsrc/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.tssrc/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsxsrc/main/ipc.tssrc/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsxsrc/renderer/src/other/appReducer.tsxsrc/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsxsrc/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-factoryin 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.tsxsrc/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsxsrc/renderer/src/other/appReducer.tsxsrc/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsxsrc/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.tsxsrc/renderer/src/components/SettingsPage/Settings/LyricsSettings.tsxsrc/renderer/src/other/appReducer.tsxsrc/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsxsrc/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 winArtwork fallback for artist online images remains intact; fullscreen background cover uses
currentSongData.artworkPathonly.
Insrc/renderer/src/components/FullScreenPlayer/FullScreenPlayer.tsx, the fullscreen backgroundimgPathnow maps directly tocurrentSongData.artworkPathand relies on<Img fallbackSrc={DefaultSongCover}>. The artist “online artwork” override logic is used elsewhere in fullscreen—src/renderer/src/components/FullScreenPlayer/containers/SongInfoContainer.tsxrenders artist avatars withsrc={artist.onlineArtworkPaths?.picture_small}andfallbackSrc={artist.artworkPath}—so that fallback path is not impacted by thisimgPathmemo change. However, I can’t confirm from the current evidence whethercurrentSongData.artworkPathitself 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 valueVerify overflow handling in side-by-side mode.
When
isShowLyricsWithSongInfois true, this container usesrelativepositioning withoutoverflow-auto(line 153), while the parent inFullScreenPlayer.tsxline 76 appliesoverflow-autoto the wrapping div. When false, this container itself hasoverflow-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 winScope 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 safetyLearnt 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 modulesLearnt 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
| {(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 && ( |
There was a problem hiding this comment.
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.
| {(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.
| const isShowLyricsWithSongInfo = preferences?.showLyricsWithSongInfo; | ||
|
|
||
| const [isLyricsVisible, setIsLyricsVisible] = useState(false); | ||
| const [isLyricsVisible, setIsLyricsVisible] = useState(isShowLyricsWithSongInfo); |
There was a problem hiding this comment.
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.
| 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).
- 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.
|
Addressed the major finding on Pushed
I went with Skipped (out of scope / per "keep changes minimal"):
Could you re-review? If anything else surfaces (e.g. a missing |
|
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 -nLength of output: 17032
🔴 Blocker —
|
…lyrics panel on isPending, add staleTime Infinity
|
@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 What I want you to verify1. Re-review the latest commitReview 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 fulfillmentThe 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:
3. Deviation from issue fixCompare 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:
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-sieveingWalk the whole diff and tell me if any of the changes are over-engineered for the original issue. Examples:
For each one, name it, and say whether it should be kept, narrowed, or removed. What to skip
Output formatReply with four clearly-labeled sections — |
|
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/NoraLength of output: 30962
1. Re-review of latest commit (
|
| 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. |
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.
-
String-concatenated query keys in
lyricsQuery.fullScreenPlayer(queries/lyrics.tsLines 54–63)queryKey: [ `fsp-title=${title}`, `fsp-artists=${artists.join(',')}`, ... ]
TanStack Query serializes nested objects natively (e.g.
['lyrics', 'fullScreenPlayer', { title, artists, ... }]). Thefsp-prefix strings are a manual collision-avoidance pattern thatcreateQueryKeysalready handles by namespacing. Thefsp-artists=a,b,cstring 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 }]
-
lyricsQuery.fullScreenPlayeras a single-use factory (queries/lyrics.tsLines 47–118, used only inLyricsContainer.tsx)
The translate → convert pipeline it contains duplicates logic that the originaluseEffect(now removed) and presumablylyricsQuery.singleboth 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 originaluseEffect. But consider whetherlyricsQuery.singlecould be extended to acceptautoTranslate/autoConvertparams instead of maintaining two parallel pipelines. That consolidation is a follow-up, not a blocker.
…ined for lyrics loading, sync isLyricsVisible with preference
|
@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 |
|
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
1. Re-review of latest commitVerdict: Two blockers remain; two new unrelated changes introduced. ✅ Addressed since last pass
🔴 Blocker 1 — SeekBarSlider still off-screen in side-by-side mode
The outer container still toggles className={`flex max-w-full ${isShowLyricsWithSongInfo ? 'flex-row' : 'flex-col'} justify-end ...`}The Minimal fix — wrap the two-column block and the seek bar together in a {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 className={`flex max-w-full flex-col justify-end ${isMouseActive && 'group/fullScreenPlayer'}`}🔴 Blocker 2 — "No lyrics" flash still present; sentinel is wrong
{(isShowLyricsWithSongInfo || isLyricsVisible) && lyrics === undefined && (
/* No lyrics panel */
)}With TanStack Query,
Fix — either gate on {(isShowLyricsWithSongInfo || isLyricsVisible) && !isPending && lyrics === null && (
<div ...>no lyrics</div>
)}
🟠 New: Unrelated scope creep —
|
| 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 |
|
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.
-
String-concatenated query keys (
queries/lyrics.tsLines 55–65) — narrowqueryKey: [`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 }]
-
lyricsQuery.fullScreenPlayeras a single-use factory (queries/lyrics.tsLines 47–118, called only inLyricsContainer.tsx) — keep (borderline)
The fetch + translate + convert pipeline here duplicates logic from the olduseEffect, but the caching, dedup, and retry are genuine wins. Worth noting as a follow-up: consider whetherlyricsQuery.singlecould be extended to acceptautoTranslate/autoConvertparams 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.
( ˶ᵔᵕᵔ˶ ) 🐇
…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
|
@coderabbitai rereview Merge conflict with
|
|
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 -nLength of output: 56290
✅ All previous blockers are resolved
|
|
@coderabbitai rereview |
|
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
The current standing hasn't changed:
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. ( ˶ᵔᵕᵔ˶ ) 🐇 |
- 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
|
@coderabbitai review my last commit and see if it satisfies what you asked and the original issue |
|
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
✅ Everything previously flagged is resolved
|
|
@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? |
|
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
I was wrong to keep flagging that as scope creep across multiple review passes — commit With that corrected, this PR has no remaining open issues. All previous blockers and majors have been resolved, and the ( ˶ᵔᵕᵔ˶ ) 🐇 ✏️ Learnings added
|
…dle null initial state
- 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
f967cc6 to
e3592b8
Compare
fixes #461
adds a showLyricsWithSongInfo preference that displays lyrics and song details side by side in fullscreen mode. disabled by default for backward compatibility.
Refs #461
Summary by CodeRabbit