diff --git a/package.json b/package.json index f69f0865..f03780c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterseqtaplus", - "version": "3.6.5", + "version": "3.6.6", "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", diff --git a/src/background.ts b/src/background.ts index b98b5514..074a4138 100644 --- a/src/background.ts +++ b/src/background.ts @@ -551,6 +551,76 @@ 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, + ); + } +} + +/** 3.7.0: Close no longer marks entries seen — clear legacy dismissal keys. */ +const THEME_OF_THE_MONTH_RELOAD_VERSION = "3.7.0"; + +async function resetThemeOfTheMonthDismissalFor370Upgrade( + 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_RELOAD_VERSION) || + !semver.lt(prev, THEME_OF_THE_MONTH_RELOAD_VERSION) + ) { + return; + } + + await browser.storage.local.set({ + themeOfTheMonthLastSeenId: undefined, + themeOfTheMonthDismissedMonth: undefined, + }); + + console.info( + `[BetterSEQTA+] Migration ${THEME_OF_THE_MONTH_RELOAD_VERSION}: Theme of the Month shows again until dismissed for the month (from ${previousVersion}).`, + ); + } catch (e) { + console.warn( + "[BetterSEQTA+] Theme of the Month 3.7.0 dismissal migration failed:", + e, + ); + } +} + browser.runtime.onInstalled.addListener(function (event) { browser.storage.local.remove(["justupdated"]); browser.storage.local.remove(["data"]); @@ -561,6 +631,8 @@ browser.runtime.onInstalled.addListener(function (event) { if (event.reason === "update" && event.previousVersion) { void migrateGlobalSearchDefaultsFor365Upgrade(event.previousVersion); + void resetThemeOfTheMonthDisabledFor366Upgrade(event.previousVersion); + void resetThemeOfTheMonthDismissalFor370Upgrade(event.previousVersion); } }); diff --git a/src/css/injected.scss b/src/css/injected.scss index c65b093e..a1580330 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3809,20 +3809,77 @@ div.day-empty { pointer-events: none; animation: themeOfTheMonthCardOut 0.18s ease-in forwards; } -.themeOfTheMonthCardClose { - position: absolute !important; - top: 4px !important; - right: 4px !important; - z-index: 2; - width: 32px; - height: 32px; - border: 1px solid rgba(255, 255, 255, 0.22); - border-radius: 16px !important; - background: rgba(0, 0, 0, 0.42); - color: white; +.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; + pointer-events: none; + transition: opacity 0.16s ease; +} +.themeOfTheMonthCardConfirm[hidden] { + display: none; +} +.themeOfTheMonthCardConfirmVisible { + opacity: 1; + pointer-events: auto; +} +.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; - font-size: 1.35rem; - line-height: 1; + 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; @@ -3861,11 +3918,34 @@ div.day-empty { .themeOfTheMonthCardActions { display: flex; flex-wrap: wrap; - justify-content: flex-end; - gap: 8px; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.themeOfTheMonthCardActionsStart { + display: flex; + flex-wrap: wrap; + align-items: center; +} +.themeOfTheMonthCardActionsEnd { + display: inline-flex; + flex-wrap: nowrap; + align-items: stretch; + margin-left: auto; + padding: 3px; + gap: 0; + overflow: hidden; + border-radius: 9999px; + background: color-mix( + in srgb, + var(--background-secondary, var(--text-primary)) 28%, + var(--background-primary) + ); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary) 8%, transparent); } .themeOfTheMonthCardPrimary, -.themeOfTheMonthCardSecondary { +.themeOfTheMonthCardSecondary, +.themeOfTheMonthCardDontShow { appearance: none; border: none; cursor: pointer; @@ -3873,23 +3953,49 @@ div.day-empty { padding: 0.58rem 0.9rem; font-size: 0.86rem; font-weight: 700; - transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease; + transition: background 0.15s ease, color 0.15s ease; } .themeOfTheMonthCardPrimary { background: var(--better-pri, #6366f1); color: white; } -.themeOfTheMonthCardSecondary { - background: color-mix(in srgb, var(--text-primary) 10%, transparent); +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow { + padding: 0.5rem 0.8rem; + font-size: 0.8rem; + font-weight: 600; + border: none !important; + border-radius: 9999px !important; + background: transparent !important; + box-shadow: none !important; + filter: none !important; + transform: none !important; +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary { color: var(--text-primary); } -.themeOfTheMonthCardPrimary:hover, -.themeOfTheMonthCardSecondary:hover { +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow { + color: color-mix(in srgb, var(--text-primary) 58%, transparent); +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:hover, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:focus-visible, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:hover, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:focus-visible { + background: color-mix(in srgb, var(--text-primary) 10%, transparent) !important; + border-radius: 9999px !important; + filter: none !important; + transform: none !important; +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:active, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:active { + background: color-mix(in srgb, var(--text-primary) 14%, transparent) !important; + border-radius: 9999px !important; +} +.themeOfTheMonthCardPrimary:hover { filter: brightness(1.08); transform: translateY(-1px); } -.themeOfTheMonthCardPrimary:active, -.themeOfTheMonthCardSecondary:active { +.themeOfTheMonthCardPrimary:active { transform: translateY(0); } @keyframes themeOfTheMonthCardIn { diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 7bc7ecc9..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} diff --git a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts index aaf6348a..c2729db3 100644 --- a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts +++ b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts @@ -45,10 +45,10 @@ export async function fetchThemeOfTheMonth(): Promise void, - markSeen = true, -) { +function closeThemeOfTheMonthCard(card: HTMLElement, onDismissed?: () => void) { if (card.classList.contains("themeOfTheMonthCardClosing")) return; - if (markSeen) { - const entryId = card.dataset.entryId; - if (entryId) settingsState.themeOfTheMonthLastSeenId = entryId; - } - card.classList.add("themeOfTheMonthCardClosing"); window.setTimeout(() => { card.remove(); @@ -143,7 +134,6 @@ export async function OpenThemeOfTheMonthPopup( const card = stringToHTML(/* html */ ` `).firstChild as HTMLElement; - card.dataset.entryId = entry.id; const autoCloseTimeout = window.setTimeout(() => { closeThemeOfTheMonthCard(card, onDismissed); - }, 12000); + }, 30_000); - const dismiss = (markSeen = true) => { + const dismiss = () => { window.clearTimeout(autoCloseTimeout); - closeThemeOfTheMonthCard(card, onDismissed, markSeen); + closeThemeOfTheMonthCard(card, onDismissed); }; card.addEventListener("mouseenter", () => window.clearTimeout(autoCloseTimeout), { once: true }); - card.querySelector(".themeOfTheMonthCardClose")?.addEventListener("click", () => { + const confirmEl = card.querySelector(".themeOfTheMonthCardConfirm"); + + card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => { + settingsState.themeOfTheMonthDismissedMonth = entry.month; dismiss(); }); card.querySelector(".themeOfTheMonthCardPrimary")?.addEventListener("click", () => { + settingsState.themeOfTheMonthDismissedMonth = entry.month; dismiss(); openThemeStoreWithHighlight(linkedThemeId!); }); - card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => { + const openDontShowConfirm = () => { + window.clearTimeout(autoCloseTimeout); + if (!confirmEl) return; + confirmEl.hidden = false; + requestAnimationFrame(() => confirmEl.classList.add("themeOfTheMonthCardConfirmVisible")); + }; + + card.querySelector(".themeOfTheMonthCardDontShow")?.addEventListener("click", openDontShowConfirm); + + 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(); }); @@ -196,7 +221,7 @@ export async function OpenThemeOfTheMonthPopup( /** * Dev helper: fetch the current month's entry and show the popup immediately, - * even if the user has already dismissed it this month. + * even if the user dismissed it for this calendar month. */ export async function showThemeOfTheMonthPopupNow(): Promise { const entry = await fetchThemeOfTheMonth(); @@ -207,7 +232,7 @@ export async function showThemeOfTheMonthPopupNow(): Promise { return; } - settingsState.themeOfTheMonthLastSeenId = undefined; + settingsState.themeOfTheMonthDismissedMonth = undefined; if (document.getElementById("whatsnewbk")) { await closePopup(); diff --git a/src/seqta/utils/Openers/StartupPopupQueue.ts b/src/seqta/utils/Openers/StartupPopupQueue.ts index 391c4a68..de10ee8b 100644 --- a/src/seqta/utils/Openers/StartupPopupQueue.ts +++ b/src/seqta/utils/Openers/StartupPopupQueue.ts @@ -15,7 +15,7 @@ type QueueStep = (goNext: () => void) => void; /** * Runs startup modals in order: What's New (if the extension just updated), - * Theme of the Month (when a new monthly entry hasn't been seen), then shows + * Theme of the Month (when the user hasn't dismissed this calendar month), then shows * the SEQTA Engage toast (once, non-blocking). */ export async function runStartupPopupQueue() { diff --git a/src/types/storage.ts b/src/types/storage.ts index d859d5f5..9ee34640 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -36,7 +36,12 @@ export interface SettingsState { engageParentsAnnouncementShown?: boolean; /** One-time announcement: BS Cloud automatic settings sync (last in startup popup queue). */ bsCloudAutoSyncAnnouncementShown?: boolean; - /** ID of the last Theme of the Month entry shown to the user (shows once per new entry). */ + /** + * Calendar month (`YYYY-MM`) for which the user closed the Theme of the Month popup. + * Cleared automatically when a new month's entry is fetched (different `month`). + */ + themeOfTheMonthDismissedMonth?: string; + /** @deprecated Migrated away; no longer read. */ themeOfTheMonthLastSeenId?: string; /** Permanently disables Theme of the Month startup prompts. */ themeOfTheMonthDisabled?: boolean;