feat: use web transitions api for themes

This commit is contained in:
SethBurkart123
2025-05-23 12:30:43 +10:00
parent da3e11e208
commit 8adba647d8
4 changed files with 150 additions and 40 deletions
+7
View File
@@ -12,6 +12,13 @@
font-family: Rubik, sans-serif !important;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
pointer-events: none;
}
.hidden {
display: none;
}
@@ -14,12 +14,14 @@
let isDragging = $state(false);
let tempTheme = $state(null);
const handleThemeClick = async (theme: CustomTheme) => {
const handleThemeClick = async (theme: CustomTheme, e: MouseEvent) => {
if (isEditMode) return;
if (theme.id === themes?.selectedTheme) {
themeManager.setTransitionPoint(e.clientX, e.clientY);
await themeManager.disableTheme();
themes.selectedTheme = '';
} else {
themeManager.setTransitionPoint(e.clientX, e.clientY);
await themeManager.setTheme(theme.id);
if (!themes) return;
themes.selectedTheme = theme.id;
@@ -127,7 +129,7 @@
{#each themes.themes as theme (theme.id)}
<button
class="relative group w-full aspect-theme flex justify-center items-center rounded-xl transition ring dark:ring-white ring-zinc-300 {theme.id === themes.selectedTheme ? 'dark:ring-2 ring-4' : 'ring-0'}"
onclick={() => handleThemeClick(theme)}
onclick={(e) => handleThemeClick(theme, e)}
>
{#if isEditMode}
<div
+93 -5
View File
@@ -25,6 +25,7 @@ export class ThemeManager {
private originalPreviewColor: string | null = null;
private originalPreviewTheme: boolean | null = null;
private imageUrlCache: Map<string, string> = new Map();
private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 };
private constructor() {
console.debug("[ThemeManager] Initializing...");
@@ -65,6 +66,61 @@ export class ThemeManager {
return settingsState.selectedTheme;
}
/**
* Update the last transition point based on a click or event
*/
public setTransitionPoint(x: number, y: number): void {
this.lastTransitionPoint = { x, y };
}
/**
* Apply a view transition animation
*/
private async applyViewTransition(callback: () => void): Promise<void> {
if (
!document.startViewTransition ||
!settingsState.animations ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
// Just run the callback without animation if transitions not supported
callback();
return;
}
// Use last known transition point or fallback to center
const x = this.lastTransitionPoint.x || window.innerWidth / 2;
const y = this.lastTransitionPoint.y || window.innerHeight / 2;
const right = window.innerWidth - x;
const bottom = window.innerHeight - y;
const maxRadius = Math.hypot(
Math.max(x, right),
Math.max(y, bottom),
);
await document.startViewTransition(() => {
callback();
}).ready;
try {
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${maxRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 400,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
);
} catch (error) {
console.error("[ThemeManager] View Transition animation error:", error);
}
}
/**
* Disable the current theme without deleting it
*/
@@ -107,7 +163,7 @@ export class ThemeManager {
"[ThemeManager] Found selected theme, restoring:",
settingsState.selectedTheme,
);
await this.setTheme(settingsState.selectedTheme);
await this.setTheme(settingsState.selectedTheme, false);
}
} catch (error) {
console.error("[ThemeManager] Error during initialization:", error);
@@ -131,7 +187,7 @@ export class ThemeManager {
/**
* Set and apply a theme by ID
*/
public async setTheme(themeId: string): Promise<void> {
public async setTheme(themeId: string, applyViewTransition: boolean = true): Promise<void> {
console.debug("[ThemeManager] Setting theme:", themeId);
try {
const theme = (await localforage.getItem(themeId)) as CustomTheme;
@@ -147,17 +203,32 @@ export class ThemeManager {
settingsState.originalDarkMode = settingsState.DarkMode;
}
// Use view transition for the theme change
if (applyViewTransition) {
await this.applyViewTransition(async () => {
// Remove current theme if exists
if (this.currentTheme) {
console.debug("[ThemeManager] Removing current theme");
await this.removeTheme(this.currentTheme);
await this.removeThemeWithoutTransition(this.currentTheme);
}
// Apply new theme
await this.applyTheme(theme);
this.currentTheme = theme;
settingsState.selectedTheme = themeId;
});
} else {
// Remove current theme if exists
if (this.currentTheme) {
console.debug("[ThemeManager] Removing current theme");
await this.removeThemeWithoutTransition(this.currentTheme);
}
// Apply new theme
await this.applyTheme(theme);
this.currentTheme = theme;
settingsState.selectedTheme = themeId;
}
} catch (error) {
console.error("[ThemeManager] Error setting theme:", error);
}
@@ -213,11 +284,28 @@ export class ThemeManager {
}
/**
* Remove theme and restore original settings
* Remove theme and restore original settings with view transition
*/
private async removeTheme(
theme: CustomTheme,
clearSelectedTheme: boolean = true,
): Promise<void> {
console.debug("[ThemeManager] Removing theme with transition:", theme.name);
try {
await this.applyViewTransition(async () => {
await this.removeThemeWithoutTransition(theme, clearSelectedTheme);
});
} catch (error) {
console.error("[ThemeManager] Error removing theme with transition:", error);
}
}
/**
* Remove theme without applying view transition animation
*/
private async removeThemeWithoutTransition(
theme: CustomTheme,
clearSelectedTheme: boolean = true,
): Promise<void> {
console.debug("[ThemeManager] Removing theme:", theme.name);
try {
+23 -10
View File
@@ -72,7 +72,7 @@ export async function AddBetterSEQTAElements() {
setupSettingsButton();
}
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
const container = document.getElementById("content")!;
const div = document.createElement("div");
div.classList.add("titlebar");
@@ -265,13 +265,14 @@ function GetLightDarkModeString() {
async function addDarkLightToggle() {
const tooltipString = GetLightDarkModeString();
const svgContent = settingsState.DarkMode
? /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`
: /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const SUN_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const MOON_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const LightDarkModeButton = stringToHTML(/* html */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
<svg xmlns="http://www.w3.org/2000/svg">${svgContent}</svg>
<svg xmlns="http://www.w3.org/2000/svg">${initialSvgContent}</svg>
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
</button>
`);
@@ -281,9 +282,9 @@ async function addDarkLightToggle() {
updateAllColors();
document
.getElementById("LightDarkModeButton")!
.addEventListener("click", async () => {
const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!;
lightDarkModeButtonElement.addEventListener("click", async () => {
const darklightText = document.getElementById("darklighttooliptext");
if (
@@ -291,11 +292,19 @@ async function addDarkLightToggle() {
settingsState.selectedTheme
) {
darklightText!.innerText = "Locked by current theme";
await delay(1000);
darklightText!.innerText = GetLightDarkModeString();
return;
}
if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors();
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const svgElement = lightDarkModeButtonElement.querySelector("svg");
if (svgElement) svgElement.innerHTML = newSvgContent;
darklightText!.innerText = GetLightDarkModeString();
return;
}
@@ -303,6 +312,10 @@ async function addDarkLightToggle() {
updateAllColors();
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
const svgElement = lightDarkModeButtonElement.querySelector("svg");
if (svgElement) svgElement.innerHTML = newSvgContent;
darklightText!.innerText = GetLightDarkModeString();
});
}