Merge branch 'themes-v2' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher into themes-v2
This commit is contained in:
5 files changed
+132
-28
No files matched your search
+32
-1
@@ -162,7 +162,10 @@
|
|||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
html {
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@@ -197,3 +200,31 @@ video::-webkit-media-controls-overlay-enclosure {
|
|||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Crossfade the rendered page when switching themes via the View Transitions API. */
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: 320ms cubic-bezier(0.4, 0, 0.2, 1) both theme-crossfade-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: 320ms cubic-bezier(0.4, 0, 0.2, 1) both theme-crossfade-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-crossfade-out {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-crossfade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,34 @@
|
|||||||
|
|
||||||
const iconSet = $derived($active_custom_theme?.definition?.iconSet ?? DEFAULT_THEME_ICON_SET);
|
const iconSet = $derived($active_custom_theme?.definition?.iconSet ?? DEFAULT_THEME_ICON_SET);
|
||||||
|
|
||||||
|
let volumeOpen = $state(false);
|
||||||
|
let volumeDragging = $state(false);
|
||||||
|
let volumeControl: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
function openVolume() {
|
||||||
|
volumeOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVolume() {
|
||||||
|
if (!volumeDragging) volumeOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startVolumeDrag(event: PointerEvent) {
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
volumeDragging = true;
|
||||||
|
volumeOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endVolumeDrag(event: PointerEvent) {
|
||||||
|
if (!volumeDragging) return;
|
||||||
|
volumeDragging = false;
|
||||||
|
|
||||||
|
const element = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
if (!element || !volumeControl?.contains(element)) {
|
||||||
|
volumeOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
|
|
||||||
@@ -26,9 +54,18 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onpointerup={endVolumeDrag} onpointercancel={endVolumeDrag} />
|
||||||
|
|
||||||
<div data-tauri-drag-region class="titlebar z-[100]" class:macos-window={$platform === 'macos'}>
|
<div data-tauri-drag-region class="titlebar z-[100]" class:macos-window={$platform === 'macos'}>
|
||||||
{#if $custom_theme_audio_playing || ($active_custom_theme && $active_custom_theme.name !== 'Default')}
|
{#if $custom_theme_audio_playing || ($active_custom_theme && $active_custom_theme.name !== 'Default')}
|
||||||
<div class="relative flex items-center justify-center group">
|
<div
|
||||||
|
class="relative flex items-center justify-center group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Theme volume"
|
||||||
|
bind:this={volumeControl}
|
||||||
|
onpointerenter={openVolume}
|
||||||
|
onpointerleave={closeVolume}
|
||||||
|
>
|
||||||
<div class="relative flex items-center justify-center p-1 cursor-pointer">
|
<div class="relative flex items-center justify-center p-1 cursor-pointer">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-background rounded
|
class="absolute inset-0 bg-background rounded
|
||||||
@@ -51,8 +88,9 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-full right-0 translate-x-[7px]
|
class="absolute top-full right-0 translate-x-[7px]
|
||||||
opacity-0 invisible pointer-events-none -translate-y-2
|
{volumeOpen
|
||||||
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto group-hover:translate-y-0
|
? 'opacity-100 visible pointer-events-auto translate-y-0'
|
||||||
|
: 'opacity-0 invisible pointer-events-none -translate-y-2'}
|
||||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||||
z-50"
|
z-50"
|
||||||
>
|
>
|
||||||
@@ -68,6 +106,7 @@
|
|||||||
max="1"
|
max="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
bind:value={$custom_theme_volume}
|
bind:value={$custom_theme_volume}
|
||||||
|
onpointerdown={startVolumeDrag}
|
||||||
oninput={() => {
|
oninput={() => {
|
||||||
config.value<number>('volume').set($custom_theme_volume);
|
config.value<number>('volume').set($custom_theme_volume);
|
||||||
}}
|
}}
|
||||||
|
|||||||
+54
-20
@@ -7,6 +7,7 @@ import { convertFileSrc, invoke } from '@tauri-apps/api/core';
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import * as path from '@tauri-apps/api/path';
|
import * as path from '@tauri-apps/api/path';
|
||||||
import * as fs from '@tauri-apps/plugin-fs';
|
import * as fs from '@tauri-apps/plugin-fs';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
folder_name: string;
|
folder_name: string;
|
||||||
@@ -486,34 +487,67 @@ export const downloadTheme = async (
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volume = 0.15) => {
|
type LoadThemeOptions = {
|
||||||
// Theme media is buffered as blob URLs; the previous theme's URLs are no
|
/** Crossfade the rendered page when switching themes. */
|
||||||
// longer needed once it is replaced.
|
animate?: boolean;
|
||||||
clearThemeMediaCache();
|
};
|
||||||
active_custom_theme.set(theme);
|
|
||||||
custom_theme_audio_playing.set(false);
|
export const loadTheme = async (
|
||||||
themeContainer.innerHTML = '';
|
theme: Theme,
|
||||||
if (theme.definition) {
|
themeContainer: HTMLElement,
|
||||||
|
volume = 0.15,
|
||||||
|
options: LoadThemeOptions = {}
|
||||||
|
) => {
|
||||||
|
const applyTheme = async () => {
|
||||||
|
// Theme media is buffered as blob URLs; the previous theme's URLs are no
|
||||||
|
// longer needed once it is replaced.
|
||||||
|
clearThemeMediaCache();
|
||||||
|
active_custom_theme.set(theme);
|
||||||
|
custom_theme_audio_playing.set(false);
|
||||||
|
themeContainer.innerHTML = '';
|
||||||
|
if (theme.definition) {
|
||||||
|
setGlobalVolume(volume);
|
||||||
|
disableMediaControls();
|
||||||
|
void invoke('unlock_media_autoplay').catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (theme.scriptUrl.length > 0) {
|
||||||
|
const themeScript = await import(/* @vite-ignore */ theme.scriptUrl);
|
||||||
|
themeScript.mountTheme(themeContainer, { assets: theme.assets });
|
||||||
|
}
|
||||||
setGlobalVolume(volume);
|
setGlobalVolume(volume);
|
||||||
disableMediaControls();
|
disableMediaControls();
|
||||||
|
|
||||||
|
// Ensure all media elements mounted by v1 themes start playback automatically
|
||||||
|
themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => {
|
||||||
|
media.muted = false;
|
||||||
|
media.volume = volume;
|
||||||
|
playThemeMedia(media);
|
||||||
|
});
|
||||||
|
|
||||||
void invoke('unlock_media_autoplay').catch(() => {});
|
void invoke('unlock_media_autoplay').catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAnimate =
|
||||||
|
options.animate &&
|
||||||
|
typeof document !== 'undefined' &&
|
||||||
|
typeof document.startViewTransition === 'function';
|
||||||
|
|
||||||
|
if (!canAnimate) {
|
||||||
|
await applyTheme();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (theme.scriptUrl.length > 0) {
|
|
||||||
const themeScript = await import(/* @vite-ignore */ theme.scriptUrl);
|
|
||||||
themeScript.mountTheme(themeContainer, { assets: theme.assets });
|
|
||||||
}
|
|
||||||
setGlobalVolume(volume);
|
|
||||||
disableMediaControls();
|
|
||||||
|
|
||||||
// Ensure all media elements mounted by v1 themes start playback automatically
|
const transition = document.startViewTransition(async () => {
|
||||||
themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => {
|
await applyTheme();
|
||||||
media.muted = false;
|
// Let Svelte paint the new theme before the transition takes its snapshot.
|
||||||
media.volume = volume;
|
await tick();
|
||||||
playThemeMedia(media);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
void invoke('unlock_media_autoplay').catch(() => {});
|
// Theme activation must not wait for, or fail because of, the visual
|
||||||
|
// transition. `finished` can reject when a transition is skipped.
|
||||||
|
void transition.finished.catch(() => {});
|
||||||
|
await transition.updateCallbackDone;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTheme = async (themeToUninstall: Theme) => {
|
export const deleteTheme = async (themeToUninstall: Theme) => {
|
||||||
|
|||||||
@@ -583,6 +583,7 @@
|
|||||||
</AlertDialog.Root>
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
|
class="relative h-screen w-full overflow-hidden"
|
||||||
use:appActivity
|
use:appActivity
|
||||||
use:themeMediaSuspension
|
use:themeMediaSuspension
|
||||||
use:themeParallax={{
|
use:themeParallax={{
|
||||||
|
|||||||
@@ -526,7 +526,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTheme(theme, $custom_theme_container, $custom_theme_volume);
|
await loadTheme(theme, $custom_theme_container, $custom_theme_volume, {
|
||||||
|
animate: !$reduceAnimations,
|
||||||
|
});
|
||||||
await config.value<string>('theme').set(theme.name);
|
await config.value<string>('theme').set(theme.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4511,9 +4513,6 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute top-1.5 left-1.5 flex max-w-[calc(100%-4.5rem)] flex-wrap items-center gap-1"
|
class="absolute top-1.5 left-1.5 flex max-w-[calc(100%-4.5rem)] flex-wrap items-center gap-1"
|
||||||
>
|
>
|
||||||
{#if isActive}
|
|
||||||
<Badge class="text-[9px]">In use</Badge>
|
|
||||||
{/if}
|
|
||||||
{#if theme.name !== 'Default' && theme.folder_name !== 'default' && theme.apiVersion}
|
{#if theme.name !== 'Default' && theme.folder_name !== 'default' && theme.apiVersion}
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
Reference in new issue
Block a user