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"); + }); +});