Skip to content

fix(splash): move handoff to backend app://ready listener (closes #42)#43

Merged
InstaZDLL merged 7 commits into
mainfrom
fix/splash-handoff-event
May 18, 2026
Merged

fix(splash): move handoff to backend app://ready listener (closes #42)#43
InstaZDLL merged 7 commits into
mainfrom
fix/splash-handoff-event

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 17, 2026

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

  1. Backend (src-tauri/src/lib.rs) — setup() now installs an app.listen("app://ready", …) that calls a new reveal_main_close_splash(&AppHandle) helper (show main → focus → close splash, in that order). A tokio::time::sleep(15s) fallback fires the same helper if the event never arrives (frontend crash before render).
  2. Frontend (src/main.tsx) — revealMainWindow becomes signalReady: keep the double rAF so React commits + the compositor paints at least one frame, then emit("app://ready"). No more IPC show/close from JS.
  3. Splash window opacity (src-tauri/tauri.conf.json + public/splash.html) — drop transparent: true (already documented as risky in a memory note for AppImage on WebKitGTK 2.52+). Body background switches to opaque #121212 to match the design.

Why this works

  • rAF guarantees a tick before the next paint, not a tick after a useful paint. The previous code assumed React had committed by the second rAF, which is true on Windows (fast init) but false on Linux first launch (heavy migrations + DB pool + WebKit profile dir init).
  • The new flow makes the splash close depend on what the user actually sees, not on a guess about renderer scheduling.
  • The 15 s safety net guarantees no eternal splash even if the frontend throws before render.

Test plan

  • Windows 11 dev — no regression, splash closes cleanly on the dev build
  • bun run typecheck + cargo clippy --all-targets — both clean
  • Fedora 44 (COPR) — confirm splash closes + main window appears
  • Ubuntu 24.04 (apt) — confirm first launch works (the previous KO case)
  • Arch / CachyOS (AUR) — confirm splash closes
  • Test the 15 s safety net by adding a throw new Error() in App.tsx and verifying the splash still goes away

Follow-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

    • Écran de chargement en mode sombre avec fond #121212.
  • Améliorations

    • Fiabilisation de la transition splash → fenêtre principale : orchestration avec signal readiness, verrou anti-double exécution et filet de sécurité (timeout) pour garantir l'ouverture.
    • Frontend émet désormais un signal "prêt" après le premier rendu.
    • Qualité d'affichage améliorée pour la vignette "Now Playing" : meilleur lissage, sélection d'artwork ajustée et flou d'arrière-plan natif optimisé.

Review Change Stack

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.
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) type: fix Bug fix size: m 50-200 lines labels May 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@InstaZDLL has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 23 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23833269-a9b6-44f6-847e-050114793b8d

📥 Commits

Reviewing files that changed from the base of the PR and between f6d439d and bb13f03.

📒 Files selected for processing (10)
  • .coderabbit.yaml
  • CHANGELOG.md
  • docs/RELEASING.md
  • src-tauri/src/backup.rs
  • src-tauri/src/commands/profile_io.rs
  • src-tauri/src/lib.rs
  • src/components/common/ReadySignal.tsx
  • src/components/views/SettingsView.tsx
  • src/contexts/ThemeContext.tsx
  • src/main.tsx
📝 Walkthrough

Walkthrough

Ce PR remplace le handoff frontend synchronisé par un signal : le frontend émet app://ready via un composant ReadySignal, et le backend Tauri l’écoute pour afficher la fenêtre main et fermer le splash, avec un fallback asynchrone de 15s. Le splash reçoit un fond sombre et le rendu “Now Playing” améliore le flou et le lissage.

Changes

Splash handoff event-driven

Layer / File(s) Summary
Splash screen styling
public/splash.html, src-tauri/tauri.conf.json
Fond du splash changé de transparent à #121212 et suppression de transparent: true dans la config Tauri.
Backend handoff orchestration
src-tauri/src/lib.rs
Ajout d'un listener pour app://ready, AtomicBool pour exécution unique, fallback tokio::time::sleep(15s) et helper reveal_main_close_splash() (show/focus main, close splash, logs d'erreur, bool résultat).
Frontend ReadySignal emitter
src/main.tsx
Ajout d'un composant ReadySignal qui émet app://ready depuis un useEffect après le premier rendu; <App /> est encapsulé conditionnellement (exclu en mode mini).
Now Playing card rendering
src/lib/nowPlayingCard.ts
Activation d'un lissage image plus qualitatif, changement de priorité des sources d'artwork (artwork_path → artwork_path_2x → artwork_path_1x), et remplacement du “fake blur” par ctx.filter blur/brightness pour le backdrop avec ajustements de wash.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#26: Évolutions précédentes du handoff splash-screen et modifications connexes à src/main.tsx et tauri.conf.json.
  • InstaZDLL/WaveFlow#35: Changements antérieurs touchant le cycle de vie des fenêtres (ex. WindowEvent::Destroyed) qui interagissent avec la logique de fermeture du splash.

Poem

Un petit splash s'assombrit en secret,
Le signal part — app://ready — en effet,
Rust veille quinze secondes, sans détour,
Main s'affiche, splash s'en va pour toujours,
🎵 L'UI respire, la transition fait son tour.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit les conventions Conventional Commits (type et scope kebab-case), décrit clairement la modification principale (déplacement du handoff au backend), et référence la fermeture du ticket #42.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed La description est complète et suit la majorité des directives du template. Conventions de commits, test plan détaillé et justification technique present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/splash-handoff-event

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.

❤️ Share

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

@InstaZDLL InstaZDLL self-assigned this May 17, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1470098 and 0e5618a.

📒 Files selected for processing (4)
  • public/splash.html
  • src-tauri/src/lib.rs
  • src-tauri/tauri.conf.json
  • src/main.tsx
💤 Files with no reviewable changes (1)
  • src-tauri/tauri.conf.json

Comment thread src-tauri/src/lib.rs
InstaZDLL added 4 commits May 18, 2026 01:22
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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5618a and f6d439d.

📒 Files selected for processing (4)
  • src-tauri/src/lib.rs
  • src-tauri/tauri.conf.json
  • src/lib/nowPlayingCard.ts
  • src/main.tsx

Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs
…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.
@github-actions github-actions Bot added size: l 200-500 lines and removed size: m 50-200 lines labels May 18, 2026
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.
@github-actions github-actions Bot added scope: docs Docs, README, assets size: xl > 500 lines and removed size: l 200-500 lines labels May 18, 2026
@InstaZDLL InstaZDLL enabled auto-merge May 18, 2026 00:32
@InstaZDLL InstaZDLL disabled auto-merge May 18, 2026 00:34
@InstaZDLL InstaZDLL merged commit 1c43fb9 into main May 18, 2026
13 checks passed
@InstaZDLL InstaZDLL deleted the fix/splash-handoff-event branch May 18, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) size: xl > 500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant