mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
feat: background resuming for calendar syncing
This commit is contained in:
@@ -3,7 +3,6 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
|
||||
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
|
||||
import { getFontPreset } from "@/seqta/ui/fonts/presets";
|
||||
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
||||
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||
import hostStyles from "./calendarSyncHost.css?inline";
|
||||
|
||||
@@ -152,7 +151,6 @@ export async function mountGoogleCalendarButton(): Promise<void> {
|
||||
if (!toolbar) return;
|
||||
|
||||
ensureHostStyles();
|
||||
registerCalendarContentHandlers();
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = CONTROLS_CLASS;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import { verboseLog } from "@/utils/verboseLog";
|
||||
import { mountGoogleCalendarButton, unmountGoogleCalendarButton } from "./calendarSyncUi";
|
||||
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
||||
|
||||
const timetablePlugin: Plugin<{}, {}> = {
|
||||
id: "timetable",
|
||||
@@ -15,6 +16,8 @@ const timetablePlugin: Plugin<{}, {}> = {
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
registerCalendarContentHandlers();
|
||||
|
||||
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "@/seqta/utils/calendarSync/providerStorage";
|
||||
|
||||
export const BSPLUS_CALENDAR_SYNC_SETTINGS_KEY = "bsplus_calendar_sync_settings";
|
||||
/** Device-local interrupted sync marker — never cloud-synced. */
|
||||
export const BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY = "bsplus_calendar_sync_in_progress";
|
||||
export const CALENDAR_WEEKLY_ALARM = "bsplus_calendar_weekly";
|
||||
export const WEEKLY_SYNC_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -98,3 +100,34 @@ export async function markWeeklySyncComplete(): Promise<void> {
|
||||
export async function markWeeklySyncPending(): Promise<void> {
|
||||
await writeSharedCalendarSyncSettings({ pendingWeeklySync: true });
|
||||
}
|
||||
|
||||
type PendingCalendarSync = {
|
||||
provider: "google" | "outlook";
|
||||
mode: "full" | "incremental";
|
||||
origin: string;
|
||||
startedAt: number;
|
||||
};
|
||||
|
||||
/** Returns a pending sync worth resuming, or clears and returns null. */
|
||||
export async function readResumableCalendarSync(): Promise<PendingCalendarSync | null> {
|
||||
const got = await browser.storage.local.get(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
|
||||
const raw = got[BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY];
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
|
||||
const pending = raw as PendingCalendarSync;
|
||||
const validProvider = pending.provider === "google" || pending.provider === "outlook";
|
||||
const validMode = pending.mode === "full" || pending.mode === "incremental";
|
||||
const fresh = Date.now() - pending.startedAt < 24 * 60 * 60 * 1000;
|
||||
|
||||
if (
|
||||
!validProvider ||
|
||||
!validMode ||
|
||||
pending.origin !== location.origin ||
|
||||
!fresh
|
||||
) {
|
||||
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ import {
|
||||
} from "@/seqta/utils/calendarSync/syncEngine";
|
||||
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
|
||||
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
||||
import {
|
||||
readGoogleCalendarState,
|
||||
readOutlookCalendarState,
|
||||
} from "@/seqta/utils/calendarSync/providerStorage";
|
||||
import {
|
||||
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
|
||||
getSyncWeeksAhead,
|
||||
readResumableCalendarSync,
|
||||
} from "@/seqta/utils/calendarSync/settings";
|
||||
import type {
|
||||
GoogleCalendarStatus,
|
||||
GoogleCalendarSyncProgress,
|
||||
@@ -98,6 +106,16 @@ export async function runCalendarSync(
|
||||
const mode = params.mode ?? "full";
|
||||
const weeksAhead = await getSyncWeeksAhead();
|
||||
|
||||
await browser.storage.local.set({
|
||||
[BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY]: {
|
||||
provider: config.provider,
|
||||
mode,
|
||||
origin: location.origin,
|
||||
startedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
reportSyncProgress(params.onProgress, {
|
||||
phase: "preparing",
|
||||
current: 0,
|
||||
@@ -110,12 +128,34 @@ export async function runCalendarSync(
|
||||
? await fetchTimetableLessons(trailingWeekRange(weeksAhead))
|
||||
: await fetchTimetableForSync(weeksAhead);
|
||||
|
||||
return syncLessonsToCalendar(
|
||||
return await syncLessonsToCalendar(
|
||||
config.lessonSyncProvider,
|
||||
{ origin: location.origin, lessons, mode, weeksAhead },
|
||||
() => getCalendarAccessToken(config.provider),
|
||||
{ onProgress: params.onProgress },
|
||||
);
|
||||
} finally {
|
||||
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resumeInterruptedCalendarSync(
|
||||
params: RunCalendarSyncParams = {},
|
||||
): Promise<GoogleCalendarSyncResult | null> {
|
||||
const pending = await readResumableCalendarSync();
|
||||
if (!pending) return null;
|
||||
|
||||
const state =
|
||||
pending.provider === "google"
|
||||
? await readGoogleCalendarState()
|
||||
: await readOutlookCalendarState();
|
||||
if (!state.refreshToken && !state.accessToken) {
|
||||
await browser.storage.local.remove(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = pending.provider === "google" ? GOOGLE_SYNC_RUNNER : OUTLOOK_SYNC_RUNNER;
|
||||
return runCalendarSync(config, { ...params, mode: pending.mode });
|
||||
}
|
||||
|
||||
export const runGoogleCalendarSync = (params?: RunCalendarSyncParams) =>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
normalizeThemeIdForSync,
|
||||
resolveThemeIdForPostSyncDownload,
|
||||
} from "./cloudSettingsSync";
|
||||
import { BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY } from "@/seqta/utils/calendarSync/settings";
|
||||
|
||||
describe("migrateLegacyToPluginSettings", () => {
|
||||
it("maps animatedbk without overwriting existing plugin fields", () => {
|
||||
@@ -30,6 +31,7 @@ describe("isKeyIncludedInCloudUploadPayload", () => {
|
||||
it("excludes auth and device cache prefixes", () => {
|
||||
expect(isKeyIncludedInCloudUploadPayload("bsplus_token")).toBe(false);
|
||||
expect(isKeyIncludedInCloudUploadPayload("bsplus_install_id")).toBe(false);
|
||||
expect(isKeyIncludedInCloudUploadPayload(BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY)).toBe(false);
|
||||
expect(isKeyIncludedInCloudUploadPayload("plugin.global-search.storage.index")).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import isEqual from "@/seqta/utils/isEqual";
|
||||
import { BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY } from "@/seqta/utils/calendarSync/settings";
|
||||
|
||||
/** Matches the contract in docs/CLOUD_SETTINGS_SYNC_SERVER.md */
|
||||
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
|
||||
@@ -42,6 +43,7 @@ export const KEYS_OMITTED_FROM_CLOUD_UPLOAD = [
|
||||
"bsplus_install_id",
|
||||
/** Pending feedback ids awaiting a reply notification — device-local. */
|
||||
"bsplus_pending_feedback_ids",
|
||||
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -64,6 +66,7 @@ const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
|
||||
BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY,
|
||||
"bsplus_lastCloudPoll",
|
||||
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
||||
BSPLUS_CALENDAR_SYNC_IN_PROGRESS_KEY,
|
||||
] as const;
|
||||
|
||||
/** After restoring from cloud, keep local session so the user stays signed in. */
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
shouldRunWeeklySync,
|
||||
} from "@/seqta/utils/calendarSync/settings";
|
||||
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
|
||||
import {
|
||||
resumeInterruptedCalendarSync,
|
||||
runGoogleCalendarSync,
|
||||
runOutlookCalendarSync,
|
||||
} from "@/seqta/utils/calendarSync/syncRunner";
|
||||
import {
|
||||
readGoogleCalendarState,
|
||||
readOutlookCalendarState,
|
||||
@@ -12,6 +16,7 @@ import {
|
||||
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
|
||||
|
||||
let listenerRegistered = false;
|
||||
let resumeChecked = false;
|
||||
|
||||
const WEEKLY_PROVIDERS = [
|
||||
{ label: "Google Calendar", read: readGoogleCalendarState, run: runGoogleCalendarSync },
|
||||
@@ -44,6 +49,10 @@ async function runWeeklySyncForConnectedProviders(): Promise<
|
||||
}
|
||||
|
||||
export function registerCalendarContentHandlers(): void {
|
||||
if (!resumeChecked) {
|
||||
resumeChecked = true;
|
||||
void resumeInterruptedCalendarSync();
|
||||
}
|
||||
if (listenerRegistered) return;
|
||||
listenerRegistered = true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user