feat(app): add configurable global background wallpaper
This commit is contained in:
1 parent
5fb667716f
commit
39aa663ebf
28 files changed
+1240
-1
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BACKGROUND_IMAGE_FILE_EXTENSIONS,
|
||||
BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES,
|
||||
BACKGROUND_IMAGE_SURFACE_VARS,
|
||||
backgroundImageFileExtension,
|
||||
backgroundImageStyle,
|
||||
backgroundImageSurfaceAlpha,
|
||||
defaultBackgroundImageSettings,
|
||||
normalizeBackgroundImageSettings,
|
||||
parseCssColorTriplet,
|
||||
surfaceColorWithAlpha,
|
||||
} from "@/lib/app/appBackgroundImage";
|
||||
|
||||
describe("backgroundImageFileExtension", () => {
|
||||
it("extracts supported extensions case-insensitively", () => {
|
||||
expect(backgroundImageFileExtension("wallpaper.PNG")).toBe("png");
|
||||
expect(backgroundImageFileExtension("photo.Jpeg")).toBe("jpeg");
|
||||
expect(backgroundImageFileExtension("/path/to/image.webp")).toBe("webp");
|
||||
});
|
||||
|
||||
it("returns null for unsupported or missing extensions", () => {
|
||||
expect(backgroundImageFileExtension("vector.svg")).toBeNull();
|
||||
expect(backgroundImageFileExtension("no-extension")).toBeNull();
|
||||
expect(backgroundImageFileExtension("")).toBeNull();
|
||||
});
|
||||
|
||||
it("exposes the dialog filter extension list", () => {
|
||||
expect(BACKGROUND_IMAGE_FILE_EXTENSIONS).toContain("png");
|
||||
expect(BACKGROUND_IMAGE_FILE_EXTENSIONS).toContain("gif");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeBackgroundImageSettings", () => {
|
||||
it("falls back to defaults for missing or invalid fields", () => {
|
||||
expect(normalizeBackgroundImageSettings(undefined)).toEqual(defaultBackgroundImageSettings());
|
||||
expect(normalizeBackgroundImageSettings(null)).toEqual(defaultBackgroundImageSettings());
|
||||
expect(normalizeBackgroundImageSettings({})).toEqual(defaultBackgroundImageSettings());
|
||||
});
|
||||
|
||||
it("keeps valid values and clamps out-of-range numbers", () => {
|
||||
const settings = normalizeBackgroundImageSettings({
|
||||
filePath: "/data/background-image.png",
|
||||
fileName: "wall.png",
|
||||
opacity: 0.75,
|
||||
blur: 8,
|
||||
displayMode: "tile",
|
||||
});
|
||||
expect(settings).toEqual({ filePath: "/data/background-image.png", fileName: "wall.png", opacity: 0.75, blur: 8, displayMode: "tile" });
|
||||
|
||||
const clamped = normalizeBackgroundImageSettings({ opacity: 5, blur: -3 });
|
||||
expect(clamped.opacity).toBe(1);
|
||||
expect(clamped.blur).toBe(0);
|
||||
|
||||
const belowMin = normalizeBackgroundImageSettings({ opacity: 0.001, blur: 99 });
|
||||
expect(belowMin.opacity).toBe(0.05);
|
||||
expect(belowMin.blur).toBe(20);
|
||||
});
|
||||
|
||||
it("falls back to the default display mode for unknown values", () => {
|
||||
expect(normalizeBackgroundImageSettings({ displayMode: "diagonal" }).displayMode).toBe("fill");
|
||||
expect(normalizeBackgroundImageSettings({ displayMode: 42 }).displayMode).toBe("fill");
|
||||
expect(normalizeBackgroundImageSettings({ displayMode: "tile" }).displayMode).toBe("tile");
|
||||
});
|
||||
|
||||
it("treats blank or non-string paths as unset", () => {
|
||||
expect(normalizeBackgroundImageSettings({ filePath: " " }).filePath).toBeNull();
|
||||
expect(normalizeBackgroundImageSettings({ filePath: 42 }).filePath).toBeNull();
|
||||
expect(normalizeBackgroundImageSettings({ fileName: "" }).fileName).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("backgroundImageStyle", () => {
|
||||
const url = "blob:http://localhost/abc";
|
||||
|
||||
it("emits nothing without an object URL", () => {
|
||||
expect(backgroundImageStyle({ displayMode: "fill", blur: 0 }, null)).toEqual({});
|
||||
});
|
||||
|
||||
it("paints fill mode as cover by default", () => {
|
||||
expect(backgroundImageStyle({ displayMode: "fill", blur: 0 }, url)).toEqual({
|
||||
backgroundImage: `url("${url}")`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
});
|
||||
expect(backgroundImageStyle({ displayMode: "nonsense" as never, blur: 0 }, url).backgroundSize).toBe("cover");
|
||||
});
|
||||
|
||||
it("maps the other display modes to their background properties", () => {
|
||||
expect(backgroundImageStyle({ displayMode: "fit", blur: 0 }, url)).toMatchObject({ backgroundSize: "contain", backgroundRepeat: "no-repeat" });
|
||||
expect(backgroundImageStyle({ displayMode: "stretch", blur: 0 }, url)).toMatchObject({ backgroundSize: "100% 100%" });
|
||||
expect(backgroundImageStyle({ displayMode: "center", blur: 0 }, url)).not.toHaveProperty("backgroundSize");
|
||||
expect(backgroundImageStyle({ displayMode: "tile", blur: 0 }, url)).toMatchObject({ backgroundRepeat: "repeat" });
|
||||
});
|
||||
|
||||
it("adds blur and a slight scale to hide transparent blur edges", () => {
|
||||
expect(backgroundImageStyle({ displayMode: "fill", blur: 12 }, url)).toMatchObject({
|
||||
filter: "blur(12px)",
|
||||
transform: "scale(1.06)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("backgroundImageSurfaceAlpha", () => {
|
||||
it("maps the surface-opacity setting 1:1 and clamps", () => {
|
||||
expect(backgroundImageSurfaceAlpha({ opacity: 0.8 })).toBe(0.8);
|
||||
expect(backgroundImageSurfaceAlpha({ opacity: 3 })).toBe(1);
|
||||
expect(backgroundImageSurfaceAlpha({ opacity: -1 })).toBe(0.05);
|
||||
});
|
||||
|
||||
it("exposes the root surface variables that go translucent", () => {
|
||||
expect(BACKGROUND_IMAGE_SURFACE_VARS).toContain("--background");
|
||||
expect(BACKGROUND_IMAGE_SURFACE_VARS).toContain("--sidebar");
|
||||
expect(BACKGROUND_IMAGE_SURFACE_VARS).not.toContain("--card");
|
||||
expect(BACKGROUND_IMAGE_SURFACE_VARS).not.toContain("--popover");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCssColorTriplet", () => {
|
||||
it("parses rgb and hex forms, dropping any alpha", () => {
|
||||
expect(parseCssColorTriplet("rgb(17 18 20)")).toEqual([17, 18, 20]);
|
||||
expect(parseCssColorTriplet("rgb(17, 18, 20)")).toEqual([17, 18, 20]);
|
||||
expect(parseCssColorTriplet("rgb(17 18 20 / 0.8)")).toEqual([17, 18, 20]);
|
||||
expect(parseCssColorTriplet("#131416")).toEqual([19, 20, 22]);
|
||||
expect(parseCssColorTriplet("#fff")).toEqual([255, 255, 255]);
|
||||
});
|
||||
|
||||
it("returns null for unparsable input", () => {
|
||||
expect(parseCssColorTriplet("")).toBeNull();
|
||||
expect(parseCssColorTriplet("red")).toBeNull();
|
||||
expect(parseCssColorTriplet("#12345")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("surfaceColorWithAlpha", () => {
|
||||
it("re-emits a color with the requested alpha", () => {
|
||||
expect(surfaceColorWithAlpha("rgb(19 20 22)", 0.8)).toBe("rgb(19 20 22 / 0.8)");
|
||||
expect(surfaceColorWithAlpha("#131416", 0.5)).toBe("rgb(19 20 22 / 0.5)");
|
||||
expect(surfaceColorWithAlpha("nope", 0.5)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps wide-gamut and hsl notations instead of converting to rgb", () => {
|
||||
expect(surfaceColorWithAlpha("oklch(0.19 0.004 285)", 0.43)).toBe("oklch(0.19 0.004 285 / 0.43)");
|
||||
expect(surfaceColorWithAlpha("oklch(0.19 0.004 285 / 0.9)", 0.2)).toBe("oklch(0.19 0.004 285 / 0.2)");
|
||||
expect(surfaceColorWithAlpha("hsl(240 10% 3.9%)", 0.6)).toBe("hsl(240 10% 3.9% / 0.6)");
|
||||
expect(surfaceColorWithAlpha("hsl(240, 10%, 3.9%)", 0.6)).toBe("hsl(240, 10%, 3.9%, 0.6)");
|
||||
expect(surfaceColorWithAlpha("240 10% 3.9%", 0.6)).toBe("hsl(240 10% 3.9% / 0.6)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES", () => {
|
||||
it("is 20 MB", () => {
|
||||
expect(BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES).toBe(20 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
export const BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
export const BACKGROUND_IMAGE_MIN_OPACITY = 0.05;
|
||||
export const BACKGROUND_IMAGE_MAX_OPACITY = 1;
|
||||
// Surface opacity: how opaque the app surfaces render over the wallpaper.
|
||||
// 1 = solid surfaces (no image visible), 0.05 = strongest wallpaper.
|
||||
export const BACKGROUND_IMAGE_DEFAULT_OPACITY = 0.8;
|
||||
export const BACKGROUND_IMAGE_MAX_BLUR_PX = 20;
|
||||
export const BACKGROUND_IMAGE_DEFAULT_BLUR_PX = 0;
|
||||
|
||||
export const BACKGROUND_IMAGE_FILE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "bmp", "gif"] as const;
|
||||
|
||||
/** Classic desktop-wallpaper presets; default "fill" crops to cover the window. */
|
||||
export const BACKGROUND_IMAGE_DISPLAY_MODES = ["fill", "fit", "stretch", "center", "tile"] as const;
|
||||
export type BackgroundImageDisplayMode = (typeof BACKGROUND_IMAGE_DISPLAY_MODES)[number];
|
||||
export const BACKGROUND_IMAGE_DEFAULT_DISPLAY_MODE: BackgroundImageDisplayMode = "fill";
|
||||
|
||||
export type BackgroundImageSettings = {
|
||||
filePath: string | null;
|
||||
fileName: string | null;
|
||||
opacity: number;
|
||||
blur: number;
|
||||
displayMode: BackgroundImageDisplayMode;
|
||||
};
|
||||
|
||||
export function defaultBackgroundImageSettings(): BackgroundImageSettings {
|
||||
return {
|
||||
filePath: null,
|
||||
fileName: null,
|
||||
opacity: BACKGROUND_IMAGE_DEFAULT_OPACITY,
|
||||
blur: BACKGROUND_IMAGE_DEFAULT_BLUR_PX,
|
||||
displayMode: BACKGROUND_IMAGE_DEFAULT_DISPLAY_MODE,
|
||||
};
|
||||
}
|
||||
|
||||
function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
||||
}
|
||||
|
||||
function normalizeOptionalText(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export function normalizeBackgroundImageDisplayMode(value: unknown): BackgroundImageDisplayMode {
|
||||
return (BACKGROUND_IMAGE_DISPLAY_MODES as readonly string[]).includes(value as string) ? (value as BackgroundImageDisplayMode) : BACKGROUND_IMAGE_DEFAULT_DISPLAY_MODE;
|
||||
}
|
||||
|
||||
export function normalizeBackgroundImageSettings(value: unknown): BackgroundImageSettings {
|
||||
const source = (value && typeof value === "object" ? value : {}) as Record<string, unknown>;
|
||||
return {
|
||||
filePath: normalizeOptionalText(source.filePath),
|
||||
fileName: normalizeOptionalText(source.fileName),
|
||||
opacity: clampNumber(source.opacity, BACKGROUND_IMAGE_MIN_OPACITY, BACKGROUND_IMAGE_MAX_OPACITY, BACKGROUND_IMAGE_DEFAULT_OPACITY),
|
||||
blur: clampNumber(source.blur, 0, BACKGROUND_IMAGE_MAX_BLUR_PX, BACKGROUND_IMAGE_DEFAULT_BLUR_PX),
|
||||
displayMode: normalizeBackgroundImageDisplayMode(source.displayMode),
|
||||
};
|
||||
}
|
||||
|
||||
/** Extension (lowercase, no dot) for dialog filters and stored-copy naming; null when the name has none. */
|
||||
export function backgroundImageFileExtension(fileName: string): string | null {
|
||||
const match = /\.([a-z0-9]+)$/i.exec(fileName.trim());
|
||||
if (!match) return null;
|
||||
const extension = match[1].toLowerCase();
|
||||
return (BACKGROUND_IMAGE_FILE_EXTENSIONS as readonly string[]).includes(extension) ? extension : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paint style for the wallpaper layer. Modes follow the classic desktop
|
||||
* wallpaper presets: fill/fit/stretch scale the image, center paints it at
|
||||
* its natural size, tile repeats it. Blur always scales the layer slightly
|
||||
* so blurred edges never reveal transparent margins.
|
||||
*/
|
||||
export function backgroundImageStyle(settings: Pick<BackgroundImageSettings, "displayMode" | "blur">, objectUrl: string | null): Record<string, string> {
|
||||
if (!objectUrl) return {};
|
||||
const style: Record<string, string> = { backgroundImage: `url("${objectUrl}")` };
|
||||
switch (settings.displayMode) {
|
||||
case "fit":
|
||||
style.backgroundSize = "contain";
|
||||
style.backgroundPosition = "center";
|
||||
style.backgroundRepeat = "no-repeat";
|
||||
break;
|
||||
case "stretch":
|
||||
style.backgroundSize = "100% 100%";
|
||||
style.backgroundRepeat = "no-repeat";
|
||||
break;
|
||||
case "center":
|
||||
style.backgroundPosition = "center";
|
||||
style.backgroundRepeat = "no-repeat";
|
||||
break;
|
||||
case "tile":
|
||||
style.backgroundRepeat = "repeat";
|
||||
break;
|
||||
case "fill":
|
||||
default:
|
||||
style.backgroundSize = "cover";
|
||||
style.backgroundPosition = "center";
|
||||
style.backgroundRepeat = "no-repeat";
|
||||
break;
|
||||
}
|
||||
if (settings.blur > 0) {
|
||||
style.filter = `blur(${settings.blur}px)`;
|
||||
style.transform = "scale(1.06)";
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root surface variables that become translucent while a wallpaper is active.
|
||||
* Card/popover/dialog surfaces stay opaque on purpose: floating panels keep
|
||||
* their text readable regardless of what the wallpaper shows behind them.
|
||||
*/
|
||||
export const BACKGROUND_IMAGE_SURFACE_VARS = ["--background", "--sidebar", "--muted", "--secondary", "--accent", "--dbx-chrome", "--dbx-chrome-muted", "--dbx-content", "--dbx-editor-toolbar", "--dbx-gutter", "--dbx-sidebar-header"] as const;
|
||||
|
||||
/** Alpha for the surface variables derived from the surface-opacity setting. */
|
||||
export function backgroundImageSurfaceAlpha(settings: Pick<BackgroundImageSettings, "opacity">): number {
|
||||
return clampNumber(settings.opacity, BACKGROUND_IMAGE_MIN_OPACITY, BACKGROUND_IMAGE_MAX_OPACITY, BACKGROUND_IMAGE_DEFAULT_OPACITY);
|
||||
}
|
||||
|
||||
/** Parses "#rgb", "#rrggbb", "rgb(r g b)", "rgb(r, g, b)" and "rgb(r g b / a)" into an rgb triplet; null when unparsable. */
|
||||
export function parseCssColorTriplet(value: string): [number, number, number] | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const rgbMatch = /^rgba?\((\d+)\s*,?\s+(\d+)\s*,?\s+(\d+)(?:\s*\/\s*[\d.]+)?\)$/i.exec(trimmed);
|
||||
if (rgbMatch) return [Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])];
|
||||
const hex = trimmed.startsWith("#") ? trimmed.slice(1) : null;
|
||||
if (hex && (hex.length === 3 || hex.length === 6)) {
|
||||
const expand =
|
||||
hex.length === 3
|
||||
? hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("")
|
||||
: hex;
|
||||
return [parseInt(expand.slice(0, 2), 16), parseInt(expand.slice(2, 4), 16), parseInt(expand.slice(4, 6), 16)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-emit a surface color with the given alpha, e.g. "rgb(17 18 20 / 0.8)".
|
||||
* Keeps the source notation when possible: wide-gamut palettes declare their
|
||||
* tokens in oklch (see the @supports block in globals.css) and converting them
|
||||
* to rgb would clip them; hsl stays hsl, oklch stays oklch, hex/rgb become rgb.
|
||||
* Also accepts raw "H S% L%" channel triplets as used by shadcn-style themes.
|
||||
*/
|
||||
export function surfaceColorWithAlpha(color: string, alpha: number): string | null {
|
||||
const trimmed = color.trim();
|
||||
if (!trimmed) return null;
|
||||
const fnMatch = /^([a-zA-Z-]+)\(([^()]+)\)$/.exec(trimmed);
|
||||
if (fnMatch) {
|
||||
const fn = fnMatch[1].toLowerCase();
|
||||
const inner = fnMatch[2].trim();
|
||||
if (fn === "rgb" || fn === "rgba") {
|
||||
const triplet = parseCssColorTriplet(trimmed);
|
||||
return triplet ? `rgb(${triplet[0]} ${triplet[1]} ${triplet[2]} / ${alpha})` : null;
|
||||
}
|
||||
if (inner.includes(",")) {
|
||||
const parts = inner.split(",").map((part) => part.trim());
|
||||
if (parts.length !== 3 || parts.some((part) => !part)) return null;
|
||||
return `${fn}(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})`;
|
||||
}
|
||||
const base = inner.split("/")[0].trim();
|
||||
if (!base) return null;
|
||||
return `${fn}(${base} / ${alpha})`;
|
||||
}
|
||||
if (trimmed.startsWith("#")) {
|
||||
const triplet = parseCssColorTriplet(trimmed);
|
||||
return triplet ? `rgb(${triplet[0]} ${triplet[1]} ${triplet[2]} / ${alpha})` : null;
|
||||
}
|
||||
if (/^[\d.]+%?\s+[\d.]+%?\s+[\d.]+%?$/.test(trimmed)) {
|
||||
return `hsl(${trimmed} / ${alpha})`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -326,6 +326,10 @@ export const loadPinnedTreeNodeIds = forward("loadPinnedTreeNodeIds");
|
||||
export const savePinnedTreeNodeIds = forward("savePinnedTreeNodeIds");
|
||||
export const loadEditorSettings = forward("loadEditorSettings");
|
||||
export const saveEditorSettings = forward("saveEditorSettings");
|
||||
export const saveBackgroundImage = forward("saveBackgroundImage");
|
||||
export const clearBackgroundImage = forward("clearBackgroundImage");
|
||||
export const readBackgroundImage = forward("readBackgroundImage");
|
||||
export const checkBackgroundImage = forward("checkBackgroundImage");
|
||||
export const loadOpenTabsState = forward("loadOpenTabsState");
|
||||
export const saveOpenTabsState = forward("saveOpenTabsState");
|
||||
export const saveDetachedTabHandoff = forward("saveDetachedTabHandoff");
|
||||
|
||||
@@ -1881,6 +1881,28 @@ export async function saveEditorSettings(settings: unknown): Promise<void> {
|
||||
await saveBrowserAppState("editor_settings", settings);
|
||||
}
|
||||
|
||||
// Background images are desktop-only: the web backend has no local data dir.
|
||||
export interface BackgroundImageInfo {
|
||||
storedPath: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export async function saveBackgroundImage(_sourcePath: string): Promise<BackgroundImageInfo> {
|
||||
throw new Error("Background images are only supported in the desktop app");
|
||||
}
|
||||
|
||||
export async function clearBackgroundImage(_storedPath: string): Promise<void> {
|
||||
throw new Error("Background images are only supported in the desktop app");
|
||||
}
|
||||
|
||||
export async function readBackgroundImage(_storedPath: string): Promise<string> {
|
||||
throw new Error("Background images are only supported in the desktop app");
|
||||
}
|
||||
|
||||
export async function checkBackgroundImage(_storedPath: string): Promise<boolean> {
|
||||
throw new Error("Background images are only supported in the desktop app");
|
||||
}
|
||||
|
||||
export async function loadOpenTabsState(): Promise<OpenTabsStatePayload | null> {
|
||||
const value = await loadBrowserAppState("open_tabs");
|
||||
if (!value || typeof value !== "object") return null;
|
||||
|
||||
@@ -740,6 +740,27 @@ export async function saveEditorSettings(settings: unknown): Promise<void> {
|
||||
return invoke("save_editor_settings", { settings });
|
||||
}
|
||||
|
||||
export interface BackgroundImageInfo {
|
||||
storedPath: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export async function saveBackgroundImage(sourcePath: string): Promise<BackgroundImageInfo> {
|
||||
return invoke("save_background_image", { sourcePath });
|
||||
}
|
||||
|
||||
export async function clearBackgroundImage(storedPath: string): Promise<void> {
|
||||
return invoke("clear_background_image", { storedPath });
|
||||
}
|
||||
|
||||
export async function readBackgroundImage(storedPath: string): Promise<string> {
|
||||
return invoke("read_background_image", { storedPath });
|
||||
}
|
||||
|
||||
export async function checkBackgroundImage(storedPath: string): Promise<boolean> {
|
||||
return invoke("check_background_image", { storedPath });
|
||||
}
|
||||
|
||||
export async function loadOpenTabsState(): Promise<OpenTabsStatePayload | null> {
|
||||
return invoke("load_open_tabs_state");
|
||||
}
|
||||
|
||||
@@ -179,6 +179,18 @@ describe("settingsTransfer", () => {
|
||||
expect(transferCategoryForKey("notARealSetting")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("round-trips the wallpaper settings under the appearance category", () => {
|
||||
const settings: EditorSettings = {
|
||||
...DEFAULT_EDITOR_SETTINGS,
|
||||
backgroundImage: { ...DEFAULT_EDITOR_SETTINGS.backgroundImage, filePath: "/data/background-image.png", fileName: "wall.png", opacity: 0.42 },
|
||||
};
|
||||
const result = parseSettingsTransferFile(serializeSettingsTransfer(settings));
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.editorSettings.backgroundImage).toEqual(settings.backgroundImage);
|
||||
expect(transferCategoryForKey("backgroundImage")).toBe("appearance");
|
||||
});
|
||||
|
||||
it("collects categories in display order without duplicates", () => {
|
||||
expect(collectTransferCategories(["snippets", "theme", "shortcuts", "fontFamily"])).toEqual(["appearance", "shortcuts", "snippets"]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { EditorSettings } from "@/stores/settingsStore";
|
||||
import { normalizeBackgroundImageSettings } from "@/lib/app/appBackgroundImage";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
@@ -11,6 +12,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"uiFontFamily",
|
||||
"uiScale",
|
||||
"theme",
|
||||
"backgroundImage",
|
||||
"customThemes",
|
||||
"activeCustomThemeId",
|
||||
"executeMode",
|
||||
@@ -131,6 +133,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn
|
||||
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
|
||||
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
|
||||
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
|
||||
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export function serializeSettingsTransfer(settings: EditorSettings, meta: Settin
|
||||
const SETTINGS_TRANSFER_CATEGORY_ORDER: readonly SettingsTransferCategoryId[] = ["appearance", "editor", "formatter", "navigation", "data", "shortcuts", "snippets", "other"];
|
||||
|
||||
const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readonly EditorSettingsDraftKey[]> = {
|
||||
appearance: ["fontFamily", "fontSize", "tableFontFamily", "uiFontFamily", "uiScale", "theme", "customThemes", "activeCustomThemeId", "toolbarItems", "updateNotificationsEnabled"],
|
||||
appearance: ["fontFamily", "fontSize", "tableFontFamily", "uiFontFamily", "uiScale", "theme", "customThemes", "activeCustomThemeId", "backgroundImage", "toolbarItems", "updateNotificationsEnabled"],
|
||||
editor: [
|
||||
"executeMode",
|
||||
"defaultTransactionMode",
|
||||
|
||||
Reference in new issue
Block a user