feat(ui): themed startup splash with deferred window show (#1030)

* feat(ui): add themed startup splash with deferred window show

Show a theme-aware loading splash before the Vite bundle mounts, hide the
native window until it paints, and use per-theme logo gradient colors.

* docs: note startup splash in CHANGELOG and credits for PR #1030

* docs: attribute PR #1030 startup splash to cucadmuh
This commit is contained in:
cucadmuh
2026-06-08 12:59:07 +03:00
committed by GitHub
parent 086c7e43b4
commit 5167d8f49e
14 changed files with 712 additions and 1 deletions
+2
View File
@@ -1,4 +1,5 @@
import { installQueueUndoHotkey } from '../store/queueUndoHotkey';
import { configureStartupSplash } from './startupSplash';
import { invoke } from '@tauri-apps/api/core';
import { getWindowKind } from './windowKind';
import { migrateThemeSelection } from '../utils/themes/themeMigration';
@@ -112,6 +113,7 @@ export function runPreReactBootstrap(): void {
migrateThemeSelection();
// Paint the correct theme on the very first frame (no Mocha flash).
applyThemeAtStartup();
configureStartupSplash();
installCrossWindowThemeSync();
markDevBuildDocument();
pushUserAgentToBackend();
+53
View File
@@ -0,0 +1,53 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
STARTUP_SPLASH_ID,
configureStartupSplash,
dismissStartupSplash,
} from './startupSplash';
vi.mock('./windowKind', () => ({
getWindowKind: vi.fn(() => 'main'),
}));
vi.mock('../utils/themes/startupThemeAppearance', () => ({
applyStartupSplashThemeFromStorage: vi.fn(() => 'mocha'),
}));
vi.mock('@tauri-apps/api/webviewWindow', () => ({
getCurrentWebviewWindow: vi.fn(() => ({ show: vi.fn(() => Promise.resolve()) })),
}));
import { getWindowKind } from './windowKind';
import { applyStartupSplashThemeFromStorage } from '../utils/themes/startupThemeAppearance';
describe('startupSplash', () => {
beforeEach(() => {
document.body.innerHTML = `<div id="${STARTUP_SPLASH_ID}"></div><div id="root"></div>`;
vi.mocked(getWindowKind).mockReturnValue('main');
vi.mocked(applyStartupSplashThemeFromStorage).mockClear();
});
afterEach(() => {
document.body.innerHTML = '';
});
it('removes splash on mini player webview', () => {
vi.mocked(getWindowKind).mockReturnValue('mini');
configureStartupSplash();
expect(document.getElementById(STARTUP_SPLASH_ID)).toBeNull();
});
it('re-applies theme from storage on main window', () => {
configureStartupSplash();
expect(applyStartupSplashThemeFromStorage).toHaveBeenCalled();
});
it('fades out and removes splash', () => {
vi.useFakeTimers();
dismissStartupSplash();
expect(document.getElementById(STARTUP_SPLASH_ID)?.classList.contains('app-startup-splash--hide')).toBe(true);
vi.runAllTimers();
expect(document.getElementById(STARTUP_SPLASH_ID)).toBeNull();
vi.useRealTimers();
});
});
+43
View File
@@ -0,0 +1,43 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { applyStartupSplashThemeFromStorage } from '../utils/themes/startupThemeAppearance';
import { getWindowKind } from './windowKind';
export const STARTUP_SPLASH_ID = 'app-startup-splash';
/** Ensure the native shell is visible once the webview bundle is alive. */
export function revealStartupWindow(): void {
if (getWindowKind() === 'mini') return;
void getCurrentWebviewWindow().show().catch(() => {});
}
/** Re-apply splash colors after bootstrap theme migration/injection. */
export function configureStartupSplash(): void {
const splash = document.getElementById(STARTUP_SPLASH_ID);
if (!splash) return;
if (getWindowKind() === 'mini') {
splash.remove();
return;
}
applyStartupSplashThemeFromStorage();
revealStartupWindow();
}
/** Fade out the splash after the first React commit. */
export function dismissStartupSplash(): void {
const splash = document.getElementById(STARTUP_SPLASH_ID);
if (!splash || splash.classList.contains('app-startup-splash--hide')) return;
splash.classList.add('app-startup-splash--hide');
const remove = () => splash.remove();
splash.addEventListener('transitionend', remove, { once: true });
window.setTimeout(remove, 500);
}
/** Schedule dismiss on the frame after the first paint. */
export function scheduleStartupSplashDismiss(): void {
requestAnimationFrame(() => {
requestAnimationFrame(dismissStartupSplash);
});
}