From 4c00dbb1dff7374b970b39bb2ac15ff2f15c6512 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 18 Oct 2026 21:16:29 +0800 Subject: [PATCH] fix: use glass as the default note image theme --- .../Data/Preferences/PreferencesStore.swift | 4 ++-- .../Features/Workspace/MemoDetailView.swift | 1 - .../EdgeEverTests/MobileUIParityTests.swift | 10 +++++++++- .../src/lib/note-image-preferences.test.mjs | 17 +++++++++++++++-- apps/mobile/src/screens/WorkspaceMemoDetail.tsx | 1 - apps/web/src/lib/note-image-preference.test.mjs | 10 ++++++++-- packages/shared/src/note-image-card.test.mjs | 7 ++++--- packages/shared/src/note-image-card.ts | 16 +++++++--------- .../shared/src/note-image-preferences.test.mjs | 11 ++++++++++- packages/shared/src/note-image-preferences.ts | 3 ++- tests/e2e/note-image-quality.spec.ts | 4 ++-- 11 files changed, 59 insertions(+), 25 deletions(-) diff --git a/apps/ios/EdgeEver/Data/Preferences/PreferencesStore.swift b/apps/ios/EdgeEver/Data/Preferences/PreferencesStore.swift index 7e9d6d26..6dddce91 100644 --- a/apps/ios/EdgeEver/Data/Preferences/PreferencesStore.swift +++ b/apps/ios/EdgeEver/Data/Preferences/PreferencesStore.swift @@ -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) { diff --git a/apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift b/apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift index 07c289a3..a41732c9 100644 --- a/apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift +++ b/apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift @@ -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") diff --git a/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift b/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift index a5dc0cc8..004aa954 100644 --- a/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift +++ b/apps/ios/Tests/EdgeEverTests/MobileUIParityTests.swift @@ -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") diff --git a/apps/mobile/src/lib/note-image-preferences.test.mjs b/apps/mobile/src/lib/note-image-preferences.test.mjs index 0a0eac74..7b55c739 100644 --- a/apps/mobile/src/lib/note-image-preferences.test.mjs +++ b/apps/mobile/src/lib/note-image-preferences.test.mjs @@ -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(); diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx index a283ae35..88fa079a 100644 --- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx +++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx @@ -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" }, diff --git a/apps/web/src/lib/note-image-preference.test.mjs b/apps/web/src/lib/note-image-preference.test.mjs index 32054e46..75457455 100644 --- a/apps/web/src/lib/note-image-preference.test.mjs +++ b/apps/web/src/lib/note-image-preference.test.mjs @@ -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 }); }); } }); diff --git a/packages/shared/src/note-image-card.test.mjs b/packages/shared/src/note-image-card.test.mjs index b0322f39..378a6ad6 100644 --- a/packages/shared/src/note-image-card.test.mjs +++ b/packages/shared/src/note-image-card.test.mjs @@ -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: "

Text

", 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", () => { diff --git a/packages/shared/src/note-image-card.ts b/packages/shared/src/note-image-card.ts index fea0d951..af31a101 100644 --- a/packages/shared/src/note-image-card.ts +++ b/packages/shared/src/note-image-card.ts @@ -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, 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 = { - 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 = { } as Record; const CANVAS_COLORS: Record = { - 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 = ({ `; }; -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); diff --git a/packages/shared/src/note-image-preferences.test.mjs b/packages/shared/src/note-image-preferences.test.mjs index e8fbb109..5245fe09 100644 --- a/packages/shared/src/note-image-preferences.test.mjs +++ b/packages/shared/src/note-image-preferences.test.mjs @@ -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) { diff --git a/packages/shared/src/note-image-preferences.ts b/packages/shared/src/note-image-preferences.ts index 92c1260f..55d12fc9 100644 --- a/packages/shared/src/note-image-preferences.ts +++ b/packages/shared/src/note-image-preferences.ts @@ -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; + if (values.theme === "gradient") preferences.theme = "glass"; for (const key of Object.keys(choices) as Array) { const value = values[key]; if (typeof value === "string" && (choices[key] as readonly string[]).includes(value)) { diff --git a/tests/e2e/note-image-quality.spec.ts b/tests/e2e/note-image-quality.spec.ts index 9b726304..eaacdd10 100644 --- a/tests/e2e/note-image-quality.spec.ts +++ b/tests/e2e/note-image-quality.spec.ts @@ -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 = ``; 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!);