From eb49e8d7f12ccea2b8ea9a88ce3edf7ebbbd21c8 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Mon, 25 May 2026 13:26:33 +0930 Subject: [PATCH 01/11] fix up TOTM --- src/background.ts | 36 +++++++ src/css/injected.scss | 94 +++++++++++++++++-- src/interface/pages/settings/general.svelte | 11 +++ .../utils/Openers/OpenThemeOfTheMonthPopup.ts | 37 +++++++- 4 files changed, 166 insertions(+), 12 deletions(-) diff --git a/src/background.ts b/src/background.ts index b98b5514..c25caa9b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -551,6 +551,41 @@ async function migrateGlobalSearchDefaultsFor365Upgrade( } } +/** One-time reset for 3.6.6: re-enable Theme of the Month for existing users. */ +const THEME_OF_THE_MONTH_RESET_VERSION = "3.6.6"; + +async function resetThemeOfTheMonthDisabledFor366Upgrade( + previousVersion: string, +): Promise { + try { + const currRaw = browser.runtime.getManifest().version; + const prev = semver.coerce(previousVersion); + const curr = semver.coerce(currRaw); + if ( + prev == null || + curr == null || + semver.lt(curr, THEME_OF_THE_MONTH_RESET_VERSION) || + !semver.lt(prev, THEME_OF_THE_MONTH_RESET_VERSION) + ) { + return; + } + + await browser.storage.local.set({ + themeOfTheMonthDisabled: false, + themeOfTheMonthLastSeenId: undefined, + }); + + console.info( + `[BetterSEQTA+] Migration ${THEME_OF_THE_MONTH_RESET_VERSION}: Theme of the Month re-enabled (from ${previousVersion}).`, + ); + } catch (e) { + console.warn( + "[BetterSEQTA+] Theme of the Month 3.6.6 reset migration failed:", + e, + ); + } +} + browser.runtime.onInstalled.addListener(function (event) { browser.storage.local.remove(["justupdated"]); browser.storage.local.remove(["data"]); @@ -561,6 +596,7 @@ browser.runtime.onInstalled.addListener(function (event) { if (event.reason === "update" && event.previousVersion) { void migrateGlobalSearchDefaultsFor365Upgrade(event.previousVersion); + void resetThemeOfTheMonthDisabledFor366Upgrade(event.previousVersion); } }); diff --git a/src/css/injected.scss b/src/css/injected.scss index 57f7fc64..22d0f211 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3753,20 +3753,98 @@ div.day-empty { pointer-events: none; animation: themeOfTheMonthCardOut 0.18s ease-in forwards; } -.themeOfTheMonthCardClose { +.themeOfTheMonthCardDisable { position: absolute !important; - top: 4px !important; - right: 4px !important; - z-index: 2; - width: 32px; - height: 32px; + top: 6px !important; + right: 6px !important; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0 !important; border: 1px solid rgba(255, 255, 255, 0.22); - border-radius: 16px !important; + border-radius: 9px !important; background: rgba(0, 0, 0, 0.42); color: white; cursor: pointer; - font-size: 1.35rem; + font-size: 0.78rem; line-height: 1; + opacity: 0.75; + transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease; +} +.themeOfTheMonthCardDisable:hover { + opacity: 1; + transform: scale(1.08); + background: rgba(0, 0, 0, 0.6); +} +.themeOfTheMonthCardConfirm { + position: absolute; + inset: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + border-radius: inherit; + background: color-mix(in srgb, var(--background-primary) 88%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + opacity: 0; + transition: opacity 0.16s ease; +} +.themeOfTheMonthCardConfirmVisible { + opacity: 1; +} +.themeOfTheMonthCardConfirmInner { + width: 100%; + text-align: center; +} +.themeOfTheMonthCardConfirmInner h3 { + margin: 0 0 6px; + font-size: 1rem; + line-height: 1.2; +} +.themeOfTheMonthCardConfirmInner p { + margin: 0 0 14px; + font-size: 0.86rem; + line-height: 1.4; + color: color-mix(in srgb, var(--text-primary) 78%, transparent); +} +.themeOfTheMonthCardConfirmActions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} +.themeOfTheMonthCardConfirmCancel, +.themeOfTheMonthCardConfirmAccept { + appearance: none; + border: none; + cursor: pointer; + border-radius: 9999px; + padding: 0.5rem 0.85rem; + font-size: 0.84rem; + font-weight: 700; + transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease; +} +.themeOfTheMonthCardConfirmCancel { + background: color-mix(in srgb, var(--text-primary) 10%, transparent); + color: var(--text-primary); +} +.themeOfTheMonthCardConfirmAccept { + background: var(--better-pri, #6366f1); + color: white; +} +.themeOfTheMonthCardConfirmCancel:hover, +.themeOfTheMonthCardConfirmAccept:hover { + filter: brightness(1.08); + transform: translateY(-1px); +} +.themeOfTheMonthCardConfirmCancel:active, +.themeOfTheMonthCardConfirmAccept:active { + transform: translateY(0); } .themeOfTheMonthCardImage { display: block; diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 7bc7ecc9..1bb59313 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -458,6 +458,17 @@ } })} + {@render Setting({ + title: "Theme of the Month", + description: "Show the monthly featured theme popup when a new entry is available", + id: 15, + Component: Switch, + props: { + state: !($settingsState.themeOfTheMonthDisabled ?? false), + onChange: (isOn: boolean) => settingsState.themeOfTheMonthDisabled = !isOn + } + })} + {#if $settingsState.devMode}
diff --git a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts index aaf6348a..c6a609ac 100644 --- a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts +++ b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts @@ -143,7 +143,7 @@ export async function OpenThemeOfTheMonthPopup( const card = stringToHTML(/* html */ `
+
+ @@ -177,7 +187,9 @@ export async function OpenThemeOfTheMonthPopup( card.addEventListener("mouseenter", () => window.clearTimeout(autoCloseTimeout), { once: true }); - card.querySelector(".themeOfTheMonthCardClose")?.addEventListener("click", () => { + const confirmEl = card.querySelector(".themeOfTheMonthCardConfirm"); + + card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => { dismiss(); }); @@ -186,7 +198,24 @@ export async function OpenThemeOfTheMonthPopup( openThemeStoreWithHighlight(linkedThemeId!); }); - card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => { + card.querySelector(".themeOfTheMonthCardDisable")?.addEventListener("click", () => { + window.clearTimeout(autoCloseTimeout); + if (confirmEl) { + confirmEl.hidden = false; + // allow CSS transition by toggling on next frame + requestAnimationFrame(() => confirmEl.classList.add("themeOfTheMonthCardConfirmVisible")); + } + }); + + card.querySelector(".themeOfTheMonthCardConfirmCancel")?.addEventListener("click", () => { + if (!confirmEl) return; + confirmEl.classList.remove("themeOfTheMonthCardConfirmVisible"); + window.setTimeout(() => { + confirmEl.hidden = true; + }, 160); + }); + + card.querySelector(".themeOfTheMonthCardConfirmAccept")?.addEventListener("click", () => { settingsState.themeOfTheMonthDisabled = true; dismiss(); }); From 7a3cbb50cc50437cbb0f4c84ddebee8e33c6dc5e Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Mon, 25 May 2026 13:49:46 +0930 Subject: [PATCH 02/11] fix style --- src/css/injected.scss | 40 ++++++++++++++------- src/interface/pages/settings/general.svelte | 23 ++++++------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/css/injected.scss b/src/css/injected.scss index 22d0f211..00d7f356 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3757,27 +3757,36 @@ div.day-empty { position: absolute !important; top: 6px !important; right: 6px !important; - z-index: 3; - display: flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; + z-index: 3 !important; + box-sizing: border-box !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 18px !important; + height: 18px !important; + min-width: 0 !important; + min-height: 0 !important; + max-width: 18px !important; + max-height: 18px !important; + margin: 0 !important; padding: 0 !important; - border: 1px solid rgba(255, 255, 255, 0.22); + border: 1px solid rgba(255, 255, 255, 0.22) !important; border-radius: 9px !important; - background: rgba(0, 0, 0, 0.42); - color: white; - cursor: pointer; - font-size: 0.78rem; - line-height: 1; + background: rgba(0, 0, 0, 0.42) !important; + color: white !important; + cursor: pointer !important; + font-family: inherit !important; + font-size: 12px !important; + font-weight: 600 !important; + line-height: 1 !important; + text-align: center !important; opacity: 0.75; transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease; } .themeOfTheMonthCardDisable:hover { opacity: 1; transform: scale(1.08); - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.6) !important; } .themeOfTheMonthCardConfirm { position: absolute; @@ -3792,10 +3801,15 @@ div.day-empty { backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); opacity: 0; + pointer-events: none; transition: opacity 0.16s ease; } +.themeOfTheMonthCardConfirm[hidden] { + display: none; +} .themeOfTheMonthCardConfirmVisible { opacity: 1; + pointer-events: auto; } .themeOfTheMonthCardConfirmInner { width: 100%; diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 1bb59313..6140c02f 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -423,6 +423,18 @@ {/each} {/if} + {#if plugin.pluginId === 'global-search'} + {@render Setting({ + title: "Theme of the Month", + description: "Show the monthly featured theme popup when a new entry is available", + id: 15, + Component: Switch, + props: { + state: !($settingsState.themeOfTheMonthDisabled ?? false), + onChange: (isOn: boolean) => settingsState.themeOfTheMonthDisabled = !isOn + } + })} + {/if} {/each} @@ -458,17 +470,6 @@ } })} - {@render Setting({ - title: "Theme of the Month", - description: "Show the monthly featured theme popup when a new entry is available", - id: 15, - Component: Switch, - props: { - state: !($settingsState.themeOfTheMonthDisabled ?? false), - onChange: (isOn: boolean) => settingsState.themeOfTheMonthDisabled = !isOn - } - })} - {#if $settingsState.devMode}
From e8d1dadfa72d97debc9dfa8c98f09721fbb07a3e Mon Sep 17 00:00:00 2001 From: StroepWafel <109832156+StroepWafel@users.noreply.github.com> Date: Thu, 28 May 2026 15:16:43 +0930 Subject: [PATCH 03/11] Update OpenWhatsNewPopup.ts --- src/seqta/utils/Openers/OpenWhatsNewPopup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts index 8b068f05..b066d886 100644 --- a/src/seqta/utils/Openers/OpenWhatsNewPopup.ts +++ b/src/seqta/utils/Openers/OpenWhatsNewPopup.ts @@ -34,7 +34,7 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) { const text = stringToHTML(/* html */ `
-

3.6.6 – Global Search improvements!

+

3.7.0 – Global Search improvements!

  • Tuned hybrid search and indexing reliability.
  • Clearer progress UI and green “Done!” when a pass finishes.
  • Merged duplicate course hits that opened the same page.
  • From 9f263c8c029d1bbd89339cb7f0febfb7316a579c Mon Sep 17 00:00:00 2001 From: StroepWafel <109832156+StroepWafel@users.noreply.github.com> Date: Thu, 28 May 2026 15:17:38 +0930 Subject: [PATCH 04/11] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f69f0865..9504b2fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterseqtaplus", - "version": "3.6.5", + "version": "3.7.0", "type": "module", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!", "browserslist": "> 0.5%, last 2 versions, not dead", From 3d1320277950a27ed38efc428898ad1f52360d3c Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Fri, 29 May 2026 00:44:02 +0930 Subject: [PATCH 05/11] feat: handlers for night city theme's features --- src/plugins/built-in/themes/theme-manager.ts | 74 +- src/plugins/built-in/themes/theme-runtime.ts | 714 +++++++++++++++++++ src/types/CustomThemes.ts | 12 + 3 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 src/plugins/built-in/themes/theme-runtime.ts diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index ccaeddf5..3ab5d46d 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -17,6 +17,15 @@ import { clearCustomThemeAdaptiveCssVariables, setCustomThemeAdaptiveCssVariables, } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; +import { + clearThemeRuntime, + injectThemeDom, + runThemeScript, + validateThemeDom, + validateThemeScript, + type ThemeDomSpec, + type ThemeScriptSpec, +} from "./theme-runtime"; type ThemeContent = { id: string; @@ -31,6 +40,8 @@ type ThemeContent = { forceDark?: boolean; adaptiveCssVariables?: string[]; images?: { id: string; variableName: string; data: string }[]; // data: base64 + themeScript?: ThemeScriptSpec; + themeDom?: ThemeDomSpec; }; export type InstallThemeMeta = { @@ -53,6 +64,7 @@ export class ThemeManager { private imageUrlCache: Map = new Map(); private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 }; private storeUpdateCheckRunning = false; + private headObserver: MutationObserver | null = null; private constructor() { console.debug("[ThemeManager] Initializing..."); @@ -307,6 +319,13 @@ export class ThemeManager { private async applyTheme(theme: CustomTheme): Promise { console.debug("[ThemeManager] Applying theme:", theme.name); try { + // Run the theme script BEFORE injecting CustomCSS so any state the + // script publishes (e.g. `data-city-state` and `--city-sky-color` for + // Noir City) is already on when the new CSS rules paint. + // Otherwise the CSS lands with var() unresolved and the page flashes + // its previous state before snapping to the right colour. + runThemeScript(theme.themeScript); + // Apply custom CSS if (theme.CustomCSS) { console.debug("[ThemeManager] Applying custom CSS"); @@ -348,6 +367,8 @@ export class ThemeManager { } setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []); + + injectThemeDom(theme.themeDom); } catch (error) { console.error("[ThemeManager] Error applying theme:", error); } @@ -379,6 +400,13 @@ export class ThemeManager { ): Promise { console.debug("[ThemeManager] Removing theme:", theme.name); try { + clearThemeRuntime(); + + // Disconnect the head observer BEFORE removing the style element, + // otherwise the removal fires the observer and it would no-op only + // because the style is already gone — wasted work, but harmless. + this.disconnectStyleObserver(); + // Remove custom CSS if (this.styleElement) { console.debug("[ThemeManager] Removing custom CSS"); @@ -448,7 +476,13 @@ export class ThemeManager { } /** - * Apply custom CSS to the document + * Apply custom CSS to the document. The `