fix: use glass as the default note image theme

This commit is contained in:
tianma-if committed 2026-10-18 21:16:29 +08:00
1 parent f4732e3df7
commit 4c00dbb1df
11 files changed
+59 -25

No files matched your search

@@ -64,7 +64,7 @@ enum AppUILocale {
/// Local image-share choices, independent of the current note or sheet lifetime.
struct NoteImageSharePreferences: Codable, Equatable {
var theme: String = "gradient"
var theme: String = "glass"
var fontStyle: String = "auto"
var fontSize: String = "lg"
var cardWidth: String = "wide"
@@ -85,7 +85,7 @@ struct NoteImageSharePreferences: Codable, Equatable {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? values.decode(String.self, forKey: .theme),
["gradient", "glass", "quote", "book", "memo", "bento", "black-sun", "frame", "handwritten", "code"].contains(value) {
theme = value
theme = value == "gradient" ? "glass" : value
}
if let value = try? values.decode(String.self, forKey: .fontStyle),
(["auto", "serif", "sans", "mono", "kai"] + NoteImageFontCatalog.choices.map(\.id)).contains(value) {
@@ -187,7 +187,6 @@ struct MemoDetailView: View {
preferences.noteImageShare = share
}
)) {
Text(env.preferences.t("默认", en: "Classic", pl: "Klasyczny")).tag("gradient")
Text(env.preferences.t("透明", en: "Glass", pl: "Szkło")).tag("glass")
Text(env.preferences.t("金句", en: "Quote", pl: "Cytat")).tag("quote")
Text(env.preferences.t("书摘", en: "Book excerpt", pl: "Fragment książki")).tag("book")
@@ -27,10 +27,18 @@ final class MobileUIParityTests: XCTestCase {
XCTAssertEqual(PreferencesStore(defaults: defaults).noteImageShare, NoteImageSharePreferences())
}
func testImageShareOldDefaultBecomesGlass() throws {
let raw = #"{"theme":"gradient","fontStyle":"serif","showTitle":false}"#
let settings = try JSONDecoder().decode(NoteImageSharePreferences.self, from: Data(raw.utf8))
XCTAssertEqual(settings.theme, "glass")
XCTAssertEqual(settings.fontStyle, "serif")
XCTAssertFalse(settings.showTitle)
}
func testImageShareInvalidFieldsDoNotDiscardValidChoices() throws {
let raw = #"{"theme":"removed","fontStyle":"serif","fontSize":12,"cardWidth":"compact","format":"svg","showTitle":false,"showNotebook":true,"showTags":"true","showUpdatedAt":false,"showBranding":false}"#
let settings = try JSONDecoder().decode(NoteImageSharePreferences.self, from: Data(raw.utf8))
XCTAssertEqual(settings.theme, "gradient")
XCTAssertEqual(settings.theme, "glass")
XCTAssertEqual(settings.fontStyle, "serif")
XCTAssertEqual(settings.fontSize, "lg")
XCTAssertEqual(settings.cardWidth, "compact")
@@ -8,6 +8,19 @@ const changed = {
showTags: true, showUpdatedAt: false, showBranding: false,
};
test('saved classic theme opens as glass and survives persistence and restart', async () => {
let raw = JSON.stringify({ ...changed, theme: 'gradient' });
const storage = { getItem: async () => raw, setItem: async (_key, value) => { raw = value; } };
const store = createMobileNoteImagePreferenceStore(storage);
await store.load();
expect(store.getSnapshot()).toEqual({ ...changed, theme: 'glass' });
store.update('showTitle', true);
await store.flush();
const restarted = createMobileNoteImagePreferenceStore(storage);
await restarted.load();
expect(restarted.getSnapshot()).toEqual({ ...changed, theme: 'glass', showTitle: true });
});
test('switching templates persists automatic typography and allows a later manual font', async () => {
let raw = JSON.stringify(changed);
const storage = { getItem: async () => raw, setItem: async (_key, value) => { raw = value; } };
@@ -55,7 +68,7 @@ test('slow startup reads never write defaults over saved choices', async () => {
const loading = store.load();
expect(store.getSnapshot()).toBeUndefined();
expect(store.load()).toBe(loading);
store.update('theme', 'gradient');
store.update('theme', 'glass');
finishRead(JSON.stringify(changed));
await loading;
expect(store.getSnapshot()).toEqual(changed);
@@ -94,7 +107,7 @@ test('corrupt fields are isolated and rejected writes keep session choices usabl
setItem: async () => { throw new Error('Storage unavailable'); },
});
await store.load();
expect(store.getSnapshot()).toEqual({ ...changed, theme: 'gradient', fontSize: 'lg', showTitle: true });
expect(store.getSnapshot()).toEqual({ ...changed, theme: 'glass', fontSize: 'lg', showTitle: true });
store.update('showTitle', false);
store.update('theme', 'code');
await store.flush();
@@ -219,7 +219,6 @@ const loadAuthenticatedSvg = (source: AuthenticatedImageSource) => {
};
const MOBILE_THEME_LABELS = {
"gradient": { labelZh: "默认", labelEn: "Classic" },
"glass": { labelZh: "透明", labelEn: "Glass" },
"quote": { labelZh: "金句", labelEn: "Quote" },
"book": { labelZh: "书摘", labelEn: "Book excerpt" },
@@ -32,14 +32,20 @@ test('migrates the existing title choice and signature without losing explicit f
test('invalid fields fall back independently while valid false choices survive', () => {
withStorage({ [key]: JSON.stringify({ theme: 'removed-theme', fontSize: 20, cardWidth: 'compact', format: 'svg', showTags: 'true', showUpdatedAt: false, showBranding: false, author: 12 }) }, () => {
expect(readNoteImagePreferences()).toMatchObject({ theme: 'gradient', fontSize: 'lg', cardWidth: 'compact', format: 'png', showTags: false, showUpdatedAt: false, showBranding: false, author: '' });
expect(readNoteImagePreferences()).toMatchObject({ theme: 'glass', fontSize: 'lg', cardWidth: 'compact', format: 'png', showTags: false, showUpdatedAt: false, showBranding: false, author: '' });
});
});
test('saved classic theme opens as glass while preserving custom typography and visibility', () => {
withStorage({ [key]: JSON.stringify({ theme: 'gradient', fontStyle: 'heavy', showTitle: false }) }, () => {
expect(readNoteImagePreferences()).toMatchObject({ theme: 'glass', fontStyle: 'heavy', showTitle: false });
});
});
test('malformed JSON and non-object storage retain usable legacy defaults', () => {
for (const stored of ['{broken', 'null', '[]', 'false', '"book"']) {
withStorage({ [key]: stored, edgeever_share_image_show_title: 'false' }, () => {
expect(readNoteImagePreferences()).toMatchObject({ theme: 'gradient', showTitle: false });
expect(readNoteImagePreferences()).toMatchObject({ theme: 'glass', showTitle: false });
});
}
});
+4 -3
View File
@@ -53,8 +53,9 @@ describe("note image templates", () => {
test("old export settings resolve to supported templates through either input", () => {
const mapping = {
gradient: "glass",
paper: "handwritten", press: "book", highlight: "memo", slate: "frame", mint: "quote",
aurora: "gradient", sunset: "glass", terminal: "code", midnight: "black-sun", editorial: "frame",
aurora: "glass", sunset: "glass", terminal: "code", midnight: "black-sun", editorial: "frame",
film: "book", lavender: "glass", notepad: "memo", xuan: "handwritten", polaroid: "frame", warm: "glass",
};
for (const [old, active] of Object.entries(mapping)) {
@@ -62,9 +63,9 @@ describe("note image templates", () => {
expect(resolveTheme(undefined, old)).toBe(active);
expect(buildNoteImageCardMarkup({ title: "Note", bodyHtml: "<p>Text</p>", theme: old })).toContain(`data-theme="${active}"`);
}
expect(resolveTheme()).toBe("gradient");
expect(resolveTheme()).toBe("glass");
expect(resolveTheme("book", "missing")).toBe("book");
expect(resolveTheme("__proto__", "constructor")).toBe("gradient");
expect(resolveTheme("__proto__", "constructor")).toBe("glass");
});
test("explicit fonts override both heading and body for every template", () => {
+7 -9
View File
@@ -2,9 +2,9 @@ import { CATALOG_FONT_STACKS, type RemoteNoteImageFont } from "./font-catalog";
export { prepareNoteImageContent } from "./note-image-content";
export type NoteImageFormat = "jpeg" | "png";
export const NOTE_IMAGE_THEME_OPTIONS = ["gradient", "glass", "quote", "book", "memo", "bento", "black-sun", "frame", "handwritten", "code"] as const;
export const NOTE_IMAGE_THEME_OPTIONS = ["glass", "quote", "book", "memo", "bento", "black-sun", "frame", "handwritten", "code"] as const;
export type NoteImageActiveTheme = (typeof NOTE_IMAGE_THEME_OPTIONS)[number];
type LegacyNoteImageTheme = "paper" | "press" | "highlight" | "slate" | "aurora" | "sunset" | "midnight" | "editorial" | "terminal" | "film" | "mint" | "lavender";
type LegacyNoteImageTheme = "gradient" | "paper" | "press" | "highlight" | "slate" | "aurora" | "sunset" | "midnight" | "editorial" | "terminal" | "film" | "mint" | "lavender";
export type NoteImageTheme = NoteImageActiveTheme | LegacyNoteImageTheme;
export type NoteImageBackground = NoteImageTheme | "warm" | "notepad" | "xuan" | "polaroid";
export type NoteImageFontStyle = "auto" | "sans" | "serif" | "mono" | "kai" | RemoteNoteImageFont;
@@ -35,7 +35,7 @@ export const planNativeNoteImageRender = (width: number, height: number) => {
};
const LEGACY_THEMES: Record<Exclude<NoteImageBackground, NoteImageActiveTheme>, NoteImageActiveTheme> = {
paper: "handwritten", press: "book", highlight: "memo", slate: "frame", aurora: "gradient",
paper: "handwritten", press: "book", highlight: "memo", slate: "frame", aurora: "glass", gradient: "glass",
sunset: "glass", midnight: "black-sun", editorial: "frame", terminal: "code", film: "book",
mint: "quote", lavender: "glass", warm: "glass", notepad: "memo", xuan: "handwritten", polaroid: "frame",
};
@@ -46,7 +46,7 @@ export const resolveTheme = (background?: NoteImageBackground, theme?: NoteImage
if ((NOTE_IMAGE_THEME_OPTIONS as readonly string[]).includes(candidate)) return candidate as NoteImageActiveTheme;
if (Object.hasOwn(LEGACY_THEMES, candidate)) return LEGACY_THEMES[candidate as keyof typeof LEGACY_THEMES];
}
return "gradient";
return "glass";
};
export type ThemeStyleConfig = {
@@ -73,7 +73,6 @@ const palette = (base: Pick<ThemeStyleConfig, "canvasBg" | "cardBg" | "textColor
// Measured against the public Firefly Card templates at their 400px / 440px design widths.
// Structure and CSS are implemented here; exports never depend on that service.
const ACTIVE_THEMES: Record<NoteImageActiveTheme, ThemeStyleConfig> = {
gradient: palette({ canvasBg: "linear-gradient(0deg, #5797f9, #6cd5c4)", cardBg: "#ffffff", textColor: "#000000", cardShadow: "0 5px 10px rgba(99,99,99,0.2), 0 15px 15px rgba(99,99,99,0.2)" }),
glass: palette({ canvasBg: "linear-gradient(45deg, #5797f9, #6cd5c4)", cardBg: "rgba(255,255,255,0.7)", textColor: "#000000", cardShadow: "0 5px 10px rgba(99,99,99,0.2), 0 15px 15px rgba(99,99,99,0.2)" }),
quote: palette({ canvasBg: "#ffffff", cardBg: "transparent", textColor: "#0a7c07", metaColor: "#497447", accentColor: "#0a7c07" }),
book: palette({ canvasBg: "#1b1c1e", cardBg: "transparent", textColor: "#e3d2b3", metaColor: "#c5b89f", isDark: true }),
@@ -91,7 +90,7 @@ export const NOTE_IMAGE_THEMES: Record<NoteImageTheme, ThemeStyleConfig> = {
} as Record<NoteImageTheme, ThemeStyleConfig>;
const CANVAS_COLORS: Record<NoteImageActiveTheme, string> = {
gradient: "#5797f9", glass: "#5797f9", quote: "#ffffff", book: "#1b1c1e", memo: "#ffffff",
glass: "#5797f9", quote: "#ffffff", book: "#1b1c1e", memo: "#ffffff",
bento: "#5797f9", "black-sun": "#101010", frame: "#ffffff", handwritten: "#e3dfd4", code: "#5797f9",
};
export const NOTE_IMAGE_BACKGROUND_COLORS = {
@@ -125,7 +124,6 @@ type TemplateTypography = {
};
export const NOTE_IMAGE_TEMPLATE_TYPOGRAPHY = {
gradient: { body: ["misans"], heading: ["misans"], headingWeight: 700 },
glass: { body: ["misans"], heading: ["mincho"], headingWeight: 400 },
quote: { body: ["mincho"], heading: ["mincho"], headingWeight: 700 },
book: { body: ["song"], heading: ["song"], headingWeight: 700 },
@@ -199,7 +197,7 @@ export type NoteImageCardMarkupOptions = {
};
export const buildNoteImageCardMarkup = ({
title, notebook = "", tags = [], updatedAt = "", bodyHtml, theme = "gradient",
title, notebook = "", tags = [], updatedAt = "", bodyHtml, theme = "glass",
showTitle = false, showNotebook = false, showTags = false, showUpdatedAt = true,
showBranding = true, author = "", showAuthor = false, showReadingTime = false, readingTimeText = "",
}: NoteImageCardMarkupOptions) => {
@@ -255,7 +253,7 @@ export const buildNoteImageCardMarkup = ({
</div>`;
};
export const generateCardCss = ({ theme = "gradient", fontStyle = "auto", fontSize = "lg", cardWidth = "wide" }: {
export const generateCardCss = ({ theme = "glass", fontStyle = "auto", fontSize = "lg", cardWidth = "wide" }: {
theme: NoteImageTheme; fontStyle: NoteImageFontStyle; fontSize: NoteImageFontSize; cardWidth: NoteImageCardWidth;
}) => {
const active = resolveTheme(undefined, theme);
@@ -1,7 +1,16 @@
import { expect, test } from "bun:test";
import { DEFAULT_NOTE_IMAGE_PREFERENCES, updateNoteImagePreference } from "./note-image-preferences";
import { DEFAULT_NOTE_IMAGE_PREFERENCES, resolveNoteImagePreferences, updateNoteImagePreference } from "./note-image-preferences";
import { NOTE_IMAGE_THEME_OPTIONS, resolveNoteImageTypography } from "./note-image-card";
test("glass replaces the old default without discarding other saved preferences", () => {
expect(DEFAULT_NOTE_IMAGE_PREFERENCES.theme).toBe("glass");
expect(NOTE_IMAGE_THEME_OPTIONS[0]).toBe("glass");
expect(NOTE_IMAGE_THEME_OPTIONS).not.toContain("gradient");
const saved = { ...DEFAULT_NOTE_IMAGE_PREFERENCES, theme: "gradient", fontStyle: "heavy", showTitle: false, author: "天马" };
expect(resolveNoteImagePreferences(saved)).toEqual({ ...saved, theme: "glass" });
expect(resolveNoteImagePreferences(saved, { ...DEFAULT_NOTE_IMAGE_PREFERENCES, theme: "book" }).theme).toBe("glass");
});
test("selecting any template restores its typography after a custom font", () => {
const custom = { ...DEFAULT_NOTE_IMAGE_PREFERENCES, fontStyle: "heavy", showAuthor: true, author: "天马" };
for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
@@ -24,7 +24,7 @@ export type NoteImagePreferences = {
};
export const DEFAULT_NOTE_IMAGE_PREFERENCES: NoteImagePreferences = {
theme: "gradient", fontStyle: "auto", fontSize: "lg", cardWidth: "wide", format: "png",
theme: "glass", fontStyle: "auto", fontSize: "lg", cardWidth: "wide", format: "png",
showTitle: true, showNotebook: false, showTags: false, showUpdatedAt: true,
showReadingTime: false, showAuthor: false, showBranding: true, author: "",
};
@@ -55,6 +55,7 @@ export const resolveNoteImagePreferences = (
const preferences = { ...fallback };
if (!stored || typeof stored !== "object" || Array.isArray(stored)) return preferences;
const values = stored as Record<string, unknown>;
if (values.theme === "gradient") preferences.theme = "glass";
for (const key of Object.keys(choices) as Array<keyof typeof choices>) {
const value = values[key];
if (typeof value === "string" && (choices[key] as readonly string[]).includes(value)) {
+2 -2
View File
@@ -18,7 +18,7 @@ type QualityApi = {
setMounted: (mounted: boolean) => void;
};
type QualityWindow = Window & { imageQuality: QualityApi; copiedImage?: { width: number; height: number; hash: string } };
const labels = { gradient: '默认', glass: '透明', quote: '金句', book: '书摘', memo: '备忘录', bento: '便当', 'black-sun': '黑日', frame: '框界', handwritten: '手写', code: '代码' };
const labels = { glass: '透明', quote: '金句', book: '书摘', memo: '备忘录', bento: '便当', 'black-sun': '黑日', frame: '框界', handwritten: '手写', code: '代码' };
const preamble = `<script type="module">import R from '/@react-refresh';R.injectIntoGlobalHook(window);window.$RefreshReg$=()=>{};window.$RefreshSig$=()=>t=>t;window.__vite_plugin_react_preamble_installed__=true;</script>`;
const fixture = fileURLToPath(new URL('./fixtures/note-image-quality.tsx', import.meta.url));
@@ -82,7 +82,7 @@ for (const theme of NOTE_IMAGE_THEME_OPTIONS) {
await boot(page);
const preview = page.getByAltText('笔记分享图片预览');
await expect(preview).toBeVisible();
if (theme !== 'gradient') {
if (theme !== 'glass') {
const old = await preview.getAttribute('src');
await page.getByRole('button', { name: labels[theme], exact: true }).click();
await expect(preview).not.toHaveAttribute('src', old!);