Files
BetterSEQTA-Plus/src/seqta/ui/Loading.ts
T

343 lines
9.0 KiB
TypeScript

import browser from "webextension-polyfill";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import loadingSpinner from "./loading-spinner.html?raw";
import loadingOverlay from "./loading-overlay.html?raw";
import { startLoadingCanvas } from "./loadingCanvas";
import {
pickLoadingVariant,
resolveLoadingTheme,
type LoadingVariant,
} from "./loadingVariants";
let stopCanvas: (() => void) | null = null;
let unbindThemeGuard: (() => void) | null = null;
let activeLoading: {
host: HTMLElement;
shadow: ShadowRoot;
variant: LoadingVariant;
} | null = null;
const THEME_GUARD_KEYS = [
"selectedTheme",
"selectedColor",
"DarkMode",
"adaptiveThemeColour",
"selectedFont",
] as const;
const LOADING_PALETTE_DARK = {
line: "rgba(255, 255, 255, 0.08)",
lineAccent: "rgba(255, 255, 255, 0.15)",
lineBlue: "rgba(96, 165, 250, 0.42)",
grid: "rgba(255, 255, 255, 0.028)",
text: "#f4f4f5",
version: "rgba(255, 255, 255, 0.35)",
} as const;
const LOADING_PALETTE_LIGHT = {
line: "rgba(24, 24, 27, 0.1)",
lineAccent: "rgba(24, 24, 27, 0.18)",
lineBlue: "rgba(37, 99, 235, 0.5)",
grid: "rgba(24, 24, 27, 0.055)",
text: "#18181b",
version: "rgba(24, 24, 27, 0.42)",
} as const;
function loadingPalette(darkMode: boolean) {
return darkMode ? LOADING_PALETTE_DARK : LOADING_PALETTE_LIGHT;
}
const loadingStyles = /* css */ `
:host {
--bk-line-color: ${LOADING_PALETTE_DARK.line};
--bk-line-accent: ${LOADING_PALETTE_DARK.lineAccent};
--bk-line-blue: ${LOADING_PALETTE_DARK.lineBlue};
--bk-grid-color: ${LOADING_PALETTE_DARK.grid};
--bk-spin-outer: 1s;
--bk-spin-inner: 3s;
--bk-spin-small: 3s;
--bk-stage-name: bkloading-stage-in;
--bk-stage-duration: 0.85s;
--bk-vignette-fill: radial-gradient(ellipse at center, transparent 32%, rgba(0, 0, 0, 0.75) 100%);
--bk-version-color: ${LOADING_PALETTE_DARK.version};
color: ${LOADING_PALETTE_DARK.text};
opacity: 1;
transition: opacity 0.85s cubic-bezier(0.4, 0, 0.2, 1);
}
:host(.closeLoading) {
opacity: 0;
pointer-events: none;
}
.bkloading__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.bkloading__vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: var(--bk-vignette-fill);
opacity: 0.88;
}
.bkloading__content {
position: absolute;
inset: 0;
display: grid;
place-items: center;
pointer-events: none;
}
.bkloading__logo-stage,
.bkloading-inline {
position: relative;
}
.bkloading__logo-stage {
width: 220px;
height: 220px;
animation-name: var(--bk-stage-name);
animation-duration: var(--bk-stage-duration);
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
animation-fill-mode: both;
}
.bkloading-inline {
width: 120px;
height: 120px;
margin: 0 auto;
}
.bkloading__logo-stage .svg,
.bkloading-inline .svg {
transform-origin: center center;
position: absolute;
top: 50%;
left: 50%;
backface-visibility: hidden;
will-change: transform;
}
.bkloading__logo-stage .logo,
.bkloading-inline .logo {
transform: translate3d(-50%, -50%, 0);
}
.bkloading__logo-stage .logo {
z-index: 2;
}
.bkloading__logo-stage .big-circle {
margin: -90px;
animation: bkloading-spin var(--bk-spin-inner) linear infinite;
}
.bkloading__logo-stage .small-circle {
margin: -67px;
animation: bkloading-spin var(--bk-spin-small) linear infinite;
}
.bkloading__logo-stage .outer-circle {
margin: -110px;
animation: bkloading-spin-reverse var(--bk-spin-outer) linear infinite;
}
.bkloading-inline .big-circle {
margin: -88px;
animation: bkloading-spin 3s linear infinite;
}
.bkloading-inline .small-circle {
margin: -66px;
animation: bkloading-spin-reverse 3s linear infinite;
}
.bkloading-inline .outer-circle {
margin: -108px;
animation: bkloading-spin 4.5s linear infinite;
}
.bkloading__version {
position: absolute;
right: 12px;
bottom: 10px;
font-size: 0.95rem;
letter-spacing: 0.02em;
color: var(--bk-version-color);
pointer-events: none;
}
@keyframes bkloading-stage-in {
from {
opacity: 0;
transform: translate3d(0, 12px, 0) scale(0.94);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1);
}
}
@keyframes bkloading-spin {
to {
transform: rotate(360deg);
}
}
@keyframes bkloading-spin-reverse {
to {
transform: rotate(-360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.bkloading__logo-stage .big-circle,
.bkloading__logo-stage .small-circle,
.bkloading__logo-stage .outer-circle,
.bkloading-inline .big-circle,
.bkloading-inline .small-circle,
.bkloading-inline .outer-circle {
animation: none !important;
}
}
`;
function overlayWithSpinner(): string {
return loadingOverlay.replace(
"<div class=\"bkloading__logo-stage\"></div>",
`<div class="bkloading__logo-stage">${loadingSpinner}</div>`,
);
}
function applyHostShell(host: HTMLElement, darkMode: boolean) {
const palette = loadingPalette(darkMode);
const shell = [
["position", "fixed"],
["inset", "0"],
["z-index", "1000000"],
["display", "block"],
["overflow", "hidden"],
["contain", "strict"],
["visibility", "visible"],
["pointer-events", "auto"],
["color", palette.text],
["--bk-line-color", palette.line],
["--bk-line-accent", palette.lineAccent],
["--bk-line-blue", palette.lineBlue],
["--bk-grid-color", palette.grid],
["--bk-version-color", palette.version],
] as const;
for (const [prop, value] of shell) {
host.style.setProperty(prop, value, "important");
}
}
function applyVariantTheme(
host: HTMLElement,
shadow: ShadowRoot,
variant: LoadingVariant,
) {
const darkMode = !!settingsState.DarkMode;
const theme = resolveLoadingTheme(variant, darkMode);
host.dataset.variant = variant.id;
host.dataset.scheme = darkMode ? "dark" : "light";
applyHostShell(host, darkMode);
host.style.setProperty("background", theme.background, "important");
host.style.setProperty("--bk-spin-outer", theme.spinOuter, "important");
host.style.setProperty("--bk-spin-inner", theme.spinInner, "important");
host.style.setProperty("--bk-stage-name", theme.stageName, "important");
host.style.setProperty("--bk-spin-small", theme.spinSmall, "important");
host.style.setProperty("--bk-stage-duration", theme.stageDuration, "important");
host.style.setProperty("--bk-vignette-fill", theme.vignetteFill, "important");
const vignette = shadow.querySelector(".bkloading__vignette") as HTMLElement | null;
if (vignette) {
vignette.style.opacity = String(theme.vignetteOpacity);
vignette.style.background = theme.vignetteFill;
}
}
function refreshActiveLoadingTheme() {
if (!activeLoading || activeLoading.host.classList.contains("closeLoading")) return;
applyVariantTheme(activeLoading.host, activeLoading.shadow, activeLoading.variant);
}
function bindLoadingThemeGuard() {
const onThemeChange = () => refreshActiveLoadingTheme();
for (const key of THEME_GUARD_KEYS) {
settingsState.register(key, onThemeChange);
}
const observer = new MutationObserver(onThemeChange);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["style", "class", "data-city-state"],
});
return () => {
for (const key of THEME_GUARD_KEYS) {
settingsState.unregister(key, onThemeChange);
}
observer.disconnect();
};
}
function startCanvasForRoot(
host: HTMLElement,
shadow: ShadowRoot,
variant: LoadingVariant,
) {
stopCanvas?.();
const canvas = shadow.querySelector(".bkloading__canvas") as HTMLCanvasElement | null;
stopCanvas = canvas ? startLoadingCanvas(canvas, host, variant) : null;
}
export function AppendLoadingSymbol(givenID: string, position: string) {
const loadingsymbol = stringToHTML(/* html */ `
<div id="${givenID}" class="bkloading-inline">
${loadingSpinner}
</div>`).firstChild;
document.querySelector(position)?.appendChild(loadingsymbol!);
}
export function stopLoadingAnimation() {
stopCanvas?.();
stopCanvas = null;
unbindThemeGuard?.();
unbindThemeGuard = null;
activeLoading = null;
}
export default function loading() {
stopLoadingAnimation();
const variant = pickLoadingVariant();
const host = document.createElement("div");
host.id = "loading";
host.className = "bkloading";
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>${loadingStyles}</style>
${overlayWithSpinner()}
<div class="bkloading__version">v${browser.runtime.getManifest().version}</div>
`;
document.documentElement.append(host);
applyVariantTheme(host, shadow, variant);
startCanvasForRoot(host, shadow, variant);
activeLoading = { host, shadow, variant };
unbindThemeGuard = bindLoadingThemeGuard();
}