mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 14:05:41 +00:00
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:
@@ -78,6 +78,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Startup — themed loading splash before the app bundle loads
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1030](https://github.com/Psychotoxical/psysonic/pull/1030)**
|
||||||
|
|
||||||
|
* Inline splash in `index.html` (progress bar + P logo) shows while the Vite bundle loads in dev and production — no empty or black window on launch.
|
||||||
|
* Splash colours follow the persisted theme (built-in palettes, day/night scheduler, and installed community themes); the logo uses each theme's accent gradient instead of a hardcoded white asset.
|
||||||
|
* The native window stays hidden until the splash has painted (`visible: false` + deferred `show` from Rust/JS); window-state restore no longer overrides startup visibility.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
### Dependencies — npm and Rust refresh
|
### Dependencies — npm and Rust refresh
|
||||||
|
|||||||
+82
@@ -6,9 +6,91 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Psysonic – Dein Navidrome Desktop Player" />
|
<meta name="description" content="Psysonic – Dein Navidrome Desktop Player" />
|
||||||
<title>Psysonic</title>
|
<title>Psysonic</title>
|
||||||
|
<script src="/startup-splash-preflight.js"></script>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
background: var(--startup-splash-bg, #1e1e2e);
|
||||||
|
}
|
||||||
|
#app-startup-splash {
|
||||||
|
--splash-bg: var(--startup-splash-bg, #1e1e2e);
|
||||||
|
--splash-text: var(--startup-splash-text, #cdd6f4);
|
||||||
|
--splash-muted: var(--startup-splash-muted, #a6adc8);
|
||||||
|
--splash-accent: var(--startup-splash-accent, #cba6f7);
|
||||||
|
--splash-track: var(--startup-splash-track, #313244);
|
||||||
|
--splash-logo-start: var(--startup-splash-logo-start, var(--splash-accent));
|
||||||
|
--splash-logo-end: var(--startup-splash-logo-end, var(--splash-accent));
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2147483646;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.75rem;
|
||||||
|
background: var(--splash-bg);
|
||||||
|
color: var(--splash-text);
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.28s ease;
|
||||||
|
}
|
||||||
|
#app-startup-splash.app-startup-splash--hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#app-startup-splash .app-startup-splash__logo {
|
||||||
|
height: 72px;
|
||||||
|
width: auto;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
#app-startup-splash .app-startup-splash__label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--splash-muted);
|
||||||
|
}
|
||||||
|
#app-startup-splash .app-startup-splash__bar {
|
||||||
|
width: min(240px, 72vw);
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--splash-track);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app-startup-splash .app-startup-splash__bar-fill {
|
||||||
|
width: 42%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: var(--splash-accent);
|
||||||
|
animation: app-startup-splash-indeterminate 1.15s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes app-startup-splash-indeterminate {
|
||||||
|
0% { transform: translateX(-120%); }
|
||||||
|
100% { transform: translateX(320%); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="app-startup-splash" role="status" aria-live="polite" aria-label="Loading Psysonic">
|
||||||
|
<svg class="app-startup-splash__logo" viewBox="0 0 115.549 130.30972" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="startupSplashLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="var(--splash-logo-start)" />
|
||||||
|
<stop offset="100%" stop-color="var(--splash-logo-end)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(220.53237,27.789086)">
|
||||||
|
<path fill="url(#startupSplashLogoGrad)" d="m -191.83501,87.581279 v -14.93937 l 1.01946,-0.029 c 1.8496,-0.0526 5.09881,-2.007 6.98453,-4.20123 2.13731,-2.48697 3.28384,-4.43657 4.52545,-7.69521 0.51751,-1.35819 1.078,-2.78694 1.24554,-3.175 0.16755,-0.38805 0.88173,-2.7693 1.58707,-5.29166 0.70533,-2.52236 1.41605,-4.90361 1.57937,-5.29167 0.16441,-0.39067 0.30759,11.85061 0.32081,27.42847 l 0.0239,28.134031 h -8.64306 -8.64305 z m -3.42317,-19.65031 c -0.81559,-0.16111 -1.84746,-0.48272 -2.29306,-0.71468 -1.09242,-0.5687 -2.72853,-2.16884 -2.74064,-2.68038 -0.005,-0.22765 -0.38465,-0.86265 -0.84281,-1.41111 -0.8626,-1.03264 -2.38323,-4.66133 -4.63113,-11.05137 -1.72997,-4.91772 -1.63358,-4.68451 -3.35352,-8.11389 -0.82714,-1.64924 -1.91998,-3.45186 -2.42853,-4.00582 -1.28805,-1.40307 -4.41406,-2.7715 -6.89485,-3.01827 l -2.08965,-0.20785 1.43221,-0.99035 c 1.5468,-1.06957 5.31147,-2.35399 6.9124,-2.35835 1.72563,-0.005 4.25283,0.7809 5.71247,1.77575 1.63175,1.11217 3.92377,3.83335 3.77488,4.48172 -0.0559,0.24344 0.11427,0.44261 0.37817,0.44261 0.53171,0 3.78445,6.24176 3.78445,7.26208 0,0.15195 0.30609,0.92171 0.6802,1.71057 0.37412,0.78887 1.08633,2.44854 1.5827,3.68817 1.00279,2.50434 2.57055,5.33152 2.95544,5.32962 0.85183,-0.004 3.83204,-7.97894 5.40479,-14.46266 1.9193,-7.91232 5.01161,-18.44694 6.10967,-20.81389 2.30114,-4.96024 4.60601,-7.03734 8.12223,-7.31959 1.95377,-0.15683 2.44243,-0.0601 4.01261,0.79453 2.49546,1.35819 3.31044,2.35029 5.40102,6.57479 0.93741,1.89425 3.29625,9.1126 4.36446,13.35583 0.51289,2.03729 1.21262,4.57729 1.55498,5.64444 0.34236,1.06716 0.83543,2.65466 1.09573,3.52778 0.96371,3.23267 3.75139,8.2344 5.51689,9.89856 2.09506,1.9748 4.10606,3.2977 5.85136,3.84922 0.72761,0.22993 1.32292,0.49404 1.32292,0.58692 0,0.0929 -0.71641,0.48577 -1.59202,0.87309 -2.29705,1.01609 -6.48839,1.02714 -8.75823,0.0231 -3.42674,-1.51581 -6.17101,-4.45149 -8.36088,-8.94406 -0.59782,-1.22642 -1.23412,-2.50231 -1.41401,-2.8353 -0.17988,-0.333 -0.47718,-1.20612 -0.66066,-1.94028 -0.74987,-3.00045 -6.42415,-19.25706 -6.99617,-20.04376 -0.79895,-1.09881 -0.87818,-1.08476 -1.55823,0.27628 -1.1693,2.3402 -2.07427,5.18987 -3.61302,11.37709 -3.03871,12.21839 -6.36478,22.38234 -8.0081,24.47148 -0.36655,0.466 -0.66646,0.99153 -0.66646,1.16785 0,0.86017 -2.61454,3.05174 -4.28395,3.59089 -1.94625,0.62857 -2.53141,0.65417 -4.78366,0.20926 z m 49.82815,-13.29265 c -2.77991,-0.70614 -6.29714,-6.05076 -8.15323,-12.38927 -0.30389,-1.03778 -0.47868,-1.96073 -0.38841,-2.051 0.0903,-0.0903 1.5695,-0.22877 3.28719,-0.30779 8.47079,-0.38969 9.78292,-0.63406 14.05919,-2.61837 3.78653,-1.75706 9.09259,-6.79386 10.56941,-10.03304 3.78708,-8.30644 4.33485,-14.20262 2.08448,-22.4376404 -1.15336,-4.22063002 -3.6401,-8.21361 -6.73205,-10.80969 -1.12271,-0.94265 -2.12066,-1.8146 -2.21767,-1.93765 -0.3794,-0.48123 -4.30858,-2.4333296 -6.41876,-3.1889796 -2.16778,-0.77628 -2.64336,-0.79956 -18.71666,-0.91597 l -16.49236,-0.11945 V -0.68605142 10.798429 h -0.8256 c -1.53109,0 -5.09758,2.09614 -6.79456,3.99338 -1.65639,1.85186 -4.54446,7.43871 -5.41264,10.47051 -0.25002,0.87312 -0.58222,1.98437 -0.73823,2.46944 -0.39136,1.2169 -2.0765,7.30176 -3.12634,11.28889 -0.2052,0.7793 -0.33685,-11.27627 -0.35693,-32.6846104 l -0.0318,-33.9193396 1.55319,-0.12371 c 0.85426,-0.068 12.32395,-0.10028 25.4882,-0.0716 20.69377,0.045 24.2694,0.12953 26.40444,0.62402 3.9887,0.92382 7.58472,2.04932 7.58472,2.3739 0,0.16576 0.52886,0.30139 1.17524,0.30139 2.09331,0 10.76432,4.87704 10.22435,5.75072 -0.12186,0.19718 -0.0447,0.24734 0.17328,0.11263 0.60692,-0.3751 4.21691,3.0333 6.9953,6.60467 2.06429,2.6534496 4.63504,8.4775396 5.94174,13.4611396 1.7681,6.7433 1.74625,15.8657704 -0.0549,22.9305504 -2.11084,8.27937 -4.97852,13.41407 -10.75456,19.25647 -2.59968,2.62955 -8.78375,7.02548 -9.88326,7.02548 -0.27557,0 -0.68644,0.1854 -0.91304,0.412 -0.39593,0.39593 -0.78905,0.56749 -4.31522,1.88319 -3.68968,1.37672 -10.83412,2.28545 -13.21446,1.68081 z m 7.57002,-15.26489 c 0,-0.19403 -0.07,-0.35278 -0.15557,-0.35278 -0.0856,0 -0.25368,0.15875 -0.3736,0.35278 -0.11992,0.19403 -0.0499,0.35278 0.15557,0.35278 0.20548,0 0.3736,-0.15875 0.3736,-0.35278 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div class="app-startup-splash__bar" aria-hidden="true">
|
||||||
|
<div class="app-startup-splash__bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span class="app-startup-splash__label">Loading</span>
|
||||||
|
</div>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script src="/startup-splash-reveal.js"></script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Synchronous startup splash theme (before the Vite bundle loads).
|
||||||
|
* Keep palette ids/hex in sync with `src/config/startupSplashPalettes.ts`.
|
||||||
|
*/
|
||||||
|
(function startupSplashPreflight() {
|
||||||
|
var THEME_KEY = 'psysonic_theme';
|
||||||
|
var INSTALLED_KEY = 'psysonic_installed_themes';
|
||||||
|
var DEFAULT = {
|
||||||
|
bg: '#1e1e2e',
|
||||||
|
text: '#cdd6f4',
|
||||||
|
muted: '#a6adc8',
|
||||||
|
accent: '#cba6f7',
|
||||||
|
track: '#313244',
|
||||||
|
logoStart: '#cba6f7',
|
||||||
|
logoEnd: '#89b4fa',
|
||||||
|
};
|
||||||
|
var BUILTIN = {
|
||||||
|
mocha: DEFAULT,
|
||||||
|
latte: {
|
||||||
|
bg: '#eff1f5',
|
||||||
|
text: '#4c4f69',
|
||||||
|
muted: '#6c6f85',
|
||||||
|
accent: '#8839ef',
|
||||||
|
track: '#ccd0da',
|
||||||
|
logoStart: '#8839ef',
|
||||||
|
logoEnd: '#1e66f5',
|
||||||
|
},
|
||||||
|
'kanagawa-wave': {
|
||||||
|
bg: '#1F1F28',
|
||||||
|
text: '#DCD7BA',
|
||||||
|
muted: '#727169',
|
||||||
|
accent: '#7E9CD8',
|
||||||
|
track: '#2A2A37',
|
||||||
|
logoStart: '#7E9CD8',
|
||||||
|
logoEnd: '#957FB8',
|
||||||
|
},
|
||||||
|
'stark-hud': {
|
||||||
|
bg: '#0b0f15',
|
||||||
|
text: '#e0f7fa',
|
||||||
|
muted: '#7da5aa',
|
||||||
|
accent: '#00f2ff',
|
||||||
|
track: '#141b24',
|
||||||
|
logoStart: '#00f2ff',
|
||||||
|
logoEnd: '#7df9ff',
|
||||||
|
},
|
||||||
|
'vision-dark': {
|
||||||
|
bg: '#0d0b12',
|
||||||
|
text: '#f2eef8',
|
||||||
|
muted: '#a6a2b8',
|
||||||
|
accent: '#ffd700',
|
||||||
|
track: '#16131e',
|
||||||
|
logoStart: '#ffd700',
|
||||||
|
logoEnd: '#a07af8',
|
||||||
|
},
|
||||||
|
'vision-navy': {
|
||||||
|
bg: '#0a1628',
|
||||||
|
text: '#e8eef8',
|
||||||
|
muted: '#9caac2',
|
||||||
|
accent: '#ffd700',
|
||||||
|
track: '#12213a',
|
||||||
|
logoStart: '#ffd700',
|
||||||
|
logoEnd: '#a07af8',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function readCssVar(css, name) {
|
||||||
|
var match = css.match(new RegExp(name + '\\s*:\\s*([^;]+);'));
|
||||||
|
var value = match && match[1] ? match[1].trim() : '';
|
||||||
|
return value || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveScheduledTheme(state) {
|
||||||
|
if (!state.enableThemeScheduler) return state.theme;
|
||||||
|
var now = new Date();
|
||||||
|
var nowMins = now.getHours() * 60 + now.getMinutes();
|
||||||
|
var dayParts = state.timeDayStart.split(':').map(Number);
|
||||||
|
var nightParts = state.timeNightStart.split(':').map(Number);
|
||||||
|
var dayMins = dayParts[0] * 60 + dayParts[1];
|
||||||
|
var nightMins = nightParts[0] * 60 + nightParts[1];
|
||||||
|
var isDay = dayMins < nightMins
|
||||||
|
? nowMins >= dayMins && nowMins < nightMins
|
||||||
|
: nowMins >= dayMins || nowMins < nightMins;
|
||||||
|
return isDay ? state.themeDay : state.themeNight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readThemeState() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(THEME_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
var parsed = JSON.parse(raw);
|
||||||
|
var s = parsed && parsed.state;
|
||||||
|
if (!s) return null;
|
||||||
|
return {
|
||||||
|
enableThemeScheduler: !!s.enableThemeScheduler,
|
||||||
|
theme: String(s.theme || 'mocha'),
|
||||||
|
themeDay: String(s.themeDay || 'latte'),
|
||||||
|
themeNight: String(s.themeNight || 'mocha'),
|
||||||
|
timeDayStart: String(s.timeDayStart || '07:00'),
|
||||||
|
timeNightStart: String(s.timeNightStart || '19:00'),
|
||||||
|
};
|
||||||
|
} catch (_err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInstalledThemes() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(INSTALLED_KEY);
|
||||||
|
if (!raw) return [];
|
||||||
|
var parsed = JSON.parse(raw);
|
||||||
|
var themes = parsed && parsed.state && parsed.state.themes;
|
||||||
|
return Array.isArray(themes) ? themes : [];
|
||||||
|
} catch (_err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paletteForTheme(themeId, installedThemes) {
|
||||||
|
if (BUILTIN[themeId]) return BUILTIN[themeId];
|
||||||
|
for (var i = 0; i < installedThemes.length; i += 1) {
|
||||||
|
var theme = installedThemes[i];
|
||||||
|
if (!theme || theme.id !== themeId || !theme.css) continue;
|
||||||
|
var bg = readCssVar(theme.css, '--bg-app');
|
||||||
|
var accent = readCssVar(theme.css, '--accent');
|
||||||
|
if (!bg || !accent) break;
|
||||||
|
var logoStart = readCssVar(theme.css, '--logo-color-start') || accent;
|
||||||
|
var logoEnd = readCssVar(theme.css, '--logo-color-end')
|
||||||
|
|| readCssVar(theme.css, '--accent-2')
|
||||||
|
|| accent;
|
||||||
|
return {
|
||||||
|
bg: bg,
|
||||||
|
text: readCssVar(theme.css, '--text-primary') || readCssVar(theme.css, '--ctp-text') || DEFAULT.text,
|
||||||
|
muted: readCssVar(theme.css, '--text-muted') || readCssVar(theme.css, '--ctp-subtext0') || DEFAULT.muted,
|
||||||
|
accent: accent,
|
||||||
|
track: readCssVar(theme.css, '--bg-card') || readCssVar(theme.css, '--border-subtle') || DEFAULT.track,
|
||||||
|
logoStart: logoStart,
|
||||||
|
logoEnd: logoEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPalette(themeId, palette) {
|
||||||
|
var root = document.documentElement;
|
||||||
|
root.setAttribute('data-theme', themeId);
|
||||||
|
root.style.setProperty('--startup-splash-bg', palette.bg);
|
||||||
|
root.style.setProperty('--startup-splash-text', palette.text);
|
||||||
|
root.style.setProperty('--startup-splash-muted', palette.muted);
|
||||||
|
root.style.setProperty('--startup-splash-accent', palette.accent);
|
||||||
|
root.style.setProperty('--startup-splash-track', palette.track);
|
||||||
|
root.style.setProperty('--startup-splash-logo-start', palette.logoStart);
|
||||||
|
root.style.setProperty('--startup-splash-logo-end', palette.logoEnd);
|
||||||
|
root.style.background = palette.bg;
|
||||||
|
if (document.body) document.body.style.background = palette.bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
var persisted = readThemeState();
|
||||||
|
var themeId = persisted ? resolveScheduledTheme(persisted) : 'mocha';
|
||||||
|
var palette = paletteForTheme(themeId, readInstalledThemes());
|
||||||
|
applyPalette(themeId, palette);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Show the native window after the inline startup splash has painted.
|
||||||
|
* __TAURI_INTERNALS__ may not exist yet when this script first runs.
|
||||||
|
*/
|
||||||
|
(function startupSplashReveal() {
|
||||||
|
var MAX_ATTEMPTS = 60;
|
||||||
|
|
||||||
|
function tryShowMainWindow() {
|
||||||
|
var internals = window.__TAURI_INTERNALS__;
|
||||||
|
if (!internals || typeof internals.invoke !== 'function') return false;
|
||||||
|
internals.invoke('plugin:window|show', { label: 'main' }).catch(function () {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reveal(attempt) {
|
||||||
|
if (tryShowMainWindow()) return;
|
||||||
|
if (attempt >= MAX_ATTEMPTS) return;
|
||||||
|
window.setTimeout(function () {
|
||||||
|
reveal(attempt + 1);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
reveal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -97,6 +97,10 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_window_state::Builder::default()
|
tauri_plugin_window_state::Builder::default()
|
||||||
|
.with_state_flags(
|
||||||
|
tauri_plugin_window_state::StateFlags::all()
|
||||||
|
& !tauri_plugin_window_state::StateFlags::VISIBLE,
|
||||||
|
)
|
||||||
.with_denylist(&["mini"])
|
.with_denylist(&["mini"])
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@@ -625,6 +629,24 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on_page_load(|webview, payload| {
|
||||||
|
if webview.label() != "main" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match payload.event() {
|
||||||
|
tauri::webview::PageLoadEvent::Started => {
|
||||||
|
let window = webview.window().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(48));
|
||||||
|
let _ = window.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tauri::webview::PageLoadEvent::Finished => {
|
||||||
|
let _ = webview.window().show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
theme_import::import_theme_zip,
|
theme_import::import_theme_zip,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"decorations": true,
|
"decorations": true,
|
||||||
"transparent": false,
|
"transparent": false,
|
||||||
"visible": true,
|
"visible": false,
|
||||||
|
"backgroundColor": "#1e1e2e",
|
||||||
"dragDropEnabled": false
|
"dragDropEnabled": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { installQueueUndoHotkey } from '../store/queueUndoHotkey';
|
import { installQueueUndoHotkey } from '../store/queueUndoHotkey';
|
||||||
|
import { configureStartupSplash } from './startupSplash';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { getWindowKind } from './windowKind';
|
import { getWindowKind } from './windowKind';
|
||||||
import { migrateThemeSelection } from '../utils/themes/themeMigration';
|
import { migrateThemeSelection } from '../utils/themes/themeMigration';
|
||||||
@@ -112,6 +113,7 @@ export function runPreReactBootstrap(): void {
|
|||||||
migrateThemeSelection();
|
migrateThemeSelection();
|
||||||
// Paint the correct theme on the very first frame (no Mocha flash).
|
// Paint the correct theme on the very first frame (no Mocha flash).
|
||||||
applyThemeAtStartup();
|
applyThemeAtStartup();
|
||||||
|
configureStartupSplash();
|
||||||
installCrossWindowThemeSync();
|
installCrossWindowThemeSync();
|
||||||
markDevBuildDocument();
|
markDevBuildDocument();
|
||||||
pushUserAgentToBackend();
|
pushUserAgentToBackend();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -154,6 +154,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Audio: Symphonia 0.6 migration with libopus adapter 0.3; ranged-stream start latency fix (probe seek-gate) and a probe timeout so a stalled stream no longer hangs playback start (PR #999)',
|
'Audio: Symphonia 0.6 migration with libopus adapter 0.3; ranged-stream start latency fix (probe seek-gate) and a probe timeout so a stalled stream no longer hangs playback start (PR #999)',
|
||||||
'Offline experience — unified media layout (cache/library/favorites), localPlaybackStore, library-index Offline Library, favorites auto-sync, cached album/playlist/artist pin reconcile, mixed-server offline queue, and single mediaDir setting (PR #1008)',
|
'Offline experience — unified media layout (cache/library/favorites), localPlaybackStore, library-index Offline Library, favorites auto-sync, cached album/playlist/artist pin reconcile, mixed-server offline queue, and single mediaDir setting (PR #1008)',
|
||||||
'Offline browse — local-bytes catalog when server is down, integration contract (context/policy/resolvers), disconnect nav fork, Home stale-cache feed, read-only context menus, PlayerBar rating/favorite guard (PR #1017)',
|
'Offline browse — local-bytes catalog when server is down, integration contract (context/policy/resolvers), disconnect nav fork, Home stale-cache feed, read-only context menus, PlayerBar rating/favorite guard (PR #1017)',
|
||||||
|
'Themed startup splash before Vite loads — deferred window show, per-theme logo gradient (PR #1030)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/** Splash colors aligned with bundled theme semantic tokens (`--bg-app`, etc.). */
|
||||||
|
export type StartupSplashPalette = {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
muted: string;
|
||||||
|
accent: string;
|
||||||
|
track: string;
|
||||||
|
/** Sidebar logo gradient start (`--logo-color-start` / `--accent`). */
|
||||||
|
logoStart: string;
|
||||||
|
/** Sidebar logo gradient end (`--logo-color-end` / `--accent-2`). */
|
||||||
|
logoEnd: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BUILTIN_SPLASH_PALETTES: Record<string, StartupSplashPalette> = {
|
||||||
|
mocha: {
|
||||||
|
bg: '#1e1e2e',
|
||||||
|
text: '#cdd6f4',
|
||||||
|
muted: '#a6adc8',
|
||||||
|
accent: '#cba6f7',
|
||||||
|
track: '#313244',
|
||||||
|
logoStart: '#cba6f7',
|
||||||
|
logoEnd: '#89b4fa',
|
||||||
|
},
|
||||||
|
latte: {
|
||||||
|
bg: '#eff1f5',
|
||||||
|
text: '#4c4f69',
|
||||||
|
muted: '#6c6f85',
|
||||||
|
accent: '#8839ef',
|
||||||
|
track: '#ccd0da',
|
||||||
|
logoStart: '#8839ef',
|
||||||
|
logoEnd: '#1e66f5',
|
||||||
|
},
|
||||||
|
'kanagawa-wave': {
|
||||||
|
bg: '#1F1F28',
|
||||||
|
text: '#DCD7BA',
|
||||||
|
muted: '#727169',
|
||||||
|
accent: '#7E9CD8',
|
||||||
|
track: '#2A2A37',
|
||||||
|
logoStart: '#7E9CD8',
|
||||||
|
logoEnd: '#957FB8',
|
||||||
|
},
|
||||||
|
'stark-hud': {
|
||||||
|
bg: '#0b0f15',
|
||||||
|
text: '#e0f7fa',
|
||||||
|
muted: '#7da5aa',
|
||||||
|
accent: '#00f2ff',
|
||||||
|
track: '#141b24',
|
||||||
|
logoStart: '#00f2ff',
|
||||||
|
logoEnd: '#7df9ff',
|
||||||
|
},
|
||||||
|
'vision-dark': {
|
||||||
|
bg: '#0d0b12',
|
||||||
|
text: '#f2eef8',
|
||||||
|
muted: '#a6a2b8',
|
||||||
|
accent: '#ffd700',
|
||||||
|
track: '#16131e',
|
||||||
|
logoStart: '#ffd700',
|
||||||
|
logoEnd: '#a07af8',
|
||||||
|
},
|
||||||
|
'vision-navy': {
|
||||||
|
bg: '#0a1628',
|
||||||
|
text: '#e8eef8',
|
||||||
|
muted: '#9caac2',
|
||||||
|
accent: '#ffd700',
|
||||||
|
track: '#12213a',
|
||||||
|
logoStart: '#ffd700',
|
||||||
|
logoEnd: '#a07af8',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BUILTIN_THEME_IDS = Object.keys(BUILTIN_SPLASH_PALETTES);
|
||||||
@@ -2,6 +2,7 @@ import { StrictMode } from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { runPreReactBootstrap } from './app/bootstrap';
|
import { runPreReactBootstrap } from './app/bootstrap';
|
||||||
|
import { scheduleStartupSplashDismiss } from './app/startupSplash';
|
||||||
import './i18n';
|
import './i18n';
|
||||||
import './styles/themes/index.css';
|
import './styles/themes/index.css';
|
||||||
import './styles/layout/index.css';
|
import './styles/layout/index.css';
|
||||||
@@ -15,3 +16,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
scheduleStartupSplashDismiss();
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { BUILTIN_SPLASH_PALETTES } from '../../config/startupSplashPalettes';
|
||||||
|
import {
|
||||||
|
applyStartupSplashPalette,
|
||||||
|
resolveEffectiveThemeId,
|
||||||
|
resolveScheduledThemeId,
|
||||||
|
resolveSplashPalette,
|
||||||
|
} from './startupThemeAppearance';
|
||||||
|
|
||||||
|
describe('startupThemeAppearance', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves scheduled day/night theme', () => {
|
||||||
|
const theme = resolveScheduledThemeId({
|
||||||
|
enableThemeScheduler: true,
|
||||||
|
theme: 'mocha',
|
||||||
|
themeDay: 'latte',
|
||||||
|
themeNight: 'stark-hud',
|
||||||
|
timeDayStart: '00:00',
|
||||||
|
timeNightStart: '12:00',
|
||||||
|
});
|
||||||
|
expect(['latte', 'stark-hud']).toContain(theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads effective theme from persisted store', () => {
|
||||||
|
localStorage.setItem('psysonic_theme', JSON.stringify({
|
||||||
|
state: {
|
||||||
|
theme: 'vision-dark',
|
||||||
|
enableThemeScheduler: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
expect(resolveEffectiveThemeId()).toBe('vision-dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses community theme css into splash palette', () => {
|
||||||
|
const palette = resolveSplashPalette('my-theme', [{
|
||||||
|
id: 'my-theme',
|
||||||
|
css: `[data-theme='my-theme'] { --bg-app: #112233; --accent: #aabbcc; --text-primary: #ddeeff; --text-muted: #99aabb; --bg-card: #223344; }`,
|
||||||
|
}]);
|
||||||
|
expect(palette).toMatchObject({
|
||||||
|
bg: '#112233',
|
||||||
|
accent: '#aabbcc',
|
||||||
|
text: '#ddeeff',
|
||||||
|
muted: '#99aabb',
|
||||||
|
track: '#223344',
|
||||||
|
logoStart: '#aabbcc',
|
||||||
|
logoEnd: '#aabbcc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies palette css variables on document root', () => {
|
||||||
|
applyStartupSplashPalette('kanagawa-wave', BUILTIN_SPLASH_PALETTES['kanagawa-wave']);
|
||||||
|
expect(document.documentElement.getAttribute('data-theme')).toBe('kanagawa-wave');
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--startup-splash-bg').trim()).toBe('#1F1F28');
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--startup-splash-accent').trim()).toBe('#7E9CD8');
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--startup-splash-logo-start').trim()).toBe('#7E9CD8');
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--startup-splash-logo-end').trim()).toBe('#957FB8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads custom logo gradient from community theme css', () => {
|
||||||
|
const palette = resolveSplashPalette('brand', [{
|
||||||
|
id: 'brand',
|
||||||
|
css: `[data-theme='brand'] { --bg-app: #111; --accent: #f00; --logo-color-start: #0f0; --logo-color-end: #00f; }`,
|
||||||
|
}]);
|
||||||
|
expect(palette.logoStart).toBe('#0f0');
|
||||||
|
expect(palette.logoEnd).toBe('#00f');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps public preflight palettes in sync with bundled theme map', () => {
|
||||||
|
const preflight = readFileSync(
|
||||||
|
resolve(process.cwd(), 'public/startup-splash-preflight.js'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
for (const themeId of Object.keys(BUILTIN_SPLASH_PALETTES)) {
|
||||||
|
expect(preflight).toContain(`'${themeId}'`);
|
||||||
|
expect(preflight).toContain(BUILTIN_SPLASH_PALETTES[themeId].bg);
|
||||||
|
expect(preflight).toContain(BUILTIN_SPLASH_PALETTES[themeId].logoStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
BUILTIN_SPLASH_PALETTES,
|
||||||
|
BUILTIN_THEME_IDS,
|
||||||
|
type StartupSplashPalette,
|
||||||
|
} from '../../config/startupSplashPalettes';
|
||||||
|
|
||||||
|
export type PersistedThemeState = {
|
||||||
|
enableThemeScheduler: boolean;
|
||||||
|
theme: string;
|
||||||
|
themeDay: string;
|
||||||
|
themeNight: string;
|
||||||
|
timeDayStart: string;
|
||||||
|
timeNightStart: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InstalledThemeSnapshot = {
|
||||||
|
id: string;
|
||||||
|
css: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const THEME_STORAGE_KEY = 'psysonic_theme';
|
||||||
|
const INSTALLED_THEMES_STORAGE_KEY = 'psysonic_installed_themes';
|
||||||
|
|
||||||
|
export function resolveScheduledThemeId(state: PersistedThemeState): string {
|
||||||
|
if (!state.enableThemeScheduler) return state.theme;
|
||||||
|
const now = new Date();
|
||||||
|
const nowMins = now.getHours() * 60 + now.getMinutes();
|
||||||
|
const [dh, dm] = state.timeDayStart.split(':').map(Number);
|
||||||
|
const [nh, nm] = state.timeNightStart.split(':').map(Number);
|
||||||
|
const dayMins = dh * 60 + dm;
|
||||||
|
const nightMins = nh * 60 + nm;
|
||||||
|
const isDay = dayMins < nightMins
|
||||||
|
? nowMins >= dayMins && nowMins < nightMins
|
||||||
|
: nowMins >= dayMins || nowMins < nightMins;
|
||||||
|
return isDay ? state.themeDay : state.themeNight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readPersistedThemeState(): PersistedThemeState | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(THEME_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as { state?: Record<string, unknown> };
|
||||||
|
const s = parsed.state;
|
||||||
|
if (!s) return null;
|
||||||
|
return {
|
||||||
|
enableThemeScheduler: !!s.enableThemeScheduler,
|
||||||
|
theme: String(s.theme ?? 'mocha'),
|
||||||
|
themeDay: String(s.themeDay ?? 'latte'),
|
||||||
|
themeNight: String(s.themeNight ?? 'mocha'),
|
||||||
|
timeDayStart: String(s.timeDayStart ?? '07:00'),
|
||||||
|
timeNightStart: String(s.timeNightStart ?? '19:00'),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readInstalledThemesFromStorage(): InstalledThemeSnapshot[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(INSTALLED_THEMES_STORAGE_KEY);
|
||||||
|
if (!raw) return [];
|
||||||
|
const parsed = JSON.parse(raw) as { state?: { themes?: InstalledThemeSnapshot[] } };
|
||||||
|
const themes = parsed.state?.themes;
|
||||||
|
return Array.isArray(themes) ? themes : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveEffectiveThemeId(): string {
|
||||||
|
const persisted = readPersistedThemeState();
|
||||||
|
if (!persisted) return 'mocha';
|
||||||
|
return resolveScheduledThemeId(persisted);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCssVar(css: string, name: string): string | null {
|
||||||
|
const match = css.match(new RegExp(`${name}\\s*:\\s*([^;]+);`));
|
||||||
|
const value = match?.[1]?.trim();
|
||||||
|
return value && value.length > 0 ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLogoColors(
|
||||||
|
css: string,
|
||||||
|
accent: string,
|
||||||
|
): Pick<StartupSplashPalette, 'logoStart' | 'logoEnd'> {
|
||||||
|
const logoStart = readCssVar(css, '--logo-color-start') ?? accent;
|
||||||
|
const logoEnd = readCssVar(css, '--logo-color-end')
|
||||||
|
?? readCssVar(css, '--accent-2')
|
||||||
|
?? accent;
|
||||||
|
return { logoStart, logoEnd };
|
||||||
|
}
|
||||||
|
|
||||||
|
function paletteFromCommunityCss(css: string, fallbackTrack: string): StartupSplashPalette | null {
|
||||||
|
const bg = readCssVar(css, '--bg-app');
|
||||||
|
const accent = readCssVar(css, '--accent');
|
||||||
|
if (!bg || !accent) return null;
|
||||||
|
const text = readCssVar(css, '--text-primary') ?? readCssVar(css, '--ctp-text') ?? '#cdd6f4';
|
||||||
|
const muted = readCssVar(css, '--text-muted') ?? readCssVar(css, '--ctp-subtext0') ?? text;
|
||||||
|
const track = readCssVar(css, '--bg-card') ?? readCssVar(css, '--border-subtle') ?? fallbackTrack;
|
||||||
|
return { bg, text, muted, accent, track, ...resolveLogoColors(css, accent) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSplashPalette(
|
||||||
|
themeId: string,
|
||||||
|
installedThemes: InstalledThemeSnapshot[] = readInstalledThemesFromStorage(),
|
||||||
|
): StartupSplashPalette {
|
||||||
|
const builtin = BUILTIN_SPLASH_PALETTES[themeId];
|
||||||
|
if (builtin) return builtin;
|
||||||
|
|
||||||
|
const installed = installedThemes.find(theme => theme.id === themeId);
|
||||||
|
if (installed) {
|
||||||
|
const fromCss = paletteFromCommunityCss(installed.css, BUILTIN_SPLASH_PALETTES.mocha.track);
|
||||||
|
if (fromCss) return fromCss;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BUILTIN_SPLASH_PALETTES.mocha;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStartupSplashPalette(
|
||||||
|
themeId: string,
|
||||||
|
palette: StartupSplashPalette,
|
||||||
|
): void {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttribute('data-theme', themeId);
|
||||||
|
root.style.setProperty('--startup-splash-bg', palette.bg);
|
||||||
|
root.style.setProperty('--startup-splash-text', palette.text);
|
||||||
|
root.style.setProperty('--startup-splash-muted', palette.muted);
|
||||||
|
root.style.setProperty('--startup-splash-accent', palette.accent);
|
||||||
|
root.style.setProperty('--startup-splash-track', palette.track);
|
||||||
|
root.style.setProperty('--startup-splash-logo-start', palette.logoStart);
|
||||||
|
root.style.setProperty('--startup-splash-logo-end', palette.logoEnd);
|
||||||
|
root.style.background = palette.bg;
|
||||||
|
document.body.style.background = palette.bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStartupSplashThemeFromStorage(): string {
|
||||||
|
const themeId = resolveEffectiveThemeId();
|
||||||
|
const palette = resolveSplashPalette(themeId);
|
||||||
|
applyStartupSplashPalette(themeId, palette);
|
||||||
|
return themeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBuiltinThemeId(themeId: string): boolean {
|
||||||
|
return BUILTIN_THEME_IDS.includes(themeId);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user