fix(splash): move handoff to backend app://ready listener (closes #42)#43
Conversation
v1.1.0's splash → main window handoff lived in the frontend
(src/main.tsx): two rAFs after ReactDOM.render(), then
`getCurrentWindow().show()` + `splash.close()` over IPC. On Linux
WebKitGTK 2.52+ (Fedora 44, recent Arch), the heavy first-launch
init (sqlx migrations + SQLite pool open + library scan) took
longer than the rAF window, so the show() either failed silently
or revealed an empty webview — splash hung forever, main window
never appeared. Ubuntu showed the same race on first launch only
(second launch cached the init, won the race).
Move the handoff to native code:
- src-tauri/src/lib.rs: setup() now installs an `app://ready`
event listener that calls a new `reveal_main_close_splash`
helper (show main → focus → close splash, same ordering as
before). A 15 s fallback timer fires the same helper if the
event never arrives (frontend crash before render).
- src/main.tsx: revealMainWindow becomes signalReady — keep the
double rAF to let React commit + the compositor paint at least
one frame, then `emit("app://ready")`. No more IPC show/close.
- src-tauri/tauri.conf.json + public/splash.html: drop
`transparent: true` on the splash window (and switch the body
background to opaque #121212). Transparency forced an alpha-
capable EGL config that some WebKitGTK builds reject, adding
a second EGL failure mode on top of the timing bug.
Tested on Windows 11 (no regression), pending Linux confirmation
on Fedora 44 + Ubuntu via CI-built bundles.
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughCe PR remplace le handoff frontend synchronisé par un signal : le frontend émet ChangesSplash handoff event-driven
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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-tauri/src/lib.rs`:
- Around line 385-403: Les deux chemins (la closure sur app.listen et la tâche
de fallback) marquent le handoff via handoff_done_for_event / handoff_done
(swap(true,...)) avant d'appeler reveal_main_close_splash, ce qui empêche tout
retry si reveal échoue; changez la logique pour n'écrire le flag qu'après un
reveal réussi: appelez reveal_main_close_splash(&handoff_handle) (et
&fallback_handle) d'abord, gérez/propaguez son éventuel Result/erreur, et
n'exécutez swap(true, Ordering::SeqCst) que si le reveal a réussi (ou utilisez
compare_exchange pour atomiquement marquer comme fait après succès); en cas
d'échec, loggez l'erreur et ne marquez pas le handoff comme terminé pour
permettre au fallback/événement de retenter.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 645807c7-0747-4699-adce-1664e6be61e3
📒 Files selected for processing (4)
public/splash.htmlsrc-tauri/src/lib.rssrc-tauri/tauri.conf.jsonsrc/main.tsx
💤 Files with no reviewable changes (1)
- src-tauri/tauri.conf.json
Previously both the `app://ready` listener and the 15s fallback timer marked the handoff as done *before* calling the reveal helper. If main.show() or splash.close() failed transiently (e.g. a brief WebKitGTK glitch on first reveal), there was no path back to a working UI — the flag stayed `true`, the listener short-circuited, the fallback was already past, and the user was stuck on an eternal splash. Make `reveal_main_close_splash` return a `bool` (true iff both ops succeeded), and have both call-sites roll the `done` flag back on failure so the other path (or a subsequent ready re-emission) can retry. Per coderabbit review on PR #43.
WebKitGTK 2.52 suspends requestAnimationFrame callbacks while a window is hidden. Since the main window is created with `visible: false` until the backend reveals it, the 2-rAF dance in signalReady() never fired, deadlocking the handoff for the full 15 s safety-net window. useEffect runs after the first React commit, which is the real guarantee we needed — the compositor paints as part of the reveal itself.
…n windows Without `transparent: true`, the WebView2 control on Windows shows its default white background until splash.html is parsed and the CSS `background: #121212` applies, producing a brief white flash. Setting the native window backgroundColor paints the OS-level window dark before WebView2 emits its first frame.
The Now Playing share PNG looked blurry / pixelated because the canvas pulled `track.artwork_path_2x` first (a 128×128 thumbnail from src-tauri/src/thumbnails.rs). On the 1080×1080 card that meant upscaling 4.5× for the 580 px cover slot and ~10× for the 1320 px backdrop — both visibly soft, the backdrop especially mushy because the old code "faked" a blur by drawing the cover huge over the canvas + a dark wash. - Prefer `track.artwork_path` (the original cover the scanner extracted from the audio file, typically 500–1500 px) and fall back to the thumbnails only when the original is missing. - Set `imageSmoothingQuality = "high"` for the residual upscale to 580 px so even small originals stay sharp instead of bilinear-soft. - Switch the backdrop from the upscale + wash hack to a real `ctx.filter = "blur(60px) brightness(0.6)"`, which both Tauri's Windows WebView2 (Chromium) and Linux/macOS WebKitGTK 2.40+ support natively. Cleaner backdrop, no quality loss on the foreground cover.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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-tauri/src/lib.rs`:
- Around line 709-728: In reveal_main_close_splash, avoid closing the splash
when the main window wasn't shown: only call splash.close() after main.show()
succeeded (or main existed and set_focus completed); e.g. track success with a
flag (or reuse ok) set to true only when app.get_webview_window("main") returns
Some and main.show() did not Err, and move or gate the splash.close() call
behind that flag so splash is not closed on main.show() failure or when main is
missing.
- Around line 401-412: Le fallback actuel dans le bloc
tauri::async_runtime::spawn (utilisant fallback_done, reveal_main_close_splash
et fallback_handle) est one-shot et perd la course avec ReadySignal; remplace la
logique sleep unique par une boucle de retry bornée (p.ex. N tentatives) qui
attend l'intervalle (15s ou un backoff), vérifie fallback_done.swap(true,
Ordering::SeqCst) avant chaque tentative, appelle
reveal_main_close_splash(&fallback_handle) et si elle échoue remet fallback_done
à false et continue la boucle jusqu'à épuisement des tentatives ; si
reveal_main_close_splash réussit ou fallback_done était déjà true, break la
boucle; ainsi le fallback retente lui-même de façon limitée au lieu d'être
consommé une seule fois.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6ba3005e-4524-4498-8e6a-d394877ced17
📒 Files selected for processing (4)
src-tauri/src/lib.rssrc-tauri/tauri.conf.jsonsrc/lib/nowPlayingCard.tssrc/main.tsx
…veal failure Two coderabbit findings on the splash handoff: 1. `reveal_main_close_splash` was still calling `splash.close()` even when the main window was missing or `main.show()` had failed. On that path the user ended up with zero visible windows. Early- return before touching the splash if the main reveal didn't actually succeed. 2. The fallback timer was one-shot: a single 15 s sleep, one reveal attempt, done. `ReadySignal` also emits `app://ready` only once at mount. If the timer narrowly won the race AND the reveal then failed, both triggers were spent and the user was stuck. Replace the one-shot sleep with a bounded retry loop: up to 10 attempts with a 250 ms backoff between failures. Permanent failure escalates to a `tracing::error!` so it shows up in diagnostics.
The inline ReadySignal component in src/main.tsx triggered react-refresh/only-export-components — Fast Refresh requires a file to export *only* components, and main.tsx has the ReactDOM.createRoot + i18n bootstrap side effects alongside. Move ReadySignal to src/components/common/ReadySignal.tsx with the same useEffect-based emit. Identical runtime behaviour, just cleaner module boundaries. main.tsx now imports it like any other component.
Summary
Hotfix for #42 — v1.1.0 splash screen never closes on Linux (Fedora COPR + Arch AUR every launch, Ubuntu first launch only). Root cause analysis: a timing race between the frontend's rAF-driven splash close and WebKitGTK 2.52's slow first-launch webview init.
Fix in three layers
setup()now installs anapp.listen("app://ready", …)that calls a newreveal_main_close_splash(&AppHandle)helper (show main → focus → close splash, in that order). Atokio::time::sleep(15s)fallback fires the same helper if the event never arrives (frontend crash before render).revealMainWindowbecomessignalReady: keep the double rAF so React commits + the compositor paints at least one frame, thenemit("app://ready"). No more IPC show/close from JS.transparent: true(already documented as risky in a memory note for AppImage on WebKitGTK 2.52+). Body background switches to opaque#121212to match the design.Why this works
Test plan
bun run typecheck+cargo clippy --all-targets— both cleanthrow new Error()inApp.tsxand verifying the splash still goes awayFollow-up
Once merged, this is a
fix:commit so release-please will roll a v1.1.1 release-please PR automatically. Merging that PR cuts the v1.1.1 tag → release.yml builds + fans out to AUR / Winget / COPR / apt-publish. The Linux regression banner on the v1.1.0 release notes can be amended at that point.Summary by CodeRabbit
Style
#121212.Améliorations