feat(studio): visual effects is one switch and the appearance page gets its advanced-appearance row, as the look board draws them

This commit is contained in:
yhh committed 2026-11-06 02:04:33 -08:00
1 parent c8eb8693fa
commit e4c97fdfab
4 files changed
+71 -11

No files matched your search

@@ -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" },
+13 -11
View File
@@ -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,
</SettingDeep>
<SettingRow id="effects" title={t("look.visual_effects")} desc={t("settings.frosted_glass_glow_effects")}>
<SegChoice<EffectsMode> label={t("look.visual_effects")} value={effects} action="appearance.effects" onPick={setEffectsMode}
options={EFFECTS.map(([id, name]) => [id, t(name)] as const)} />
<Switch on={effects === "full"} label={t("look.visual_effects")} data-action="appearance.effects" data-value={effects}
onClick={() => setEffectsMode(effects === "full" ? "reduced" : "full")} />
</SettingRow>
{deep.pages && (
<SettingRow id="advanced-look" title={t("look.advanced_look")} desc={t("look.import_themes_custom_colours")}>
<PillButton action="settings.open-page" target="scheme" onClick={() => deep.go("scheme")}>{t("look.advanced_look")}</PillButton>
</SettingRow>
)}
</SectionCard>
<SectionCard title={t("settings.text_card")}>
<SettingDeep id="font" title={t("look.typeface")} hint={t("settings.interface_and_code_typeface")} act={t("settings.change")}>
@@ -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(<Appearance port={new MockPort() as unknown as AgentPort} theme="light" onTheme={() => {}} 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");
});
});
@@ -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");
});
});