feat(#461): add dedicated display mode with large album art showcase#497
feat(#461): add dedicated display mode with large album art showcase#497Owie6789 wants to merge 9 commits into
Conversation
|
there was no visual showcase mode. fullscreen player is more or less lyrics-focused. so built a dedicated display mode with large centered album art (50vh), oversized song title (text-7xl), artist name (text-3xl), and auto-hidden controls via useMouseActiveState. close button uses NavLink to /main-player/home matching the existing fullscreen/mini player pattern. layout reuses the blurred artwork background pattern from FullScreenPlayer. |
|
@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 info and diff
gh pr view 497 --repo Sandakan/Nora --json title,body,files,headRefName,baseRefName
echo "---DIFF---"
gh pr diff 497 --repo Sandakan/NoraLength of output: 21218 Good, I have the full diff. Let me now gather more context from the existing codebase to do a thorough comparative analysis. #!/bin/bash
# Get the FullScreenPlayer component for comparison
fd -i "FullScreenPlayer.tsx" --exec cat {}
echo "=== useMouseActiveState ==="
fd -i "useMouseActiveState*" --exec cat {}
echo "=== appReducer context around playerType persistence ==="
fd -i "appReducer.tsx" src/renderer/src/other/ --exec cat {}
echo "=== main.ts changePlayerType function context ==="
fd -i "main.ts" src/main/ --exec grep -n "changePlayerType\|PlayerTypes\|display\|fullscreen\|setFullScreen\|MAIN_WINDOW" {} | head -80
echo "=== Preferences interface / LocalStorage shape ==="
grep -n "displayModeIdleTimeout\|showSongTitleInDisplayMode\|showArtistNameInDisplayMode\|showControlsInDisplayMode\|LocalStorage\|Preferences" src/types/app.d.ts | head -60
echo "=== OtherSongControlsContainer full file ==="
fd -i "OtherSongControlsContainer.tsx" --exec cat {}
echo "=== existing NavLink component ==="
fd -i "NavLink.tsx" src/renderer/src/components/ --exec cat {}
echo "=== SeekBarSlider component ==="
fd -i "SeekBarSlider.tsx" src/renderer/src/components/ --exec cat {}
echo "=== index.tsx (router subscription) full ==="
cat src/renderer/src/index.tsx
echo "=== full appReducer UPDATE_PLAYER_TYPE case ==="
grep -n -A 20 "UPDATE_PLAYER_TYPE" src/renderer/src/other/appReducer.tsx
echo "=== fullscreen player route for comparison ==="
fd -i "index.tsx" src/renderer/src/routes/fullscreen-player/ --exec cat {}
echo "=== check for existing i18n locale files besides en ==="
fd -i "*.json" src/renderer/src/assets/locales/ --exec basename {} | sort
echo "=== check for any existing tests ==="
fd -i "*.test.*" src/ | head -20
fd -i "*.spec.*" src/ | head -20
echo "=== check how 'full' type is handled in appReducer ==="
grep -n -B5 -A30 "'full'" src/renderer/src/other/appReducer.tsx | head -60
echo "=== check how player type is persisted / saved ==="
grep -n "playerType\|saveUserSettings\|localStorage" src/renderer/src/other/appReducer.tsx | head -40
echo "=== check if display preferences have UI settings page ==="
grep -rn "showSongTitleInDisplayMode\|showArtistNameInDisplayMode\|showControlsInDisplayMode\|displayModeIdleTimeout" src/ | head -20
echo "=== check existing keyboard shortcuts / global keydown handlers ==="
grep -rn "Escape\|keydown\|keyup\|KeyboardEvent" src/renderer/src/components/DisplayModePlayer/ src/renderer/src/routes/display-player/ 2>/dev/null | head -20
grep -rn "Escape\|ArrowLeft\|ArrowRight\|Space" src/renderer/src/components/FullScreenPlayer/ 2>/dev/null | head -20
echo "=== check if DisplayModePlayer handles no-song state ==="
grep -n "currentSongData\|SongId\|songId" src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
echo "=== check cursor hiding approach in existing players ==="
grep -rn "cursor-none\|cursor-auto\|isMouseActive\|hideCursor" src/renderer/src/ | head -20
echo "=== check MAIN_WINDOW constants ==="
grep -n "MAIN_WINDOW_MAX_SIZE_X\|MAIN_WINDOW_MAX_SIZE_Y\|MAIN_WINDOW_MIN_SIZE_X\|MAIN_WINDOW_MIN_SIZE_Y\|MAIN_WINDOW_ASPECT_RATIO" src/main/main.ts | head -20
echo "=== check how fullscreen exit (Esc) works in existing full player ==="
grep -rn "fullscreen-player\|full.*player\|setFullScreen\|leaveFullScreen" src/ --include="*.ts" --include="*.tsx" | grep -v "routeTree\|node_modules" | head -30
echo "=== check appUpdateContext for updatePlayerType ==="
grep -rn "updatePlayerType\|UPDATE_PLAYER_TYPE" src/renderer/src/ | head -20
</thinking>
```shell
#!/bin/bash
echo "=== FullScreenPlayer component ==="
fd -i "FullScreenPlayer.tsx" --exec cat {}
echo "=== useMouseActiveState hook ==="
fd -i "useMouseActiveState*" --exec cat {}
echo "=== main.ts changePlayerType full function ==="
grep -n -A 40 "export async function changePlayerType" src/main/main.ts
echo "=== appReducer UPDATE_PLAYER_TYPE + defaults ==="
grep -n -B2 -A 30 "UPDATE_PLAYER_TYPE\|LOCAL_STORAGE_DEFAULT" src/renderer/src/other/appReducer.tsx | head -120
echo "=== how playerType saved/persisted ==="
grep -n "playerType\|saveUserSettings\|localStorage" src/renderer/src/other/appReducer.tsx | head -30
echo "=== display preferences usage in codebase ==="
grep -rn "showSongTitleInDisplayMode\|showArtistNameInDisplayMode\|showControlsInDisplayMode\|displayModeIdleTimeout" src/ 2>/dev/null
echo "=== cursor hide on idle - existing patterns ==="
grep -rn "cursor-none\|isMouseActive" src/renderer/src/ 2>/dev/null | grep -v "node_modules" | head -20
echo "=== how full player exits fullscreen ==="
grep -rn "setFullScreen\|leaveFullscreen\|fullscreen" src/main/main.ts | head -20
grep -rn "fullscreen-player\|miniPlayer\|changePlayerType" src/renderer/src/index.tsx
echo "=== OtherSongControlsContainer full file ==="
cat src/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx
echo "=== Display mode prefs settings page ==="
grep -rn "displayMode\|DisplayMode" src/renderer/src/components/ src/renderer/src/routes/ 2>/dev/null | grep -v "DisplayModePlayer\|routeTree\|node_modules" | head -20
echo "=== existing locale files ==="
fd -t f -e json . src/renderer/src/assets/locales/ | head -30
echo "=== check for console.log in new files ==="
grep -n "console\." src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
grep -n "console\." src/renderer/src/routes/display-player/index.tsx
echo "=== artworkPath on empty/no song ==="
grep -n "artworkPath\|currentSongData" src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
echo "=== MAIN_WINDOW size constants ==="
grep -n "MAIN_WINDOW_MAX_SIZE\|MAIN_WINDOW_MIN_SIZE\|MAIN_WINDOW_ASPECT" src/main/main.ts | head -10
echo "=== index.tsx full ==="
cat src/renderer/src/index.tsx
echo "=== existing tests ==="
fd -e "test.ts" -e "test.tsx" -e "spec.ts" -e "spec.tsx" . src/ 2>/dev/null | head -10#!/bin/bash
echo "=== FullScreenPlayer ==="
find . -name "FullScreenPlayer.tsx" -not -path "*/node_modules/*" -exec cat {} \;
echo "=== useMouseActiveState ==="
find . -name "useMouseActiveState*" -not -path "*/node_modules/*" -exec cat {} \;
echo "=== changePlayerType in main.ts ==="
sed -n '/export async function changePlayerType/,/^}/p' src/main/main.ts | head -60
echo "=== appReducer UPDATE_PLAYER_TYPE ==="
grep -n -A 20 "UPDATE_PLAYER_TYPE" src/renderer/src/other/appReducer.tsx
echo "=== LOCAL_STORAGE_DEFAULT_TEMPLATE ==="
grep -n -A 60 "LOCAL_STORAGE_DEFAULT_TEMPLATE" src/renderer/src/other/appReducer.tsx | head -80
echo "=== display preferences wired anywhere? ==="
grep -rn "displayModeIdleTimeout\|showSongTitleInDisplayMode\|showArtistNameInDisplayMode\|showControlsInDisplayMode" src/ 2>/dev/null
echo "=== cursor hide pattern ==="
grep -rn "cursor-none\|isMouseActive\|useMouseActiveState" src/renderer/src/ 2>/dev/null | grep -v "node_modules" | head -20
echo "=== what happens when user presses Escape in fullscreen ==="
grep -rn "Escape\|fullscreenchange\|setFullScreen" src/ 2>/dev/null | grep -v "node_modules\|routeTree" | head -20
echo "=== index.tsx ==="
cat src/renderer/src/index.tsx 2>/dev/null
echo "=== OtherSongControlsContainer ==="
cat src/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx 2>/dev/null
echo "=== locales list ==="
find src/renderer/src/assets/locales -name "*.json" 2>/dev/null | head -20
echo "=== SeekBarSlider ==="
find . -name "SeekBarSlider.tsx" -not -path "*/node_modules/*" -exec cat {} \; 2>/dev/null | head -80 |
|
@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:
📝 WalkthroughWalkthroughAdds a fullscreen "display mode" player: new player type and preferences, renderer routing and UI entry points, DisplayModePlayer component, reducer defaults, generated route updates, and main-process window placement/fullscreen handling. ChangesDisplay Mode Player Feature
Sequence DiagramsequenceDiagram
participant OtherSongControlsContainer
participant TanStackRouter
participant DisplayModePlayer
participant WindowControlsAPI
OtherSongControlsContainer->>TanStackRouter: navigate('/display-player')
TanStackRouter->>DisplayModePlayer: render route component
DisplayModePlayer->>WindowControlsAPI: (onRendered) changePlayerType('display')
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.
🧹 Nitpick comments (2)
src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx (1)
46-48: 💤 Low valueClarify preference default behavior.
The
!== falsepattern meansundefinedornullpreferences will be treated as truthy (showing controls). While this provides a sensible default, using explicit?? truewould be clearer and more maintainable.Suggested refinement for clarity
- const showSongTitle = preferences?.showSongTitleInDisplayMode !== false; - const showArtistName = preferences?.showArtistNameInDisplayMode !== false; - const showControls = preferences?.showControlsInDisplayMode !== false; + const showSongTitle = preferences?.showSongTitleInDisplayMode ?? true; + const showArtistName = preferences?.showArtistNameInDisplayMode ?? true; + const showControls = preferences?.showControlsInDisplayMode ?? true;🤖 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/DisplayModePlayer/DisplayModePlayer.tsx` around lines 46 - 48, The three booleans showSongTitle, showArtistName, and showControls currently use the pattern preferences?.<prop> !== false which treats undefined/null as truthy but is less explicit; replace those expressions to use the nullish coalescing default (e.g. preferences?.showSongTitleInDisplayMode ?? true, preferences?.showArtistNameInDisplayMode ?? true, preferences?.showControlsInDisplayMode ?? true) so missing preferences explicitly default to true while preserving an explicit false when set.src/main/main.ts (1)
868-876: ⚡ Quick winConsolidate duplicate fullscreen logic or document intent.
The
displaycase (lines 868-871) is identical to theelseblock (lines 872-876). Both set main window constraints and enable fullscreen. While keeping them separate allows future divergence, the current duplication is a code smell.Consider:
- Consolidate by handling
displayin theelseblock, or- Document why
displayis kept separate (e.g., anticipated future differences in window behavior).Option 1: Consolidate into else block
mainWindow.setAspectRatio(MAIN_WINDOW_ASPECT_RATIO); - } else if (type === 'display') { - mainWindow.setMaximumSize(MAIN_WINDOW_MAX_SIZE_X, MAIN_WINDOW_MAX_SIZE_Y); - mainWindow.setMinimumSize(MAIN_WINDOW_MIN_SIZE_X, MAIN_WINDOW_MIN_SIZE_Y); - mainWindow.setFullScreen(true); - } else { + } else { + // Handles 'full' and 'display' player types mainWindow.setMaximumSize(MAIN_WINDOW_MAX_SIZE_X, MAIN_WINDOW_MAX_SIZE_Y); mainWindow.setMinimumSize(MAIN_WINDOW_MIN_SIZE_X, MAIN_WINDOW_MIN_SIZE_Y); mainWindow.setFullScreen(true); }Option 2: Add clarifying comment
mainWindow.setAspectRatio(MAIN_WINDOW_ASPECT_RATIO); } else if (type === 'display') { + // Display mode uses main window constraints with fullscreen + // (kept separate from 'full' for potential future divergence) mainWindow.setMaximumSize(MAIN_WINDOW_MAX_SIZE_X, MAIN_WINDOW_MAX_SIZE_Y); mainWindow.setMinimumSize(MAIN_WINDOW_MIN_SIZE_X, MAIN_WINDOW_MIN_SIZE_Y); mainWindow.setFullScreen(true);🤖 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/main/main.ts` around lines 868 - 876, The two branches for when type === 'display' and the else branch contain identical window setup calls (mainWindow.setMaximumSize, mainWindow.setMinimumSize, mainWindow.setFullScreen), which is duplicated; either remove the redundant 'display' branch and let the else branch handle it, or keep both but add a concise comment above the 'display' branch explaining why it must remain separate for future behavior changes (e.g., "kept separate for future display-specific window behavior"). Ensure the change references the 'type' conditional and the mainWindow.setMaximumSize / setMinimumSize / setFullScreen calls so the duplicate logic is consolidated or documented.
🤖 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.
Nitpick comments:
In `@src/main/main.ts`:
- Around line 868-876: The two branches for when type === 'display' and the else
branch contain identical window setup calls (mainWindow.setMaximumSize,
mainWindow.setMinimumSize, mainWindow.setFullScreen), which is duplicated;
either remove the redundant 'display' branch and let the else branch handle it,
or keep both but add a concise comment above the 'display' branch explaining why
it must remain separate for future behavior changes (e.g., "kept separate for
future display-specific window behavior"). Ensure the change references the
'type' conditional and the mainWindow.setMaximumSize / setMinimumSize /
setFullScreen calls so the duplicate logic is consolidated or documented.
In `@src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx`:
- Around line 46-48: The three booleans showSongTitle, showArtistName, and
showControls currently use the pattern preferences?.<prop> !== false which
treats undefined/null as truthy but is less explicit; replace those expressions
to use the nullish coalescing default (e.g.
preferences?.showSongTitleInDisplayMode ?? true,
preferences?.showArtistNameInDisplayMode ?? true,
preferences?.showControlsInDisplayMode ?? true) so missing preferences
explicitly default to true while preserving an explicit false when set.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b5e4e50d-06f2-490d-8934-2134e6e6f6f4
📒 Files selected for processing (9)
src/main/main.tssrc/renderer/src/assets/locales/en/en.jsonsrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsxsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsxsrc/renderer/src/index.tsxsrc/renderer/src/other/appReducer.tsxsrc/renderer/src/routeTree.gen.tssrc/renderer/src/routes/display-player/index.tsxsrc/types/app.d.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/renderer/src/index.tsxsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsxsrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsxsrc/types/app.d.tssrc/renderer/src/routes/display-player/index.tsxsrc/main/main.tssrc/renderer/src/other/appReducer.tsxsrc/renderer/src/routeTree.gen.ts
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/index.tsxsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsxsrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsxsrc/renderer/src/routes/display-player/index.tsxsrc/renderer/src/other/appReducer.tsxsrc/renderer/src/routeTree.gen.ts
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/index.tsxsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsxsrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsxsrc/renderer/src/routes/display-player/index.tsxsrc/renderer/src/other/appReducer.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/main.ts
🧠 Learnings (8)
📚 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/renderer/src/index.tsxsrc/main/main.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/renderer/src/**/*.{ts,tsx} : Use TanStack Router's <Link>, useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions
Applied to files:
src/renderer/src/index.tsxsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsxsrc/renderer/src/routes/display-player/index.tsxsrc/renderer/src/routeTree.gen.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} : 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
Applied to files:
src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
📚 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/renderer/src/**/*.{ts,tsx} : Use dispatch() function from store.ts for all state updates in the renderer, never modify store state directly
Applied to files:
src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
📚 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: Custom hooks in the renderer should extract feature-specific logic from App.tsx to maintain simplicity and improve testability
Applied to files:
src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
📚 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/renderer/src/**/*.tsx : 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)
Applied to files:
src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
📚 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/renderer/src/**/*.{ts,tsx} : Use useSuspenseQuery() for data fetching in components with TanStack Router loaders for pre-fetching, not custom fetch hooks or useQuery without suspense
Applied to files:
src/renderer/src/routes/display-player/index.tsxsrc/renderer/src/routeTree.gen.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: Use module-level variables for main process state (mainWindow, playerType, currentSongPath) rather than refs, persisting user settings to database via saveUserSettings()
Applied to files:
src/renderer/src/other/appReducer.tsx
🔇 Additional comments (11)
src/types/app.d.ts (2)
203-203: LGTM!
516-519: LGTM!src/renderer/src/other/appReducer.tsx (2)
210-210: LGTM!
400-404: LGTM!src/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx (2)
65-70: LGTM!
119-133: LGTM!src/renderer/src/routes/display-player/index.tsx (1)
1-6: LGTM!src/renderer/src/routeTree.gen.ts (1)
15-15: LGTM!Also applies to: 55-59, 188-188, 217-217, 247-247, 278-278, 307-307, 336-336, 366-366, 387-393, 633-633
src/renderer/src/index.tsx (1)
74-76: LGTM!src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx (1)
1-45: LGTM!Also applies to: 49-151
src/renderer/src/assets/locales/en/en.json (1)
142-143: LGTM!
…rify defaults - In DisplayModePlayer.tsx, replace the show*InDisplayMode !== false pattern with ?? true so missing preferences explicitly default to showing controls (CodeRabbit nitpick). - In main.ts changePlayerType, fold the duplicated ype === 'display' branch into the existing else since both ran identical fullscreen setup; add a comment explaining why the if/else above is still split (intent clarity + future divergence).
|
Addressed both nitpicks on PR #497. Pushed
No other findings to address. Could you re-review and confirm? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check the latest diff for the two changed files in the PR
gh pr diff 497 --repo Sandakan/Nora -- src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
echo "=== MAIN.TS DIFF ==="
gh pr diff 497 --repo Sandakan/Nora -- src/main/main.tsLength of output: 600 🏁 Script executed: #!/bin/bash
# Fetch full diff and extract relevant sections
gh pr diff 497 --repo Sandakan/Nora | awk '
/^diff --git.*DisplayModePlayer\.tsx/ { p=1 }
/^diff --git.*main\.ts/ { p=1 }
/^diff --git/ && !/DisplayModePlayer\.tsx/ && !/main\.ts/ { p=0 }
p { print }
'Length of output: 7904
Both addressed correctly — no new issues introduced. The PR looks good from my end on these two points. (´• ω •`) 🐇✨ |
|
@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 "=== FULL CURRENT DIFF ==="
gh pr diff 497 --repo Sandakan/Nora
echo "=== ISSUE `#461` ==="
gh issue view 461 --repo Sandakan/Nora --json title,body,comments
echo "=== COMMIT HISTORY ON BRANCH ==="
gh pr view 497 --repo Sandakan/Nora --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline}'
echo "=== DisplayModePlayer full current state ==="
gh api repos/Sandakan/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx \
--jq '.content' | base64 -d
echo "=== main.ts changePlayerType current state ==="
gh api repos/Sandakan/Nora/git/refs/heads/fix/461-display-mode --jq '.object.sha' | \
xargs -I{} gh api "repos/Sandakan/Nora/contents/src/main/main.ts?ref={}" --jq '.content' | \
base64 -d | grep -n -A 50 "changePlayerType"
echo "=== appReducer current state (display-relevant parts) ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/other/appReducer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -B2 -A10 "display\|UPDATE_PLAYER_TYPE\|LOCAL_STORAGE_DEFAULT"
echo "=== index.tsx current state ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/index.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== OtherSongControlsContainer current state ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== app.d.ts display-relevant types ==="
gh api "repos/Sandakan/Nora/contents/src/types/app.d.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -B2 -A5 "display\|PlayerTypes\|Preferences"
echo "=== en.json player section ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/assets/locales/en/en.json?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get('player',{}), indent=2))"
echo "=== check other locale files for missing keys ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/assets/locales?ref=fix/461-display-mode" \
--jq '.[].name'Length of output: 26938
1. Re-review of latest commit (
|
| Severity | Location | Issue |
|---|---|---|
| Major | DisplayModePlayer.tsx ~Line 63 |
Album art + metadata wrapped in the same isMouseActive ? 'opacity-100' : 'opacity-0' div as the rest. When the mouse is idle the artwork disappears. Only the controls div (Lines 99–112) correctly uses showControls && isMouseActive. The content div should stay at opacity-100 always; only the controls row should fade. |
| Minor | DisplayModePlayer.tsx Lines 44, 79, 94 |
currentSongData.artworkPath and currentSongData.title are accessed without optional chaining. If the store is initialised before a song is loaded (cold start, empty library) these will throw. The artists access on the same object already uses ?. — apply it consistently. |
| Nit | DisplayModePlayer.tsx Line 115 |
SeekBarSlider gets opacity-0 when mouse is idle but no pointer-events-none — it remains interactive while invisible. |
| Nit | DisplayModePlayer.tsx (container) |
No cursor-none applied to the container when !isMouseActive. Standard display-mode behaviour hides the cursor on idle. |
2. Original issue fulfillment
Verdict: Partial — happy path works, but the core idle UX contradicts the spec.
Issue #461 item 4 asks for:
"a larger or centered album cover … along with prominent song and artist information, optimized for viewing from a distance or on secondary displays."
What the final code produces:
- ✅ Route, fullscreen window, large art (50vh), oversized title/artist, auto-hidden controls, close NavLink — all present.
⚠️ When the mouse goes idle the album art fades to invisible (opacity-0). A display mode viewed from a distance with no mouse movement shows a black screen. This directly contradicts "optimized for viewing from a distance."⚠️ Cold-start / empty-library path:currentSongData.artworkPathwill throw before a song is set.- ℹ️ The four new preferences (
showSongTitleInDisplayMode, etc.) have no settings UI. They work via defaults, so this doesn't block the happy path, but users can't change them yet.
The spec is 80 % met. The idle-fade-content bug is the one user-visible failure against the stated goal.
3. Deviation from issue fix
Verdict: Pass — scope is tight.
Both commits touch only the files needed for display mode. No unrelated refactors or API reshaping:
| Change | Justified? |
|---|---|
app.d.ts — 'display' added to PlayerTypes, 4 new Preferences keys |
Required |
appReducer.tsx — defaults + UPDATE_PLAYER_TYPE guard |
Required |
main.ts — changePlayerType fullscreen path |
Required |
index.tsx — onRendered subscription |
Required |
en.json — 2 new keys |
Required for UI strings |
routeTree.gen.ts — auto-generated |
Not a deviation |
OtherSongControlsContainer.tsx — entry point buttons |
Required (how users reach the mode) |
Issue #461 has 7 items; this PR implements only item 4 — that's explicitly the stated goal. Items 1–3, 5–7 are untouched.
4. Over-engineering
Verdict: Mostly proportionate; two items worth noting.
| Item | File:Line | Keep / Narrow / Remove |
|---|---|---|
handleSkipForwardClickWithParams useCallback |
DisplayModePlayer.tsx Lines 36–38 |
Keep — consistent with the pattern used elsewhere in the codebase for IPC calls with fixed params. |
imgPath useMemo — searches for onlineArtworkPaths?.picture_xl across all artists |
DisplayModePlayer.tsx Lines 40–45 |
Narrow — this logic is only used once and silently prefers an online XL artist picture over the actual song artwork, which is undocumented behaviour. Consider inlining it as a simple currentSongData?.artworkPath fallback to DefaultSongCover for now, or at minimum add a comment explaining the intent. |
4 new preferences in LOCAL_STORAGE_DEFAULT_TEMPLATE with no settings UI |
appReducer.tsx Lines 400–404 |
Keep — defaults are sane, wiring is forward-looking and costs nothing. |
TL;DR before merge:
⚠️ (Major) Fix the content-div fade: album art must stay visible on idle; only controls should hide.⚠️ (Minor) GuardcurrentSongData.artworkPath/.titlewith optional chaining.- 💡 Add
cursor-noneto the container when!isMouseActive. - 💡 Add
pointer-events-nonetoSeekBarSliderwhen it's opacity-0.
(´• ω •`) 🐇
…ryContainer for jsx-a11y/tabindex-no-positive
|
@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 "=== FULL CURRENT DIFF ==="
gh pr diff 497 --repo Sandakan/Nora
echo "=== ISSUE `#461` ==="
gh issue view 461 --repo Sandakan/Nora --json title,body,comments
echo "=== COMMIT HISTORY ON BRANCH ==="
gh pr view 497 --repo Sandakan/Nora --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline}'
echo "=== DisplayModePlayer CURRENT STATE ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== main.ts changePlayerType ==="
gh api "repos/Sandakan/Nora/contents/src/main/main.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -A 50 "changePlayerType"
echo "=== appReducer display-relevant ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/other/appReducer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -B2 -A10 "display\|UPDATE_PLAYER_TYPE\|LOCAL_STORAGE_DEFAULT"
echo "=== index.tsx ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/index.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== OtherSongControlsContainer ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== app.d.ts display-relevant ==="
gh api "repos/Sandakan/Nora/contents/src/types/app.d.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -B2 -A5 "display\|PlayerTypes\|Preferences"
echo "=== en.json player section ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/assets/locales/en/en.json?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get('player',{}), indent=2))"
echo "=== other locale files ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/assets/locales?ref=fix/461-display-mode" \
--jq '.[].name'
echo "=== useMouseActiveState hook ==="
gh api "repos/Sandakan/Nora/git/trees/fix/461-display-mode?recursive=1" \
--jq '.tree[] | select(.path | test("useMouseActiveState")) | .path' | \
xargs -I{} gh api "repos/Sandakan/Nora/contents/{}?ref=fix/461-display-mode" --jq '.content' | base64 -dLength of output: 28488
1. Re-review of latest commit (
|
| Change | Verdict | |
|---|---|---|
| ✅ | MainContainer.tsx / SecondaryContainer.tsx — tabIndex={1} → tabIndex={0} |
Correct. tabIndex=1 breaks the natural tab order for every keyboard user; tabIndex=0 is the right value. |
| ✅ | Controls div — 'opacity-0 pointer-events-none' when idle |
Correct. The previous nit is fixed here. |
Still open from the prior round:
| Severity | Location | Issue |
|---|---|---|
| Major | DisplayModePlayer.tsx Lines 63–91 |
The album art + title + artist div is still wrapped in isMouseActive ? 'opacity-100' : 'opacity-0'. Everything fades to black when the mouse is idle. Only the controls row should fade. |
| Minor | DisplayModePlayer.tsx Line 44 |
currentSongData.artworkPath inside the imgPath useMemo fallback: currentSongData is not guarded with ?. here (the artist search above uses ?. correctly, but the final ?? currentSongData.artworkPath does not). Cold-start / empty-library will throw. |
| Nit | DisplayModePlayer.tsx ~Line 143 |
SeekBarSlider still has only opacity-0 on idle, no pointer-events-none — slider remains interactive while invisible. |
| Nit | DisplayModePlayer.tsx (container) |
Still no cursor-none on the container when !isMouseActive. |
Smallest fixes:
// Line 63 — keep content always visible; only controls fade
<div className="z-10 flex flex-col items-center justify-center gap-8">
// Line 44 — guard the fallback
return selectedArtist?.onlineArtworkPaths?.picture_xl ?? currentSongData?.artworkPath;
// SeekBarSlider className — add pointer-events-none
isMouseActive ? 'opacity-100' : 'opacity-0 pointer-events-none'
// Container className — append cursor
!isMouseActive && 'cursor-none'2. Original issue fulfillment
Verdict: 80% — happy path works; idle-fade bug directly contradicts the spec.
Issue #461 item 4 specifies:
"A larger or centered album cover, along with prominent song and artist information, optimized for viewing from a distance or on secondary displays."
| Criterion | Status |
|---|---|
| Route + fullscreen window | ✅ |
| Large centered album art (50vh) | ✅ |
| Oversized title (text-7xl) / artist (text-3xl) | ✅ |
| Auto-hidden controls | ✅ |
| Close NavLink → /main-player/home | ✅ |
| Visible from a distance without mouse movement | |
| Empty library / cold start (no current song) | currentSongData.artworkPath without ?. |
| Settings UI for the 4 new preferences | ℹ️ Not present — defaults are sane, not a blocker |
The single user-visible failure maps directly to the major bug above.
3. Deviation from issue fix
Verdict: One drive-by, justified.
| Commit | File | Lines | Deviation? | Justified? |
|---|---|---|---|---|
0c07c633 |
MainContainer.tsx |
tabIndex 1→0 | Yes — #461 item 4 did not ask for this |
✅ Justified: real a11y bug, zero risk |
0c07c633 |
SecondaryContainer.tsx |
tabIndex 1→0 | Yes — same | ✅ Justified: same reasoning |
| All other files | — | — | No — tightly scoped to display mode | — |
The tabIndex commits are fine to keep; they fix a real regression but could equally be a separate PR if you prefer a clean commit graph.
4. Over-engineering
Verdict: One item worth narrowing.
| Item | File:Lines | Verdict |
|---|---|---|
imgPath useMemo silently prefers picture_xl (online XL artist image) over the song's own artwork |
DisplayModePlayer.tsx Lines 40–45 |
Narrow or document. Used only once, the fallback logic is non-obvious (prioritises an online artist portrait over the actual album art), and there is no comment explaining why. Either inline as a simple currentSongData?.artworkPath ?? DefaultSongCover for now, or add an inline comment explaining the intent. |
4 new preferences in LOCAL_STORAGE_DEFAULT_TEMPLATE with no settings UI |
appReducer.tsx Lines 400–404 |
Keep. Defaults are sane; the wiring costs nothing and prevents throwing when the keys are absent. |
handleSkipForwardClickWithParams useCallback |
DisplayModePlayer.tsx Lines 36–38 |
Keep. Consistent with the rest of the codebase. |
TL;DR before merge:
⚠️ (Major / blocker for the stated use-case) Removeopacity-0fade from the content div — only controls should hide on idle, not the artwork.⚠️ (Minor) Add?.guard tocurrentSongDatain theimgPathfallback.- 💡 Add
pointer-events-nonetoSeekBarSliderwhen opacity-0. - 💡 Add
cursor-noneto the container on idle.
(´• ω •`) 🐇✨
… into fix/461-display-mode
- Remove opacity fade from content div (album art, title, artist) — only controls should hide on idle - Add optional chaining guard to currentSongData.artworkPath fallback in imgPath useMemo - Add pointer-events-none to SeekBarSlider when idle (was interactive while invisible) - Add cursor-none to container when mouse is idle Fixes 4 issues from CodeRabbit review on PR Sandakan#497
|
@CodeRabbit please review |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/main.ts (1)
879-903: ⚡ Quick winAdd error handling around display and window APIs.
The function directly calls
screen.getAllDisplays(),screen.getPrimaryDisplay(),mainWindow.setFullScreen(), andmainWindow.setPosition()without try-catch. While Electron APIs are generally stable, defensive coding would wrap these calls in error handling, especially since this function is triggered by display change events that could occur during edge cases (rapid hotplug, driver issues).🛡️ Proposed error handling
function manageWindowOnDisplayMetricsChange() { + try { const bounds = mainWindow.getBounds(); const displays = screen.getAllDisplays(); // Use rectangle overlap so a partially visible window counts as on-screen // (otherwise a window straddling a display edge gets teleported to the primary). const isOnAnyDisplay = displays.some((display) => { const { x, y, width, height } = display.workArea; return ( bounds.x < x + width && bounds.x + bounds.width > x && bounds.y < y + height && bounds.y + bounds.height > y ); }); if (!isOnAnyDisplay) { const primaryDisplay = screen.getPrimaryDisplay(); if (mainWindow.fullScreen) mainWindow.setFullScreen(false); mainWindow.setPosition(primaryDisplay.workArea.x, primaryDisplay.workArea.y); logger.debug('Window was off-screen; moved to primary display', { previousPosition: { x: bounds.x, y: bounds.y }, newPosition: { x: primaryDisplay.workArea.x, y: primaryDisplay.workArea.y } }); } + } catch (error) { + logger.error('Failed to manage window position on display metrics change', { error }); + } }🤖 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/main/main.ts` around lines 879 - 903, Wrap the body of manageWindowOnDisplayMetricsChange in a try-catch and guard against a missing mainWindow: call screen.getAllDisplays() and screen.getPrimaryDisplay() inside the try, check mainWindow is defined before using mainWindow.getBounds(), mainWindow.setFullScreen(), and mainWindow.setPosition(), and on any thrown error use logger.error with contextual data (e.g., include bounds and display info) so failures in screen.getAllDisplays(), screen.getPrimaryDisplay(), mainWindow.setFullScreen, or mainWindow.setPosition are caught and logged rather than bubbling up.
🤖 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.
Nitpick comments:
In `@src/main/main.ts`:
- Around line 879-903: Wrap the body of manageWindowOnDisplayMetricsChange in a
try-catch and guard against a missing mainWindow: call screen.getAllDisplays()
and screen.getPrimaryDisplay() inside the try, check mainWindow is defined
before using mainWindow.getBounds(), mainWindow.setFullScreen(), and
mainWindow.setPosition(), and on any thrown error use logger.error with
contextual data (e.g., include bounds and display info) so failures in
screen.getAllDisplays(), screen.getPrimaryDisplay(), mainWindow.setFullScreen,
or mainWindow.setPosition are caught and logged rather than bubbling up.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ad1caba6-7614-48ae-b3c6-45775c008ddc
📒 Files selected for processing (4)
src/main/main.tssrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsxsrc/renderer/src/components/MainContainer.tsxsrc/renderer/src/components/SecondaryContainer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/renderer/src/components/SecondaryContainer.tsxsrc/renderer/src/components/MainContainer.tsxsrc/main/main.ts
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/SecondaryContainer.tsxsrc/renderer/src/components/MainContainer.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/SecondaryContainer.tsxsrc/renderer/src/components/MainContainer.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/main.ts
🧠 Learnings (2)
📚 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/main.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: Use module-level variables for main process state (mainWindow, playerType, currentSongPath) rather than refs, persisting user settings to database via saveUserSettings()
Applied to files:
src/main/main.ts
🔇 Additional comments (4)
src/main/main.ts (2)
848-848: LGTM!Also applies to: 867-867, 906-906
908-911:⚠️ Potential issue | 🟡 MinorAdd cleanup for Electron
screendisplay event listeners.
manageWindowPositionInMonitor()registersscreen.on('display-metrics-changed', ...)andscreen.on('display-removed', ...), but there’s no matchingscreen.removeListener/off/removeAllListenersanywhere insrc/main(includinghandleBeforeQuit). Even thoughmanageWindowFinishLoadis wired viawebContents.once('did-finish-load', ...), this still leaves best-practice cleanup gaps—especially if the main window ever gets recreated in the same process.Refactor to store the listener callbacks (named functions or variables) so they can be removed in
handleBeforeQuit.⛔ 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 **/*.{ts,tsx} : Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leakssrc/renderer/src/components/MainContainer.tsx (1)
62-62: LGTM!src/renderer/src/components/SecondaryContainer.tsx (1)
22-22: LGTM!
…Change - Wrap function body in try-catch - Guard against missing mainWindow before use - Add contextual error logging with bounds and display info - Log error message and stack trace for debugging Addresses CodeRabbit nitpick on src/main/main.ts lines 879-903
|
@CodeRabbit review once more |
|
✅ Action performedReview finished.
|
|
@CodeRabbit the review?? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Latest commits on PR ==="
gh pr view 497 --repo Sandakan/Nora --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline, committedDate: .committedDate}'
echo "=== PR status ==="
gh pr view 497 --repo Sandakan/Nora --json state,reviewDecision,statusCheckRollup --jq '{state,reviewDecision}'
echo "=== Latest DisplayModePlayer state ==="
gh api "repos/Sandakan/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -dLength of output: 2944 🏁 Script executed: #!/bin/bash
echo "=== DisplayModePlayer from fork ==="
gh api "repos/Owie6789/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== main.ts manageWindowOnDisplayMetricsChange from fork ==="
gh api "repos/Owie6789/Nora/contents/src/main/main.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -A 40 "manageWindowOnDisplayMetricsChange\|changePlayerType" | head -100Length of output: 11358
Current state of
|
| Finding | Status |
|---|---|
| Content (artwork/title/artist) fading on idle | ✅ Fixed — content div has no opacity toggle; only controls fade |
Controls: opacity-0 pointer-events-none when idle |
✅ Fixed |
cursor-none on container when idle |
✅ Fixed |
SeekBarSlider gets pointer-events-none when hidden |
✅ Fixed |
imgPath useMemo uses currentSongData?.artworkPath |
✅ Fixed |
?? true pattern for preferences |
✅ Fixed |
One remaining nit (minor, non-blocking):
// foreground Img — currentSongData itself has no ?. guard
<Img src={currentSongData.artworkPath} .../>
// and
{showSongTitle && currentSongData.title && ...}
{showArtistName && currentSongData.artists && ...}If the store always initialises currentSongData as a non-null object (which is typical in this codebase), these are safe. But adding ?. for consistency with the imgPath useMemo above would be cleaner:
src={currentSongData?.artworkPath}
// and
{showSongTitle && currentSongData?.title && ...}
{showArtistName && currentSongData?.artists && currentSongData.artists.length > 0 && ...}main.ts looks clean — the manageWindowOnDisplayMetricsChange try/catch, mainWindow guard, and else-fold for 'full'/'display' are all in place.
Overall: the PR is in good shape. The one remaining nit above is the only open item.
(´• ω •`) 🐇✨
|
@CodeRabbit can you write the code auto fix for your nitpicks and commit them upstream here from my fork which you have write access in and push it upstream through this branch? |
|
Tip For best results, initiate chat on the files or code changes.
What I can do is give you the exact copy-paste-ready changes so the fix takes under a minute to apply yourself:
Find the foreground // Foreground album art — add optional chaining
<Img
src={currentSongData?.artworkPath} // was: currentSongData.artworkPath
fallbackSrc={DefaultSongCover}
...
/>
// Song title
{showSongTitle && currentSongData?.title && (
<p className="text-7xl ...">{currentSongData.title}</p>
)}
// Artist names
{showArtistName && currentSongData?.artists && currentSongData.artists.length > 0 && (
<p className="text-3xl ...">
{currentSongData.artists.map(a => a.name).join(', ')}
</p>
)}These are all minor defensive guards — once you paste them in and commit, the PR will be ready to merge. Would you like me to open a follow-up GitHub issue for the settings UI for the four new display preferences ( (´• ω •`) 🐇 |
|
@coderabbitai rereview Merge conflicts with Conflict in
Resolution: Combined both — kept the try-catch wrapper from the PR and incorporated upstream's Please re-review the updated branch. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Latest commits on PR ==="
gh pr view 497 --repo Sandakan/Nora --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline, committedDate: .committedDate}'
echo "=== main.ts manageWindowOnDisplayMetricsChange + manageWindowPositionInMonitor + changePlayerType ==="
gh api "repos/Owie6789/Nora/contents/src/main/main.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | grep -n -A 60 "manageWindowOnDisplayMetricsChange\|manageWindowPositionInMonitor\|reposition\|leave-full-screen\|saveUserSettings" | head -150
echo "=== changePlayerType full function ==="
gh api "repos/Owie6789/Nora/contents/src/main/main.ts?ref=fix/461-display-mode" \
--jq '.content' | base64 -d | sed -n '/export async function changePlayerType/,/^export/p' | head -80
echo "=== DisplayModePlayer current state ==="
gh api "repos/Owie6789/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== full PR diff ==="
gh pr diff 497 --repo Sandakan/NoraLength of output: 40316
|
| Item | Status |
|---|---|
| try-catch wrapping the entire function body | ✅ |
mainWindow null guard at the top |
✅ |
reposition() function with saveUserSettings persistence (both mini and normal paths) |
✅ |
macOS fullscreen deferral via once('leave-full-screen', reposition) |
✅ |
| Contextual logging in catch block | ✅ |
One harmless nit: screen.getAllDisplays?.() in the catch block — screen is a module-level Electron import that's always defined; the optional chaining is unnecessary. Completely safe to leave as-is.
DisplayModePlayer.tsx — Pre-existing nit still open ⚠️ (minor)
The ?. guard I recommended last pass was not included in 5bbfe533. These three lines still access currentSongData without optional chaining:
// Line 79 — foreground Img
src={currentSongData.artworkPath} // ← should be currentSongData?.artworkPath
// Line 87 — song title
{showSongTitle && currentSongData.title && ...} // ← currentSongData?.title
// Line 93 — artist names
{showArtistName && currentSongData.artists && ...} // ← currentSongData?.artistsIf currentSongData is undefined on cold start or with an empty library, these will throw. Everything else in the component already guards with ?. (the imgPath useMemo, preferences?., artists?.find). This is the last open item.
Overall: the PR is ready to merge once the three ?. guards above are applied. Everything else — merge, main.ts wiring, controls hide-on-idle, cursor-none, pointer-events-none on SeekBarSlider, tabIndex fixes — is solid.
(´• ω •`) 🐇✨
|
@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 "=== Latest commits ==="
gh pr view 497 --repo Sandakan/Nora --json commits --jq '.commits[-3:] | .[] | {oid: .oid, messageHeadline: .messageHeadline, committedDate: .committedDate}'
echo "=== DisplayModePlayer current state ==="
gh api "repos/Owie6789/Nora/contents/src/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx?ref=fix/461-display-mode" \
--jq '.content' | base64 -d
echo "=== Issue `#461` ==="
gh issue view 461 --repo Sandakan/Nora --json title,body --jq '{title,body}'Length of output: 11063
All prior nits: ✅ Resolved
Issue
|
| Criterion from issue | Code evidence |
|---|---|
| Larger or centered album cover | h-[50vh] max-h-[70vh] w-[50vh], centred via flex column |
| Prominent song and artist information | text-7xl title, text-3xl artist — always visible regardless of mouse state |
| Optimized for viewing from a distance / secondary display | Content stays fully visible on idle; only controls (opacity-0 pointer-events-none) and SeekBar fade; cursor-none hides the mouse pointer |
| Route + fullscreen window | /display-player route, changePlayerType('display') → fullscreen in main process |
| Back navigation | NavLink to="/main-player/home" matches existing full/mini pattern |
No new issues in this commit. The PR is good to merge.
(´• ω •`) 🐇✨
Ready to MergeThis branch has passed CodeRabbit review with all flagged items resolved. Why this should be merged
Sporadic
@Sandakan review and merge when convenient. |
Summary
adds a new display mode — a dedicated full-screen visual showcase with large centered album art, oversized song metadata, and auto-hidden controls.
Root Causes
no display mode existed in nora. the closest option was fullscreen player which focuses on lyrics.
Changes by file
src/types/app.d.ts— add'display'toPlayerTypes, add 4 display mode preferencessrc/renderer/src/other/appReducer.tsx— add defaults, treat'display'like'full'in UPDATE_PLAYER_TYPE (skip IPC, go fullscreen)src/main/main.ts— add'display'case tochangePlayerType(fullscreen)src/renderer/src/index.tsx— route subscriber detects/display-playerand callschangePlayerType('display')src/renderer/src/routes/display-player/index.tsx— new route filesrc/renderer/src/components/DisplayModePlayer/DisplayModePlayer.tsx— new component with blurred artwork background, 50vh album art, text-7xl title, text-3xl artist, auto-hidden controls via useMouseActiveState, seek bar, NavLink close buttonsrc/renderer/src/routeTree.gen.ts— full route registrationsrc/renderer/src/components/SongsControlsContainer/OtherSongControlsContainer.tsx— add NavLink + context menu entry for display modesrc/renderer/src/assets/locales/en/en.json— addplayer.backToPlayer+player.openInDisplayModeSummary by CodeRabbit
New Features
Bug Fixes / Accessibility