From e4c97fdfab8fee00c2e4ceb089686ef524d737f4 Mon Sep 17 00:00:00 2001
From: yhh <359807859@qq.com>
Date: Fri, 6 Nov 2026 02:04:33 -0800
Subject: [PATCH] feat(studio): visual effects is one switch and the appearance
page gets its advanced-appearance row, as the look board draws them
---
desktop/frontend-next/src/i18n/msg/look.ts | 2 +
desktop/frontend-next/src/ui/Appearance.tsx | 24 +++++-----
.../ui/effects-switch.interaction.test.tsx | 47 +++++++++++++++++++
.../src/ui/size-row.interaction.test.tsx | 9 ++++
4 files changed, 71 insertions(+), 11 deletions(-)
create mode 100644 desktop/frontend-next/src/ui/effects-switch.interaction.test.tsx
diff --git a/desktop/frontend-next/src/i18n/msg/look.ts b/desktop/frontend-next/src/i18n/msg/look.ts
index 8b36b33a77..a96eed0408 100644
--- a/desktop/frontend-next/src/i18n/msg/look.ts
+++ b/desktop/frontend-next/src/i18n/msg/look.ts
@@ -2,6 +2,7 @@ import type { Messages } from "./types";
export const LOOK: Messages = {
"look.a_few_of_name_s": { zh: "{name} 有几项没生效", en: "A few of {name}'s entries did not take" },
+ "look.advanced_look": { zh: "高级外观", en: "Advanced appearance" },
"look.at_the_current_window_size": { zh: "当前窗口尺寸下,此壁纸只有上下会被裁剪,左右正好填满,因此横向焦点无法调整。", en: "At the current window size this wallpaper is cropped only top and bottom and fills the width exactly, so the horizontal focus cannot be moved." },
"look.at_the_current_window_size_2": { zh: "当前窗口尺寸下,此壁纸只有左右会被裁剪,上下正好填满,因此纵向焦点无法调整。", en: "At the current window size this wallpaper is cropped only left and right and fills the height exactly, so the vertical focus cannot be moved." },
"look.back_to_the_default_typeface": { zh: "恢复默认字体", en: "Back to the default typeface" },
@@ -41,6 +42,7 @@ export const LOOK: Messages = {
"look.if_the_window_keeps_your": { zh: "窗口一直占用显卡、或界面花屏时,先试「视觉效果:节能」,不行再改为「仅软件渲染」:改用 CPU 绘制,显卡不再参与。设置保存后,需退出并重新打开 Studio 才会生效。", en: "If the window keeps your GPU busy or draws garbage, try Visual effects: Reduced first; if that is not enough, choose Software only: the CPU draws the window and the GPU stays out of it. The choice is saved now and applies after you quit and reopen Studio." },
"look.illustrated_and_background_themes_you": { zh: "插画与背景主题,可随时切回基础主题", en: "Illustrated and background themes; you can return to a foundation theme at any time" },
"look.import_theme": { zh: "导入主题…", en: "Import theme…" },
+ "look.import_themes_custom_colours": { zh: "导入主题、自定义颜色", en: "Import themes, custom colours" },
"look.import_wallpaper_engine": { zh: "导入 Wallpaper Engine…", en: "Import Wallpaper Engine…" },
"look.imported_name": { zh: "已导入「{name}」。", en: "Imported “{name}”." },
"look.in_effect_now": { zh: "立即生效", en: "In effect now" },
diff --git a/desktop/frontend-next/src/ui/Appearance.tsx b/desktop/frontend-next/src/ui/Appearance.tsx
index a1de9c3b36..02aaf39497 100644
--- a/desktop/frontend-next/src/ui/Appearance.tsx
+++ b/desktop/frontend-next/src/ui/Appearance.tsx
@@ -1,7 +1,7 @@
import { ApplyNote } from "./Group";
-import { SettingDeep } from "./SettingDeep";
-import { useCallback, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from "react";
-import { effectsMode, onEffectsChange, setEffectsMode, type EffectsMode } from "../state/prefs";
+import { SettingDeep, SettingDeepContext } from "./SettingDeep";
+import { useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from "react";
+import { effectsMode, onEffectsChange, setEffectsMode } from "../state/prefs";
import type { AgentPort, Appearance as Look, ThemePack } from "../port/port";
import { SIZE_STEPS, ZOOM_PRESETS } from "./zoom";
import { chord } from "./keys";
@@ -14,7 +14,8 @@ import { useCrop } from "./useCrop";
import { WindowSection } from "./WindowSection";
import { GraphicsSection } from "./GraphicsSection";
import { SectionCard } from "./SectionCard";
-import { SegChoice, SettingRow } from "./SettingRow";
+import { PillButton, SegChoice, SettingRow } from "./SettingRow";
+import { Switch } from "./Switch";
import { ThemeModeRow } from "./ThemeModeRow";
import { ThemeImport } from "./ThemeImport";
import { ThemeRemove } from "./ThemeRemove";
@@ -62,11 +63,6 @@ const MEASURES: [string, string, string][] = [
["custom", "look.custom_percentage", "look.set_the_conversation_and_composer"],
];
-const EFFECTS: [EffectsMode, string, string][] = [
- ["full", "look.full", "look.background_blur_the_animated_sky"],
- ["reduced", "look.reduced", "look.turns_off_background_blur_the"],
-];
-
const CONTRASTS: [string, string, string][] = [
["", "look.follow_the_system", "look.uses_the_strongest_step_when"],
["soft", "look.soft", "look.softer_body_text_easier_over"],
@@ -84,6 +80,7 @@ export const WALLPAPER_ACCEPT = "image/png,image/jpeg,image/webp,image/avif,imag
export function Appearance({ port, theme, onTheme, contrast, onContrast, weight, onWeight, reloadThemes, look, onLook }: Props) {
const { packs, unread, load } = useThemeInventory(port);
const effects = useSyncExternalStore(onEffectsChange, effectsMode);
+ const deep = useContext(SettingDeepContext);
// Activating repaints through App's own theme effect, so this only refreshes
// the list and asks App to re-read which pack is active.
@@ -387,9 +384,14 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
- label={t("look.visual_effects")} value={effects} action="appearance.effects" onPick={setEffectsMode}
- options={EFFECTS.map(([id, name]) => [id, t(name)] as const)} />
+ setEffectsMode(effects === "full" ? "reduced" : "full")} />
+ {deep.pages && (
+
+ deep.go("scheme")}>{t("look.advanced_look")}
+
+ )}
diff --git a/desktop/frontend-next/src/ui/effects-switch.interaction.test.tsx b/desktop/frontend-next/src/ui/effects-switch.interaction.test.tsx
new file mode 100644
index 0000000000..9da7912ffd
--- /dev/null
+++ b/desktop/frontend-next/src/ui/effects-switch.interaction.test.tsx
@@ -0,0 +1,47 @@
+// @vitest-environment jsdom
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "./testkit";
+import { Appearance } from "./Appearance";
+import { apply } from "./look";
+import { effectsMode, setEffectsMode } from "../state/prefs";
+import { MockPort } from "../port/mock";
+import { boot, STORAGE } from "../i18n";
+import type { AgentPort } from "../port/port";
+
+beforeEach(() => {
+ localStorage.clear();
+ localStorage.setItem(STORAGE, "zh");
+ boot();
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
+});
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ apply(null);
+ setEffectsMode("full");
+});
+
+function draw() {
+ render( {}} contrast="" onContrast={() => {}}
+ weight="" onWeight={() => {}} look={{}} onLook={() => {}} reloadThemes={() => {}} />);
+}
+
+describe("the visual effects row", () => {
+ it("is one switch, on for the full effects", () => {
+ draw();
+ const sw = screen.getByRole("switch", { name: "视觉效果" });
+ expect(sw.getAttribute("aria-checked")).toBe("true");
+ });
+
+ it("turns the effects down and back up", async () => {
+ draw();
+ const sw = screen.getByRole("switch", { name: "视觉效果" });
+ await userEvent.click(sw);
+ expect(effectsMode()).toBe("reduced");
+ expect(sw.getAttribute("aria-checked")).toBe("false");
+ await userEvent.click(sw);
+ expect(effectsMode()).toBe("full");
+ });
+});
diff --git a/desktop/frontend-next/src/ui/size-row.interaction.test.tsx b/desktop/frontend-next/src/ui/size-row.interaction.test.tsx
index 12eb93ceb0..c627e03f72 100644
--- a/desktop/frontend-next/src/ui/size-row.interaction.test.tsx
+++ b/desktop/frontend-next/src/ui/size-row.interaction.test.tsx
@@ -62,3 +62,12 @@ describe("the size row on the appearance page", () => {
expect(go).toHaveBeenCalledWith("size");
});
});
+
+describe("the advanced appearance row", () => {
+ it("opens the page that holds theme import and custom colours", async () => {
+ const { go } = draw();
+ expect(screen.getByText("导入主题、自定义颜色")).toBeTruthy();
+ await userEvent.click(screen.getByRole("button", { name: "高级外观" }));
+ expect(go).toHaveBeenCalledWith("scheme");
+ });
+});