Merge pull request #441 from StroepWafel/main

fix: TOTM
This commit is contained in:
Aden Lindsay
2026-06-01 18:37:59 +09:30
committed by GitHub
7 changed files with 273 additions and 53 deletions
+72
View File
@@ -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<void> {
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<void> {
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);
}
});
+129 -23
View File
@@ -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 {
@@ -423,6 +423,18 @@
{/each}
{/if}
</div>
{#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}
</div>
{/each}
@@ -45,10 +45,10 @@ export async function fetchThemeOfTheMonth(): Promise<ThemeOfTheMonthEntry | nul
}
}
/** True when we have a new monthly entry the user hasn't dismissed yet. */
/** True when the current month's entry should appear in the startup queue. */
export function shouldShowThemeOfTheMonth(entry: ThemeOfTheMonthEntry | null): boolean {
if (!entry || settingsState.themeOfTheMonthDisabled) return false;
return settingsState.themeOfTheMonthLastSeenId !== entry.id;
return settingsState.themeOfTheMonthDismissedMonth !== entry.month;
}
function escapeHTML(str: string): string {
@@ -108,18 +108,9 @@ async function resolvePopupHeroImageUrl(entry: ThemeOfTheMonthEntry): Promise<st
return fallback || null;
}
function closeThemeOfTheMonthCard(
card: HTMLElement,
onDismissed?: () => 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 */ `
<aside id="theme-of-the-month-card" class="themeOfTheMonthCard" role="dialog" aria-label="Theme of the Month">
<button type="button" class="themeOfTheMonthCardClose" aria-label="Close Theme of the Month">×</button>
${
heroUrl
? `<img class="themeOfTheMonthCardImage" src="${escapeHTML(heroUrl)}" alt="${escapeHTML(entry.title)}" />`
@@ -154,39 +144,74 @@ export async function OpenThemeOfTheMonthPopup(
<h2>${escapeHTML(entry.title)}</h2>
<p class="themeOfTheMonthCardDescription">${description}</p>
<div class="themeOfTheMonthCardActions">
${
linkedThemeId
? `<button type="button" class="themeOfTheMonthCardPrimary">Open Store</button>`
: ""
}
<button type="button" class="themeOfTheMonthCardSecondary">Don't show again</button>
<div class="themeOfTheMonthCardActionsStart">
${
linkedThemeId
? `<button type="button" class="themeOfTheMonthCardPrimary">Open Store</button>`
: ""
}
</div>
<div class="themeOfTheMonthCardActionsEnd">
<button type="button" class="themeOfTheMonthCardSecondary">Close</button>
<button type="button" class="themeOfTheMonthCardDontShow">Don't show again</button>
</div>
</div>
</div>
<div class="themeOfTheMonthCardConfirm" hidden>
<div class="themeOfTheMonthCardConfirmInner">
<h3>Don't show again?</h3>
<p>Theme of the Month popups will be turned off. You can turn them back on in BetterSEQTA+ settings.</p>
<div class="themeOfTheMonthCardConfirmActions">
<button type="button" class="themeOfTheMonthCardConfirmCancel">Cancel</button>
<button type="button" class="themeOfTheMonthCardConfirmAccept">Don't show again</button>
</div>
</div>
</div>
</aside>
`).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<HTMLElement>(".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<void> {
const entry = await fetchThemeOfTheMonth();
@@ -207,7 +232,7 @@ export async function showThemeOfTheMonthPopupNow(): Promise<void> {
return;
}
settingsState.themeOfTheMonthLastSeenId = undefined;
settingsState.themeOfTheMonthDismissedMonth = undefined;
if (document.getElementById("whatsnewbk")) {
await closePopup();
+1 -1
View File
@@ -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() {
+6 -1
View File
@@ -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;