mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: clamp\storage dry
This commit is contained in:
@@ -94,12 +94,15 @@ Download. Core по канону «не знает ни о ком».
|
|||||||
`findMaxColorsWithin`, `findQualityWithin`. Это доменная логика, не ввод-вывод
|
`findMaxColorsWithin`, `findQualityWithin`. Это доменная логика, не ввод-вывод
|
||||||
(у файла 19 экспортов).
|
(у файла 19 экспортов).
|
||||||
|
|
||||||
### M7. `clamp` продублирован 5 раз
|
### M7. `clamp` продублирован 5 раз — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
Идентичная локальная функция: `core/alpha.ts:167`, `core/background.ts:136`,
|
Создан `web/src/lib/core/math.ts` (`clamp`, `clampInt`, `clamp01`, `clampByte` —
|
||||||
`core/color.ts:239`, `core/convolution.ts:181`, `registry-schema.ts:408`. Плюс
|
round-семантика `clampByte` и trunc-семантика `clampInt` сохранены раздельно);
|
||||||
`clampInt` дважды: `convolution.ts:185` и `geometry.ts:288`. Прямое нарушение
|
локальные копии удалены из `core/alpha.ts`, `core/background.ts`,
|
||||||
инварианта «сначала искать».
|
`core/color.ts`, `core/convolution.ts`, `core/geometry.ts`, `core/palette.ts`,
|
||||||
|
`core/pixel-fx.ts`. Вариант с опциональными границами в `registry-schema.ts`
|
||||||
|
переведён на общий `clamp` через `?? -Infinity / ?? Infinity`. Копия в
|
||||||
|
`palette.test.ts` оставлена — тесты независимы.
|
||||||
|
|
||||||
### M8. Два параллельных hex-парсера
|
### M8. Два параллельных hex-парсера
|
||||||
|
|
||||||
@@ -108,12 +111,11 @@ Download. Core по канону «не знает ни о ком».
|
|||||||
(`affine.ts:31`, `geometry.ts:20`, `morphology.ts:123,150`), другая — второй
|
(`affine.ts:31`, `geometry.ts:20`, `morphology.ts:123,150`), другая — второй
|
||||||
(`masks.ts:25,98`, `pixel-fx.ts:195`, `registry/tools/generate.ts:36`).
|
(`masks.ts:25,98`, `pixel-fx.ts:195`, `registry/tools/generate.ts:36`).
|
||||||
|
|
||||||
### M9. Гард `localStorage` продублирован 4 раза
|
### M9. Гард `localStorage` продублирован 4 раза — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
Идентичный `typeof localStorage === "undefined" ? null : localStorage`:
|
Создан `web/src/lib/storage.ts` (`storage()` + `safeSetItem`); локальные гарды
|
||||||
`chains.svelte.ts:22-24`, `favorites.svelte.ts:8`, `i18n/locale.svelte.ts:12`,
|
удалены из `chains.svelte.ts`, `favorites.svelte.ts`, `i18n/locale.svelte.ts`,
|
||||||
`theme.svelte.ts:7-9`. Общий `storage()`-хелпер даст одну точку
|
`theme.svelte.ts`.
|
||||||
SSR-безопасности.
|
|
||||||
|
|
||||||
### M10. Логика темы дублируется между `app.html` и `theme.svelte.ts` — ОТКЛОНЕНО
|
### M10. Логика темы дублируется между `app.html` и `theme.svelte.ts` — ОТКЛОНЕНО
|
||||||
|
|
||||||
@@ -172,12 +174,11 @@ production).
|
|||||||
`clamp(var(--space-brand), 30vh, 60vh)` в `PreviewTile.svelte:96` и
|
`clamp(var(--space-brand), 30vh, 60vh)` в `PreviewTile.svelte:96` и
|
||||||
`SchemaResultTile.svelte:129`; магические `30vh/60vh` не в токенах.
|
`SchemaResultTile.svelte:129`; магические `30vh/60vh` не в токенах.
|
||||||
|
|
||||||
### L7. `localStorage.setItem` без try/catch вне app.html
|
### L7. `localStorage.setItem` без try/catch вне app.html — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
`theme.svelte.ts:29`, `chains.svelte.ts`, `favorites.svelte.ts`,
|
Все `setItem` в `theme.svelte.ts`, `chains.svelte.ts`, `favorites.svelte.ts`,
|
||||||
`locale.svelte.ts`: в privacy-режимах `setItem` бросает; только inline-скрипт
|
`locale.svelte.ts` переведены на `safeSetItem` из `web/src/lib/storage.ts`
|
||||||
`app.html:9-20` обёрнут в try/catch. SSR не ломается (typeof-гарды есть), но
|
(best-effort persistence: бросок в privacy-режиме проглатывается).
|
||||||
клик по переключателю темы может уронить обработчик.
|
|
||||||
|
|
||||||
### L8. Переросшие файлы групп реестра и core
|
### L8. Переросшие файлы групп реестра и core
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { formatStamp } from "./core/datefmt";
|
|||||||
import type { OutputMime } from "./core/io";
|
import type { OutputMime } from "./core/io";
|
||||||
import type { PixelImage } from "./core/types";
|
import type { PixelImage } from "./core/types";
|
||||||
import { t } from "./i18n/t";
|
import { t } from "./i18n/t";
|
||||||
|
import { safeSetItem, storage } from "./storage";
|
||||||
import {
|
import {
|
||||||
CHAIN_VERSION,
|
CHAIN_VERSION,
|
||||||
parseChain,
|
parseChain,
|
||||||
@@ -19,10 +20,6 @@ export interface NamedChain {
|
|||||||
|
|
||||||
const STORE_KEY = "chains:named";
|
const STORE_KEY = "chains:named";
|
||||||
|
|
||||||
function storage(): Storage | null {
|
|
||||||
return typeof localStorage === "undefined" ? null : localStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function autoChainName(date = new Date()): string {
|
export function autoChainName(date = new Date()): string {
|
||||||
return t("savedChains.autoName", {
|
return t("savedChains.autoName", {
|
||||||
stamp: formatStamp(date, "YYYY-MM-DD hh:mm"),
|
stamp: formatStamp(date, "YYYY-MM-DD hh:mm"),
|
||||||
@@ -92,7 +89,7 @@ function readAll(): NamedChain[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeAll(chains: NamedChain[]): void {
|
function writeAll(chains: NamedChain[]): void {
|
||||||
storage()?.setItem(STORE_KEY, JSON.stringify(serializeNamedChains(chains)));
|
safeSetItem(STORE_KEY, JSON.stringify(serializeNamedChains(chains)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Newest first. */
|
/** Newest first. */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createPixelImage, type PixelImage } from "./types";
|
import { createPixelImage, type PixelImage } from "./types";
|
||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
|
import { clamp } from "./math";
|
||||||
|
|
||||||
const MAX_COLOR_DISTANCE = Math.sqrt(3 * 255 * 255);
|
const MAX_COLOR_DISTANCE = Math.sqrt(3 * 255 * 255);
|
||||||
|
|
||||||
@@ -163,7 +164,3 @@ export function parseHex(hex: string): [number, number, number] {
|
|||||||
parseInt(digits.slice(4, 6), 16),
|
parseInt(digits.slice(4, 6), 16),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { parseHex } from "./alpha";
|
import { parseHex } from "./alpha";
|
||||||
import { createPixelImage, type PixelImage } from "./types";
|
import { createPixelImage, type PixelImage } from "./types";
|
||||||
|
import { clamp } from "./math";
|
||||||
|
|
||||||
export type BackgroundOptions = {
|
export type BackgroundOptions = {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -132,7 +133,3 @@ export function backgroundMaskPreview(
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
|
import { clamp } from "./math";
|
||||||
|
|
||||||
export type RgbChannel = "red" | "green" | "blue";
|
export type RgbChannel = "red" | "green" | "blue";
|
||||||
|
|
||||||
@@ -236,10 +237,6 @@ export function rgbToHex(r: number, g: number, b: number): string {
|
|||||||
return `#${byte(r)}${byte(g)}${byte(b)}`;
|
return `#${byte(r)}${byte(g)}${byte(b)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gammaCorrection(img: PixelImage, value: number): PixelImage {
|
export function gammaCorrection(img: PixelImage, value: number): PixelImage {
|
||||||
const g = clamp(value, 0.1, 5);
|
const g = clamp(value, 0.1, 5);
|
||||||
const lut = new Uint8ClampedArray(256);
|
const lut = new Uint8ClampedArray(256);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
|
import { clamp, clampInt } from "./math";
|
||||||
|
|
||||||
type Plane = Float64Array;
|
type Plane = Float64Array;
|
||||||
|
|
||||||
@@ -177,11 +178,3 @@ function boxesForGauss(sigma: number, boxes: number): number[] {
|
|||||||
}
|
}
|
||||||
return sizes;
|
return sizes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(v: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, v));
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampInt(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parseHex } from "./alpha";
|
import { parseHex } from "./alpha";
|
||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||||
|
import { clampInt } from "./math";
|
||||||
|
|
||||||
export function expandCanvas(
|
export function expandCanvas(
|
||||||
img: PixelImage,
|
img: PixelImage,
|
||||||
@@ -285,10 +286,6 @@ export function sampleBilinear(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampInt(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Anchor9 =
|
export type Anchor9 =
|
||||||
| "top-left"
|
| "top-left"
|
||||||
| "top-center"
|
| "top-center"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampInt(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp01(value: number): number {
|
||||||
|
return clamp(value, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampByte(value: number): number {
|
||||||
|
return value < 0 ? 0 : value > 255 ? 255 : Math.round(value);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
import type { PixelImage } from "./types";
|
import type { PixelImage } from "./types";
|
||||||
import { createPixelImage } from "./types";
|
import { createPixelImage } from "./types";
|
||||||
|
import { clamp01 } from "./math";
|
||||||
|
|
||||||
export type Rgb = { r: number; g: number; b: number };
|
export type Rgb = { r: number; g: number; b: number };
|
||||||
export type Hsl = { h: number; s: number; l: number };
|
export type Hsl = { h: number; s: number; l: number };
|
||||||
@@ -180,10 +181,6 @@ export function sortPalette(hexes: string[], key: SortKey): string[] {
|
|||||||
.map((s) => s.h);
|
.map((s) => s.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp01(v: number): number {
|
|
||||||
return Math.min(1, Math.max(0, v));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Horizontal equal swatch columns (strip) or a grid of ~square cells (grid). */
|
/** Horizontal equal swatch columns (strip) or a grid of ~square cells (grid). */
|
||||||
export function renderSwatches(
|
export function renderSwatches(
|
||||||
colors: string[],
|
colors: string[],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { hexToRgb } from "./palette";
|
|||||||
import type { PixelImage } from "./types";
|
import type { PixelImage } from "./types";
|
||||||
import { createPixelImage } from "./types";
|
import { createPixelImage } from "./types";
|
||||||
import { gaussianBlur } from "./convolution";
|
import { gaussianBlur } from "./convolution";
|
||||||
|
import { clampByte } from "./math";
|
||||||
|
|
||||||
/** Deterministic PRNG (mulberry32): same seed gives the same result. */
|
/** Deterministic PRNG (mulberry32): same seed gives the same result. */
|
||||||
export function mulberry32(seed: number): () => number {
|
export function mulberry32(seed: number): () => number {
|
||||||
@@ -15,10 +16,6 @@ export function mulberry32(seed: number): () => number {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampByte(v: number): number {
|
|
||||||
return v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Averages each blockSize x blockSize block into a single color. */
|
/** Averages each blockSize x blockSize block into a single color. */
|
||||||
export function pixelate(img: PixelImage, blockSize: number): PixelImage {
|
export function pixelate(img: PixelImage, blockSize: number): PixelImage {
|
||||||
const bs = Math.max(1, Math.round(blockSize));
|
const bs = Math.max(1, Math.round(blockSize));
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { SvelteSet } from "svelte/reactivity";
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
|
import { safeSetItem, storage } from "./storage";
|
||||||
|
|
||||||
const KEY = "fav-tools";
|
const KEY = "fav-tools";
|
||||||
|
|
||||||
const slugs = new SvelteSet<string>();
|
const slugs = new SvelteSet<string>();
|
||||||
|
|
||||||
function storage(): Storage | null {
|
|
||||||
return typeof localStorage === "undefined" ? null : localStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initFavorites(): void {
|
export function initFavorites(): void {
|
||||||
const raw = storage()?.getItem(KEY);
|
const raw = storage()?.getItem(KEY);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
@@ -31,7 +28,7 @@ export function toggleFav(slug: string): void {
|
|||||||
} else {
|
} else {
|
||||||
slugs.add(slug);
|
slugs.add(slug);
|
||||||
}
|
}
|
||||||
storage()?.setItem(KEY, JSON.stringify([...slugs]));
|
safeSetItem(KEY, JSON.stringify([...slugs]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Favorite pages from the list (order preserved). */
|
/** Favorite pages from the list (order preserved). */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BASE_LOCALE, isLocale, type Dict, type Locale } from "./dict";
|
import { BASE_LOCALE, isLocale, type Dict, type Locale } from "./dict";
|
||||||
import { ru } from "./ru";
|
import { ru } from "./ru";
|
||||||
import { en } from "./en";
|
import { en } from "./en";
|
||||||
|
import { safeSetItem, storage } from "../storage";
|
||||||
|
|
||||||
const STORAGE_KEY = "locale";
|
const STORAGE_KEY = "locale";
|
||||||
|
|
||||||
@@ -8,10 +9,6 @@ const DICTS: Record<Locale, Dict> = { ru, en };
|
|||||||
|
|
||||||
let locale = $state<Locale>(BASE_LOCALE);
|
let locale = $state<Locale>(BASE_LOCALE);
|
||||||
|
|
||||||
function storage(): Storage | null {
|
|
||||||
return typeof localStorage === "undefined" ? null : localStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLocale(): Locale {
|
export function getLocale(): Locale {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ export function getMergedDict(): Dict {
|
|||||||
|
|
||||||
export function setLocale(next: Locale): void {
|
export function setLocale(next: Locale): void {
|
||||||
locale = next;
|
locale = next;
|
||||||
storage()?.setItem(STORAGE_KEY, next);
|
safeSetItem(STORAGE_KEY, next);
|
||||||
syncLangAttr();
|
syncLangAttr();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import type { TextFont } from "./core/domText";
|
import type { TextFont } from "./core/domText";
|
||||||
import type { Position9 } from "./core/textdraw";
|
import type { Position9 } from "./core/textdraw";
|
||||||
|
import { clamp } from "./core/math";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional field of every spec: the field's label key in the dictionary.
|
* Optional field of every spec: the field's label key in the dictionary.
|
||||||
@@ -405,12 +406,6 @@ export function resolveLayoutGroups<P>(
|
|||||||
return named;
|
return named;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min?: number, max?: number): number {
|
|
||||||
if (min !== undefined && value < min) return min;
|
|
||||||
if (max !== undefined && value > max) return max;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Field cap: from source dimensions under maxFromSource, else static. */
|
/** Field cap: from source dimensions under maxFromSource, else static. */
|
||||||
export function effectiveMax(
|
export function effectiveMax(
|
||||||
spec: NumberSpec,
|
spec: NumberSpec,
|
||||||
@@ -444,7 +439,11 @@ export function clampSourceAwareMaxes<P>(
|
|||||||
if (spec.kind === "number" && spec.maxFromSource) {
|
if (spec.kind === "number" && spec.maxFromSource) {
|
||||||
const v = out[key as string];
|
const v = out[key as string];
|
||||||
if (typeof v !== "number" || !Number.isFinite(v)) continue;
|
if (typeof v !== "number" || !Number.isFinite(v)) continue;
|
||||||
const clamped = clamp(v, spec.min, effectiveMax(spec, source));
|
const clamped = clamp(
|
||||||
|
v,
|
||||||
|
spec.min ?? -Infinity,
|
||||||
|
effectiveMax(spec, source) ?? Infinity,
|
||||||
|
);
|
||||||
if (clamped !== v) {
|
if (clamped !== v) {
|
||||||
out[key as string] = clamped;
|
out[key as string] = clamped;
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -456,12 +455,12 @@ export function clampSourceAwareMaxes<P>(
|
|||||||
}
|
}
|
||||||
const width = clamp(
|
const width = clamp(
|
||||||
v.width,
|
v.width,
|
||||||
spec.min,
|
spec.min ?? -Infinity,
|
||||||
dimensionAxisMax(spec, "width", source),
|
dimensionAxisMax(spec, "width", source),
|
||||||
);
|
);
|
||||||
const height = clamp(
|
const height = clamp(
|
||||||
v.height,
|
v.height,
|
||||||
spec.min,
|
spec.min ?? -Infinity,
|
||||||
dimensionAxisMax(spec, "height", source),
|
dimensionAxisMax(spec, "height", source),
|
||||||
);
|
);
|
||||||
if (width !== v.width || height !== v.height) {
|
if (width !== v.width || height !== v.height) {
|
||||||
@@ -583,7 +582,7 @@ export function sanitizeSchemaParams<P>(
|
|||||||
spec.kind === "number"
|
spec.kind === "number"
|
||||||
? effectiveMax(spec, context?.source)
|
? effectiveMax(spec, context?.source)
|
||||||
: spec.max;
|
: spec.max;
|
||||||
out[key] = clamp(n, spec.min, max);
|
out[key] = clamp(n, spec.min ?? -Infinity, max ?? Infinity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "select":
|
case "select":
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function storage(): Storage | null {
|
||||||
|
return typeof localStorage === "undefined" ? null : localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setItem throws in privacy modes; persistence is best-effort.
|
||||||
|
export function safeSetItem(key: string, value: string): void {
|
||||||
|
try {
|
||||||
|
storage()?.setItem(key, value);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import { safeSetItem } from "./storage";
|
||||||
|
|
||||||
export type ThemeChoice = "light" | "dark";
|
export type ThemeChoice = "light" | "dark";
|
||||||
|
|
||||||
const STORAGE_KEY = "theme";
|
const STORAGE_KEY = "theme";
|
||||||
|
|
||||||
let theme = $state<ThemeChoice>("light");
|
let theme = $state<ThemeChoice>("light");
|
||||||
|
|
||||||
function storage(): Storage | null {
|
|
||||||
return typeof localStorage === "undefined" ? null : localStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function systemTheme(): ThemeChoice {
|
function systemTheme(): ThemeChoice {
|
||||||
if (typeof window === "undefined" || !window.matchMedia) return "light";
|
if (typeof window === "undefined" || !window.matchMedia) return "light";
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
@@ -26,7 +24,7 @@ export function getTheme(): ThemeChoice {
|
|||||||
|
|
||||||
export function setTheme(next: ThemeChoice): void {
|
export function setTheme(next: ThemeChoice): void {
|
||||||
theme = next;
|
theme = next;
|
||||||
storage()?.setItem(STORAGE_KEY, next);
|
safeSetItem(STORAGE_KEY, next);
|
||||||
apply();
|
apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user