From e7f620033fad829ef6ee23491f2d172aaba835f2 Mon Sep 17 00:00:00 2001 From: penge Date: Sun, 3 Dec 2023 16:24:47 +0100 Subject: [PATCH] Fix theme flickering on page load - use template.html to create notes.html and options.html - update README --- README.md | 22 ++++--- build.ts | 7 +++ public/options.html | 16 ----- public/themes/shared.css | 1 - src/notes.tsx | 31 ++-------- src/options.tsx | 17 ------ public/notes.html => src/template.html | 8 ++- src/themes/__tests__/set-theme.test.ts | 85 -------------------------- src/themes/__tests__/themes.test.ts | 26 ++++---- src/themes/custom/custom.tsx | 4 +- src/themes/init.ts | 51 ++++++++++++++++ src/themes/set-theme.ts | 55 ----------------- 12 files changed, 100 insertions(+), 223 deletions(-) delete mode 100644 public/options.html rename public/notes.html => src/template.html (62%) delete mode 100644 src/themes/__tests__/set-theme.test.ts create mode 100644 src/themes/init.ts delete mode 100644 src/themes/set-theme.ts diff --git a/README.md b/README.md index 358515e0..1600a9e5 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,12 @@ Custom theme allows you to customize My Notes styles in many ways. To use a Custom theme, open Options, select **"Custom"** theme, and click on the **"Customize"** button to start creating your own theme. -To start, paste into the editor content of [light.css](public/themes/light.css) or [dark.css](public/themes/dark.css). +To start, either copy and paste into the editor the content of [light.css](public/themes/light.css) or [dark.css](public/themes/dark.css), or use CSS `@import`. + Then, modify CSS variables as you like to change background colors, text colors, etc. You can add any valid CSS as well to make further changes. -Click on the **"Save"** button to save the custom theme. + +Click on the **"Save"** button to save the Custom theme.

@@ -227,12 +229,10 @@ src/ # - Registers the ways to open My Notes (icon click, keyboard shortcut) # - Registers events to trigger Google Drive Sync from My Notes - dom/ # Helpers to get DOM elements + i18n/ # Internationalization (English) integration/ # Integration tests for Google Drive Sync - messages/ # Communication between My Notes and background script - notes/ # Everything related to Notes # - Create/Rename/Delete notes; Note editing, Note saving # - Toolbar @@ -249,11 +249,14 @@ src/ # - Helpers for Chrome Storage # - Default values (Notes, Options) + svg/ # SVG images for Toolbar + themes/ # Light, Dark, Custom - background.ts # Main script for background page + background.ts # Main script for service worker notes.ts # Main script for notes options.ts # Main script for options + template.html # Template for notes.html and options.html public/ # All public files (images, icons, HTML, CSS) copied to dist/ @@ -264,7 +267,8 @@ public/ # All public files (images, icons, HTML, CSS) copied to dist .gitignore # Files excluded from Git jest.config.ts # Jest configuration -tsconfig.json # Typescript configuration +jest.setup.ts # Jest setup +tsconfig.json # TypeScript configuration package-lock.json package.json @@ -272,7 +276,8 @@ package.json LICENSE # MIT manifest.json # Main extension file -build.ts # Produces /dist folder +register.js # Uses ts-node/esm to resolve build.ts +build.ts # Produces dist/ folder README.md ``` @@ -307,6 +312,7 @@ My Notes has the permissions listed in `manifest.json`. - `"storage"` — used to save your notes and options to Chrome Storage (locally in your Chrome) - `"unlimitedStorage"` — used to increase the default storage limit (which is 5MB) - `"contextMenus"` — used to create My Notes Context menu +- `"notifications"` — used to show a Chrome notification (when Context menu was used) Required permissions are shown to the user before installing the extension, and are needed at all times to provide the basic functionality. diff --git a/build.ts b/build.ts index 64b88b3e..d704919b 100644 --- a/build.ts +++ b/build.ts @@ -1,3 +1,4 @@ +import fs from "fs"; import esbuild from "esbuild"; esbuild.build({ @@ -6,6 +7,7 @@ esbuild.build({ "./src/notes.tsx", "./src/options.tsx", "./src/themes/custom/custom.tsx", + "./src/themes/init.ts", ...(process.env.NODE_ENV === "development" ? ["./src/integration/index.ts"] : []), ], chunkNames: "chunks/[name]-[hash]", @@ -23,4 +25,9 @@ esbuild.build({ minify: process.env.NODE_ENV === "production", sourcemap: process.env.NODE_ENV === "development" ? "inline" : false, logLevel: "info", +}).then(() => { + const templateString = fs.readFileSync("./src/template.html", "utf8"); + ["notes", "options"].forEach((page) => { + fs.writeFileSync(`./dist/${page}.html`, templateString.replaceAll("{{page}}", page)); + }); }); diff --git a/public/options.html b/public/options.html deleted file mode 100644 index 59a4cc57..00000000 --- a/public/options.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - My Notes - - - - - - - - - diff --git a/public/themes/shared.css b/public/themes/shared.css index c646a6f4..2cf992b3 100644 --- a/public/themes/shared.css +++ b/public/themes/shared.css @@ -10,7 +10,6 @@ body { margin: 0; padding: 0; - opacity: 0; background: var(--background-color); color: var(--text-color); } diff --git a/src/notes.tsx b/src/notes.tsx index 771df7be..7c2ceb7c 100644 --- a/src/notes.tsx +++ b/src/notes.tsx @@ -10,14 +10,12 @@ import { Notification, RegularFont, GoogleFont, - Theme, NotesObject, NotesOrder, Sync, Message, MessageType, } from "shared/storage/schema"; -import { setTheme as setThemeCore } from "themes/set-theme"; import Overview from "notes/components/Overview"; import __Notification from "notes/components/Notification"; @@ -62,7 +60,7 @@ interface NotesProps { active: string } -const Notes = (): h.JSX.Element => { +const Notes = (): h.JSX.Element | null => { const [os, setOs] = useState(undefined); const [tabId, setTabId] = useState(undefined); const [initialized, setInitialized] = useState(false); @@ -78,8 +76,6 @@ const Notes = (): h.JSX.Element => { const [sidebar, setSidebar] = useState(false); const [sidebarWidth, setSidebarWidth] = useState(undefined); const [toolbar, setToolbar] = useState(false); - const [theme, setTheme] = useState(undefined); - const [customTheme, setCustomTheme] = useState(""); // Notes const [notesProps, setNotesProps] = useState({ @@ -164,8 +160,6 @@ const Notes = (): h.JSX.Element => { setSidebar(local.sidebar); setSidebarWidth(local.sidebarWidth); setToolbar(local.toolbar); - setTheme(local.theme); - setCustomTheme(local.customTheme); // Notes const activeFromUrl = getActiveFromUrl(); @@ -209,14 +203,6 @@ const Notes = (): h.JSX.Element => { setSize(changes.size.newValue); } - if (changes.theme) { - setTheme(changes.theme.newValue); - } - - if (changes.customTheme) { - setCustomTheme(changes.customTheme.newValue); - } - if (changes.notes) { const oldNotes: NotesObject = changes.notes.oldValue; const newNotes: NotesObject = changes.notes.newValue; @@ -391,17 +377,6 @@ const Notes = (): h.JSX.Element => { document.body.style.left = sidebarWidth ?? ""; }, [sidebarWidth]); - // Theme - useEffect(() => { - // setThemeCore injects one of: - // - light.css - // - dark.css - // - customTheme string - if (theme) { - setThemeCore(document, { theme, customTheme }); - } - }, [theme, customTheme]); - // Focus useEffect(() => { document.body.classList.toggle("focus", focus); @@ -575,6 +550,10 @@ const Notes = (): h.JSX.Element => { ); } + if (!initialized) { + return null; + } + return ( {notification && ( diff --git a/src/options.tsx b/src/options.tsx index 5121b9b9..1e848d4e 100644 --- a/src/options.tsx +++ b/src/options.tsx @@ -20,7 +20,6 @@ import { Theme, Sync, } from "shared/storage/schema"; -import { setTheme as setThemeCore } from "themes/set-theme"; const Options = (): h.JSX.Element | null => { const [initialized, setInitialized] = useState(false); @@ -31,7 +30,6 @@ const Options = (): h.JSX.Element | null => { const [font, setFont] = useState(undefined); const [size, setSize] = useState(0); const [theme, setTheme] = useState(undefined); - const [customTheme, setCustomTheme] = useState(""); const [tab, setTab] = useState(false); const [tabSize, setTabSize] = useState(-1); const [openNoteOnMouseHover, setOpenNoteOnMouseHover] = useState(false); @@ -67,7 +65,6 @@ const Options = (): h.JSX.Element | null => { setFont(local.font); setSize(local.size); setTheme(local.theme); - setCustomTheme(local.customTheme); // Notes setNotesCount(Object.keys(local.notes).length); @@ -103,10 +100,6 @@ const Options = (): h.JSX.Element | null => { setTheme(changes.theme.newValue); } - if (changes.customTheme) { - setCustomTheme(changes.customTheme.newValue); - } - if (changes.notes) { const { newValue } = changes.notes; const newNotesCount = Object.keys(newValue).length; @@ -139,16 +132,6 @@ const Options = (): h.JSX.Element | null => { }); }, []); - useEffect(() => { - // setThemeCore injects one of: - // - light.css - // - dark.css - // - customTheme string - if (theme) { - setThemeCore(document, { theme, customTheme }); - } - }, [theme, customTheme]); - if (!initialized) { return null; } diff --git a/public/notes.html b/src/template.html similarity index 62% rename from public/notes.html rename to src/template.html index ad746884..40d3909c 100644 --- a/public/notes.html +++ b/src/template.html @@ -5,10 +5,14 @@ My Notes + - - + + + + + diff --git a/src/themes/__tests__/set-theme.test.ts b/src/themes/__tests__/set-theme.test.ts deleted file mode 100644 index 8c105967..00000000 --- a/src/themes/__tests__/set-theme.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -/* eslint-disable @typescript-eslint/naming-convention */ -import { JSDOM } from "jsdom"; -import { Theme } from "shared/storage/schema"; -import { setTheme } from "../set-theme"; - -const __expectOnlyOneChildInHead = (dom: JSDOM) => { - // only 1 theme should be inserted, changing theme should remove the previous theme - expect(dom.window.document.head.children.length).toBe(1); -}; - -const __expectInsertedLinkElement = (dom: JSDOM, expectedTheme: Theme) => { - __expectOnlyOneChildInHead(dom); - - const linkElement = dom.window.document.head.firstChild as HTMLLinkElement; - expect(linkElement.id).toBe("theme"); - expect(linkElement.rel).toBe("stylesheet"); - expect(linkElement.href).toBe(`themes/${expectedTheme}.css`); -}; - -const __expectInsertedStyleElement = (dom: JSDOM, expectedCustomTheme: string) => { - __expectOnlyOneChildInHead(dom); - - const styleElement = dom.window.document.head.firstChild as HTMLStyleElement; - expect(styleElement.id).toBe("theme"); - expect(styleElement.innerHTML).toBe(expectedCustomTheme); -}; - -const __expectUpdatedBody = (dom: JSDOM, expectedId: Theme) => { - expect(dom.window.document.body.id).toBe(expectedId); - expect(dom.window.document.body.style.opacity).toBe("1"); -}; - -test("light theme is inserted", () => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "light" }); - - __expectInsertedLinkElement(dom, "light"); - __expectUpdatedBody(dom, "light"); -}); - -test("dark theme is inserted", () => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "dark" }); - - __expectInsertedLinkElement(dom, "dark"); - __expectUpdatedBody(dom, "dark"); -}); - -test("custom theme is inserted", () => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "custom", customTheme: "body{color:#333;}" }); - - __expectInsertedStyleElement(dom, "body{color:#333;}"); - __expectUpdatedBody(dom, "custom"); -}); - -test("custom theme fallbacks to light theme if customTheme string is not provided", () => { - const customThemes = [undefined, "", " "]; // all should result in using light theme instead - customThemes.forEach((customTheme) => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "custom", customTheme }); - - __expectInsertedLinkElement(dom, "light"); - __expectUpdatedBody(dom, "light"); - }); -}); - -test("changing theme should remove the previous theme", () => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "light" }); // inserting light theme first - setTheme(dom.window.document, { theme: "dark" }); // then inserting dark theme - - __expectInsertedLinkElement(dom, "dark"); // already tests only 1 theme should be inserted - __expectUpdatedBody(dom, "dark"); -}); - -test("using the same theme again should insert the theme only once", () => { - const dom = new JSDOM(); - setTheme(dom.window.document, { theme: "light" }); - setTheme(dom.window.document, { theme: "light" }); - - __expectInsertedLinkElement(dom, "light"); // already tests only 1 theme should be inserted - __expectUpdatedBody(dom, "light"); -}); diff --git a/src/themes/__tests__/themes.test.ts b/src/themes/__tests__/themes.test.ts index 48f7136e..7238b9ec 100644 --- a/src/themes/__tests__/themes.test.ts +++ b/src/themes/__tests__/themes.test.ts @@ -5,21 +5,23 @@ const getLines = (filename: string): string[] => fs .readFileSync(path.join(__dirname, `../../../public/themes/${filename}`), "utf8") .split("\n"); -it("has the same CSS variables", () => { +describe("themes in public folder", () => { const lightLines = getLines("light.css"); const darkLines = getLines("dark.css"); - // Themes have the same length - expect(lightLines.length).toEqual(darkLines.length); + test("they have same number of lines", () => { + expect(lightLines.length).toEqual(darkLines.length); + }); - // Themes have the same lines, variable names - for (let i = 0; i < lightLines.length; i += 1) { - const isVariable = lightLines[i].startsWith(" --"); - if (!isVariable) { - expect(lightLines[i]).toEqual(darkLines[i]); - } else { - const variableName = lightLines[i].split(":")[0]; - expect(darkLines[i].startsWith(variableName)).toBe(true); + test("they have same CSS variables", () => { + for (let i = 0; i < lightLines.length; i += 1) { + const isVariable = lightLines[i].startsWith(" --"); + if (!isVariable) { + expect(lightLines[i]).toEqual(darkLines[i]); + } else { + const variableName = lightLines[i].split(":")[0]; + expect(darkLines[i].startsWith(variableName)).toBe(true); + } } - } + }); }); diff --git a/src/themes/custom/custom.tsx b/src/themes/custom/custom.tsx index 5f5a1bb7..f301c9be 100644 --- a/src/themes/custom/custom.tsx +++ b/src/themes/custom/custom.tsx @@ -2,7 +2,9 @@ import { Fragment, h, render } from "preact"; import { useEffect, useState } from "preact/hooks"; import { t, tString } from "i18n"; -const placeholder = `:root { +const placeholder = `@import "themes/light.css"; + +:root { }`; diff --git a/src/themes/init.ts b/src/themes/init.ts new file mode 100644 index 00000000..e0f7ffa5 --- /dev/null +++ b/src/themes/init.ts @@ -0,0 +1,51 @@ +import { Storage } from "shared/storage/schema"; + +let theme = localStorage.getItem("theme") as Storage["theme"] | null; +let customTheme = localStorage.getItem("customTheme") as Storage["customTheme"] | null; + +const init = () => { + if (theme === null || customTheme === null || !["light", "dark", "custom"].includes(theme)) { + return false; + } + + document.getElementById("theme")!.innerHTML = theme === "custom" + ? customTheme.trim() + : `@import "themes/${theme}.css`; + + document.documentElement.id = theme; + return true; +}; + +const updateTheme = (aTheme: Storage["theme"]) => { + theme = aTheme; + localStorage.setItem("theme", aTheme); +}; + +const updateCustomTheme = (aCustomTheme: Storage["customTheme"]) => { + customTheme = aCustomTheme; + localStorage.setItem("customTheme", aCustomTheme); +}; + +if (!init()) { + chrome.storage.local.get(["theme", "customTheme"], (items) => { + updateTheme(items.theme); + updateCustomTheme(items.customTheme); + init(); + }); +} + +chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== "local") { + return; + } + + if (changes.theme) { + updateTheme(changes.theme.newValue); + init(); + } + + if (changes.customTheme) { + updateCustomTheme(changes.customTheme.newValue); + init(); + } +}); diff --git a/src/themes/set-theme.ts b/src/themes/set-theme.ts deleted file mode 100644 index 1166c94f..00000000 --- a/src/themes/set-theme.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Theme } from "shared/storage/schema"; - -const reset = (document: Document) => { - const elem = document.getElementById("theme"); - if (elem) { - elem.remove(); - } -}; - -const appendTheme = ( - document: Document, - element: HTMLLinkElement | HTMLStyleElement, - theme: Theme, -) => { - document.head.appendChild(element); - document.body.id = theme; // eslint-disable-line no-param-reassign - document.body.style.opacity = "1"; // eslint-disable-line no-param-reassign -}; - -const insertTheme = (document: Document, theme: Theme) => { - const link = document.createElement("link"); - link.id = "theme"; - link.rel = "stylesheet"; - link.href = `themes/${theme}.css`; - appendTheme(document, link, theme); -}; - -const insertCustomTheme = (document: Document, customTheme: string) => { - const style = document.createElement("style"); - style.id = "theme"; - style.innerHTML = customTheme; - document.getElementsByTagName("head")[0].appendChild(style); - appendTheme(document, style, "custom"); -}; - -export interface SetThemeOptions { - theme: Theme - customTheme?: string -} - -export function setTheme(document: Document, { theme, customTheme }: SetThemeOptions): void { - reset(document); - - if (theme === "light" || theme === "dark") { - insertTheme(document, theme); - } - - if (theme === "custom") { - if (customTheme && customTheme.trim().length > 0) { - insertCustomTheme(document, customTheme); - } else { - insertTheme(document, "light"); - } - } -}