mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: tweak calendar syncing for final release
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
import { portalToBody } from "./calendarSyncPortal";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = false,
|
open = false,
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="bsplus-cal-modal-backdrop"
|
class="bsplus-cal-modal-backdrop"
|
||||||
|
use:portalToBody
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
if (e.target === e.currentTarget && !busy) onCancel();
|
if (e.target === e.currentTarget && !busy) onCancel();
|
||||||
}}
|
}}
|
||||||
@@ -36,11 +38,11 @@
|
|||||||
transition:fade={{ duration: 180 }}
|
transition:fade={{ duration: 180 }}
|
||||||
>
|
>
|
||||||
<h2 id="bsplus-cal-delete-title" class="bsplus-cal-modal-title">
|
<h2 id="bsplus-cal-delete-title" class="bsplus-cal-modal-title">
|
||||||
Remove synced events?
|
Delete synced classes?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="bsplus-cal-modal-body">
|
<p class="bsplus-cal-modal-body">
|
||||||
This removes all BetterSEQTA+ timetable events from your {providerLabel} Calendar for this school.
|
Removes every BetterSEQTA+ timetable event from your {providerLabel} Calendar for this school.
|
||||||
Your connection stays active — you can sync again later.
|
Your account stays connected — use Update calendar to sync again.
|
||||||
</p>
|
</p>
|
||||||
<div class="bsplus-cal-modal-actions">
|
<div class="bsplus-cal-modal-actions">
|
||||||
<button
|
<button
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
disabled={busy}
|
disabled={busy}
|
||||||
onclick={() => void onConfirm()}
|
onclick={() => void onConfirm()}
|
||||||
>
|
>
|
||||||
{busy ? "Removing…" : "Remove from calendar"}
|
{busy ? "Deleting…" : "Delete synced classes"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
.bsplus-cal-modal-backdrop {
|
.bsplus-cal-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 2147483647;
|
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
import { portalToBody } from "./calendarSyncPortal";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = false,
|
open = false,
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="bsplus-cal-modal-backdrop"
|
class="bsplus-cal-modal-backdrop"
|
||||||
|
use:portalToBody
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
if (e.target === e.currentTarget && !busy) onCancel();
|
if (e.target === e.currentTarget && !busy) onCancel();
|
||||||
}}
|
}}
|
||||||
@@ -39,8 +41,8 @@
|
|||||||
Disconnect {providerLabel} Calendar?
|
Disconnect {providerLabel} Calendar?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="bsplus-cal-modal-body">
|
<p class="bsplus-cal-modal-body">
|
||||||
Your synced timetable events will stay in {providerLabel} Calendar, but BetterSEQTA+ will stop
|
Stops BetterSEQTA+ from updating your calendar. Synced classes stay in {providerLabel} Calendar
|
||||||
updating them until you connect again.
|
until you delete them or connect again.
|
||||||
</p>
|
</p>
|
||||||
<div class="bsplus-cal-modal-actions">
|
<div class="bsplus-cal-modal-actions">
|
||||||
<button
|
<button
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
disabled={busy}
|
disabled={busy}
|
||||||
onclick={() => void onConfirm()}
|
onclick={() => void onConfirm()}
|
||||||
>
|
>
|
||||||
{busy ? "Disconnecting…" : "Disconnect"}
|
{busy ? "Disconnecting…" : "Disconnect account"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
.bsplus-cal-modal-backdrop {
|
.bsplus-cal-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 2147483647;
|
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -25,10 +25,18 @@
|
|||||||
import { deleteSyncedEventsFromOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
import { deleteSyncedEventsFromOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
||||||
import CalendarDeleteEventsModal from "./CalendarDeleteEventsModal.svelte";
|
import CalendarDeleteEventsModal from "./CalendarDeleteEventsModal.svelte";
|
||||||
import CalendarDisconnectModal from "./CalendarDisconnectModal.svelte";
|
import CalendarDisconnectModal from "./CalendarDisconnectModal.svelte";
|
||||||
import CalendarSyncProgress from "./CalendarSyncProgress.svelte";
|
|
||||||
import OutlookCalendarIcon from "./OutlookCalendarIcon.svelte";
|
import OutlookCalendarIcon from "./OutlookCalendarIcon.svelte";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import { syncCalendarSyncTheme } from "./calendarSyncTheme";
|
import { syncCalendarSyncTheme } from "./calendarSyncTheme";
|
||||||
|
import { isCalendarSyncModalTarget, portalToBody } from "./calendarSyncPortal";
|
||||||
|
|
||||||
|
function syncProgressPercent(progress: GoogleCalendarSyncProgress | null): number {
|
||||||
|
if (!progress || progress.phase === "done") return 0;
|
||||||
|
if (progress.total > 0) {
|
||||||
|
return Math.min(100, Math.round((progress.current / progress.total) * 100));
|
||||||
|
}
|
||||||
|
return progress.phase === "preparing" ? 8 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
type CalendarProvider = "google" | "outlook";
|
type CalendarProvider = "google" | "outlook";
|
||||||
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
|
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
|
||||||
@@ -54,6 +62,38 @@
|
|||||||
|
|
||||||
const isBusy = $derived(busy !== null);
|
const isBusy = $derived(busy !== null);
|
||||||
const anyConnected = $derived(googleStatus.connected || outlookStatus.connected);
|
const anyConnected = $derived(googleStatus.connected || outlookStatus.connected);
|
||||||
|
const showTriggerProgress = $derived(
|
||||||
|
isBusy &&
|
||||||
|
(busy?.phase === "sync" ||
|
||||||
|
busy?.phase === "delete" ||
|
||||||
|
busy?.phase === "connect" ||
|
||||||
|
(syncProgress !== null && syncProgress.phase !== "done")),
|
||||||
|
);
|
||||||
|
const triggerProgressPercent = $derived.by(() => {
|
||||||
|
if (!showTriggerProgress) return 0;
|
||||||
|
if (syncProgress && syncProgress.phase !== "done") {
|
||||||
|
return syncProgressPercent(syncProgress);
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
});
|
||||||
|
const triggerStatusText = $derived.by(() => {
|
||||||
|
if (!showTriggerProgress) return "Sync with Calendar";
|
||||||
|
const verb = busy?.phase === "delete" ? "Deleting" : "Syncing";
|
||||||
|
if (syncProgress?.total) return `${verb} ${triggerProgressPercent}%`;
|
||||||
|
return `${verb}…`;
|
||||||
|
});
|
||||||
|
const triggerAriaLabel = $derived.by(() => {
|
||||||
|
if (!showTriggerProgress) {
|
||||||
|
return anyConnected ? "Calendar sync options" : "Sync with Calendar";
|
||||||
|
}
|
||||||
|
if (syncProgress?.total) {
|
||||||
|
const verb = busy?.phase === "delete" ? "Deleting" : "Syncing";
|
||||||
|
return `Calendar ${verb.toLowerCase()} in progress, ${triggerProgressPercent} percent complete`;
|
||||||
|
}
|
||||||
|
return busy?.phase === "delete"
|
||||||
|
? "Calendar deletion in progress"
|
||||||
|
: "Calendar sync in progress";
|
||||||
|
});
|
||||||
const accent = "var(--bsplus-cal-accent, var(--better-main, #3b82f6))";
|
const accent = "var(--bsplus-cal-accent, var(--better-main, #3b82f6))";
|
||||||
|
|
||||||
function isProviderBusy(provider: CalendarProvider): boolean {
|
function isProviderBusy(provider: CalendarProvider): boolean {
|
||||||
@@ -150,7 +190,7 @@
|
|||||||
async function connectProvider(provider: CalendarProvider) {
|
async function connectProvider(provider: CalendarProvider) {
|
||||||
const status = provider === "google" ? googleStatus : outlookStatus;
|
const status = provider === "google" ? googleStatus : outlookStatus;
|
||||||
if (!status.configured || isBusy) return;
|
if (!status.configured || isBusy) return;
|
||||||
menuOpen = true;
|
menuOpen = false;
|
||||||
busy = { provider, phase: "connect" };
|
busy = { provider, phase: "connect" };
|
||||||
const connectType =
|
const connectType =
|
||||||
provider === "google" ? "googleCalendarConnect" : "outlookCalendarConnect";
|
provider === "google" ? "googleCalendarConnect" : "outlookCalendarConnect";
|
||||||
@@ -187,6 +227,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
busy = { provider, phase: "sync" };
|
busy = { provider, phase: "sync" };
|
||||||
|
menuOpen = false;
|
||||||
try {
|
try {
|
||||||
await performSync(provider);
|
await performSync(provider);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -200,6 +241,8 @@
|
|||||||
async function confirmDeleteEvents() {
|
async function confirmDeleteEvents() {
|
||||||
if (isBusy || !modalProvider) return;
|
if (isBusy || !modalProvider) return;
|
||||||
const provider = modalProvider;
|
const provider = modalProvider;
|
||||||
|
showDeleteEvents = false;
|
||||||
|
menuOpen = false;
|
||||||
busy = { provider, phase: "delete" };
|
busy = { provider, phase: "delete" };
|
||||||
syncProgress = {
|
syncProgress = {
|
||||||
phase: "preparing",
|
phase: "preparing",
|
||||||
@@ -222,13 +265,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removed = result.deleted ?? 0;
|
const removed = result.deleted ?? 0;
|
||||||
showDeleteEvents = false;
|
|
||||||
menuOpen = false;
|
|
||||||
modalProvider = null;
|
modalProvider = null;
|
||||||
|
const label = provider === "google" ? "Google" : "Outlook";
|
||||||
if (removed === 0) {
|
if (removed === 0) {
|
||||||
showToastMessage("No synced events to remove.");
|
showToastMessage("No synced events to remove.");
|
||||||
} else {
|
} else {
|
||||||
showToastMessage(`Removed ${removed} event${removed === 1 ? "" : "s"} from Google Calendar.`);
|
showToastMessage(`Removed ${removed} event${removed === 1 ? "" : "s"} from ${label} Calendar.`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToastMessage(err instanceof Error ? err.message : "Remove failed.", true);
|
showToastMessage(err instanceof Error ? err.message : "Remove failed.", true);
|
||||||
@@ -281,8 +323,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenu() {
|
function openDeleteModal(provider: CalendarProvider) {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
modalProvider = provider;
|
||||||
|
menuOpen = false;
|
||||||
|
showDeleteEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDisconnectModal(provider: CalendarProvider) {
|
||||||
|
if (isBusy) return;
|
||||||
|
modalProvider = provider;
|
||||||
|
menuOpen = false;
|
||||||
|
showDisconnect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
menuOpen = !menuOpen;
|
menuOpen = !menuOpen;
|
||||||
if (menuOpen) {
|
if (menuOpen) {
|
||||||
queueMicrotask(() => syncMenuTheme());
|
queueMicrotask(() => syncMenuTheme());
|
||||||
@@ -317,12 +372,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function portalMenu(node: HTMLElement) {
|
function portalMenu(node: HTMLElement) {
|
||||||
document.body.appendChild(node);
|
return portalToBody(node);
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
node.remove();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -332,8 +382,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!menuOpen || !menuEl) return;
|
if (menuOpen && menuEl) syncMenuTheme();
|
||||||
syncMenuTheme();
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!menuOpen || !triggerEl) return;
|
||||||
updateMenuPosition();
|
updateMenuPosition();
|
||||||
const onLayout = () => updateMenuPosition();
|
const onLayout = () => updateMenuPosition();
|
||||||
window.addEventListener("resize", onLayout);
|
window.addEventListener("resize", onLayout);
|
||||||
@@ -376,6 +429,7 @@
|
|||||||
const target = event.target as Node;
|
const target = event.target as Node;
|
||||||
if (rootEl?.contains(target)) return;
|
if (rootEl?.contains(target)) return;
|
||||||
if (menuEl?.contains(target)) return;
|
if (menuEl?.contains(target)) return;
|
||||||
|
if (isCalendarSyncModalTarget(target)) return;
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -397,21 +451,20 @@
|
|||||||
class="uiButton bsplus-cal-trigger"
|
class="uiButton bsplus-cal-trigger"
|
||||||
bind:this={triggerEl}
|
bind:this={triggerEl}
|
||||||
class:bsplus-cal-trigger--open={menuOpen}
|
class:bsplus-cal-trigger--open={menuOpen}
|
||||||
class:bsplus-cal-trigger--connected={anyConnected}
|
|
||||||
class:bsplus-cal-trigger--busy={isBusy}
|
class:bsplus-cal-trigger--busy={isBusy}
|
||||||
|
class:bsplus-cal-trigger--progress={showTriggerProgress}
|
||||||
|
style:--bsplus-cal-trigger-progress="{triggerProgressPercent}%"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={menuOpen}
|
aria-expanded={menuOpen}
|
||||||
aria-busy={isBusy}
|
aria-busy={isBusy}
|
||||||
aria-label={anyConnected ? "Calendar sync options" : "Sync with Calendar"}
|
aria-label={triggerAriaLabel}
|
||||||
onclick={() => {
|
onclick={() => toggleMenu()}
|
||||||
if (!isBusy) toggleMenu();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<span class="bsplus-cal-trigger-fill" aria-hidden="true"></span>
|
||||||
|
<span class="bsplus-cal-trigger-content">
|
||||||
<span class="bsplus-cal-trigger-icon iconFamily" aria-hidden="true"></span>
|
<span class="bsplus-cal-trigger-icon iconFamily" aria-hidden="true"></span>
|
||||||
<span class="bsplus-cal-trigger-text">Sync with Calendar</span>
|
<span class="bsplus-cal-trigger-text">{triggerStatusText}</span>
|
||||||
{#if anyConnected}
|
</span>
|
||||||
<span class="bsplus-cal-status-dot" aria-hidden="true"></span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
@@ -425,7 +478,7 @@
|
|||||||
>
|
>
|
||||||
<div class="bsplus-cal-menu-header">
|
<div class="bsplus-cal-menu-header">
|
||||||
<span class="bsplus-cal-menu-title">Calendar sync</span>
|
<span class="bsplus-cal-menu-title">Calendar sync</span>
|
||||||
<span class="bsplus-cal-menu-sub">Connect providers to sync your timetable</span>
|
<span class="bsplus-cal-menu-sub">Copy your SEQTA timetable classes to Google or Outlook</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bsplus-cal-provider" role="none">
|
<div class="bsplus-cal-provider" role="none">
|
||||||
@@ -479,7 +532,7 @@
|
|||||||
disabled={!googleStatus.configured || isBusy}
|
disabled={!googleStatus.configured || isBusy}
|
||||||
onclick={() => void connectProvider("google")}
|
onclick={() => void connectProvider("google")}
|
||||||
>
|
>
|
||||||
{providerPhase("google") === "connect" ? "Connecting…" : "Connect"}
|
{providerPhase("google") === "connect" ? "Connecting…" : "Connect & sync"}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
@@ -490,31 +543,25 @@
|
|||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => void syncProvider("google")}
|
onclick={() => void syncProvider("google")}
|
||||||
>
|
>
|
||||||
{providerPhase("google") === "sync" ? "Syncing…" : "Sync now"}
|
{providerPhase("google") === "sync" ? "Updating…" : "Update calendar"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => {
|
onclick={() => openDeleteModal("google")}
|
||||||
modalProvider = "google";
|
|
||||||
showDeleteEvents = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{providerPhase("google") === "delete" ? "Removing…" : "Remove from calendar"}
|
{providerPhase("google") === "delete" ? "Deleting…" : "Delete synced classes"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => {
|
onclick={() => openDisconnectModal("google")}
|
||||||
modalProvider = "google";
|
|
||||||
showDisconnect = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Disconnect
|
Disconnect account
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -549,7 +596,7 @@
|
|||||||
disabled={!outlookStatus.configured || isBusy}
|
disabled={!outlookStatus.configured || isBusy}
|
||||||
onclick={() => void connectProvider("outlook")}
|
onclick={() => void connectProvider("outlook")}
|
||||||
>
|
>
|
||||||
{providerPhase("outlook") === "connect" ? "Connecting…" : "Connect"}
|
{providerPhase("outlook") === "connect" ? "Connecting…" : "Connect & sync"}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
@@ -560,40 +607,39 @@
|
|||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => void syncProvider("outlook")}
|
onclick={() => void syncProvider("outlook")}
|
||||||
>
|
>
|
||||||
{providerPhase("outlook") === "sync" ? "Syncing…" : "Sync now"}
|
{providerPhase("outlook") === "sync" ? "Updating…" : "Update calendar"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => {
|
onclick={() => openDeleteModal("outlook")}
|
||||||
modalProvider = "outlook";
|
|
||||||
showDeleteEvents = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{providerPhase("outlook") === "delete" ? "Removing…" : "Remove from calendar"}
|
{providerPhase("outlook") === "delete" ? "Deleting…" : "Delete synced classes"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onclick={() => {
|
onclick={() => openDisconnectModal("outlook")}
|
||||||
modalProvider = "outlook";
|
|
||||||
showDisconnect = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Disconnect
|
Disconnect account
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if anyConnected}
|
{#if anyConnected}
|
||||||
<div class="bsplus-cal-settings" role="group" aria-label="Sync settings">
|
<div class="bsplus-cal-settings" role="group" aria-label="Sync options">
|
||||||
<label class="bsplus-cal-setting">
|
<label class="bsplus-cal-setting">
|
||||||
<span class="bsplus-cal-setting-label">Weeks ahead</span>
|
<div class="bsplus-cal-setting-copy">
|
||||||
|
<span class="bsplus-cal-setting-label">Weeks to sync</span>
|
||||||
|
<span class="bsplus-cal-setting-desc">
|
||||||
|
How many weeks of classes to add when you connect or tap Update calendar.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="bsplus-cal-setting-input"
|
class="bsplus-cal-setting-input"
|
||||||
@@ -605,7 +651,12 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="bsplus-cal-setting bsplus-cal-setting--toggle">
|
<label class="bsplus-cal-setting bsplus-cal-setting--toggle">
|
||||||
<span class="bsplus-cal-setting-label">Auto-sync weekly</span>
|
<div class="bsplus-cal-setting-copy">
|
||||||
|
<span class="bsplus-cal-setting-label">Sync new weeks automatically</span>
|
||||||
|
<span class="bsplus-cal-setting-desc">
|
||||||
|
Each week, add the next week of your timetable without opening this menu.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="bsplus-cal-setting-checkbox"
|
class="bsplus-cal-setting-checkbox"
|
||||||
@@ -614,13 +665,8 @@
|
|||||||
onchange={(e) => void onAutoSyncToggle(e)}
|
onchange={(e) => void onAutoSyncToggle(e)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p class="bsplus-cal-setting-hint">
|
|
||||||
Syncs {syncWeeksAhead} weeks ahead on connect and manual sync. Weekly auto-sync adds each new week forward.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<CalendarSyncProgress progress={syncProgress} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -676,7 +722,32 @@
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: all 0.2s ease;
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-trigger-fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: var(--bsplus-cal-trigger-progress, 0%);
|
||||||
|
border-radius: inherit;
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 38%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-trigger-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-trigger-icon {
|
.bsplus-cal-trigger-icon {
|
||||||
@@ -735,25 +806,17 @@
|
|||||||
background: color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 14%, transparent) !important;
|
background: color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 14%, transparent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-trigger--connected .bsplus-cal-status-dot {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-cal-trigger--busy {
|
.bsplus-cal-trigger--busy {
|
||||||
opacity: 0.85;
|
opacity: 0.95;
|
||||||
cursor: wait;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-status-dot {
|
.bsplus-cal-trigger--progress {
|
||||||
position: absolute;
|
cursor: default;
|
||||||
top: 4px;
|
}
|
||||||
right: 4px;
|
|
||||||
width: 7px;
|
.bsplus-cal-trigger--progress:hover,
|
||||||
height: 7px;
|
.bsplus-cal-trigger--progress:active {
|
||||||
border-radius: 999px;
|
transform: none;
|
||||||
background: #22c55e;
|
|
||||||
box-shadow: 0 0 0 2px var(--bsplus-cal-surface, #fff);
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-settings {
|
.bsplus-cal-settings {
|
||||||
@@ -768,19 +831,37 @@
|
|||||||
|
|
||||||
.bsplus-cal-setting {
|
.bsplus-cal-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-setting-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.bsplus-cal-setting-label {
|
.bsplus-cal-setting-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-setting-desc {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 58%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-setting--toggle {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.bsplus-cal-setting-input {
|
.bsplus-cal-setting-input {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 18%, transparent));
|
border: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 18%, transparent));
|
||||||
@@ -793,19 +874,13 @@
|
|||||||
.bsplus-cal-setting-checkbox {
|
.bsplus-cal-setting-checkbox {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
flex: 0 0 auto;
|
||||||
accent-color: var(--bsplus-cal-accent, var(--better-main, #3b82f6));
|
accent-color: var(--bsplus-cal-accent, var(--better-main, #3b82f6));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-setting-hint {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 58%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-cal-menu {
|
.bsplus-cal-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 2147483647;
|
z-index: var(--bsplus-cal-z-menu, 2147483646);
|
||||||
width: min(320px, calc(100vw - 24px));
|
width: min(320px, calc(100vw - 24px));
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|||||||
@@ -11,11 +11,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timetable-calendar-controls.bsplus-cal-menu-open {
|
.timetable-calendar-controls.bsplus-cal-menu-open {
|
||||||
z-index: 2147483646;
|
z-index: 2147483645;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timetablepage #toolbar:has(.bsplus-cal-menu-open) {
|
.timetablepage #toolbar:has(.bsplus-cal-menu-open) {
|
||||||
z-index: 2147483646 !important;
|
z-index: 2147483645 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bsplus-cal-z-menu: 2147483646;
|
||||||
|
--bsplus-cal-z-modal: 2147483647;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-calendar-sync-mount {
|
.bsplus-calendar-sync-mount {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/** Layer order: toolbar boost < menu < modal (all portaled UI uses the upper layers). */
|
||||||
|
export const CALENDAR_SYNC_Z_MENU = 2_147_483_646;
|
||||||
|
export const CALENDAR_SYNC_Z_MODAL = 2_147_483_647;
|
||||||
|
|
||||||
|
export function portalToBody(node: HTMLElement) {
|
||||||
|
document.body.appendChild(node);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.remove();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCalendarSyncModalTarget(target: EventTarget | null): boolean {
|
||||||
|
return target instanceof Element && Boolean(target.closest(".bsplus-cal-modal-backdrop"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
jest.mock("@/utils/verboseLog", () => ({
|
||||||
|
verboseLog: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
reportSyncProgress,
|
||||||
|
resetSyncProgressThrottle,
|
||||||
|
SYNC_PROGRESS_THROTTLE_MS,
|
||||||
|
} from "./lessonSyncShared";
|
||||||
|
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||||
|
|
||||||
|
describe("reportSyncProgress", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date("2026-06-28T12:00:00.000Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports preparing and done immediately", () => {
|
||||||
|
const onProgress = jest.fn();
|
||||||
|
const preparing: GoogleCalendarSyncProgress = {
|
||||||
|
phase: "preparing",
|
||||||
|
current: 0,
|
||||||
|
total: 10,
|
||||||
|
message: "Preparing…",
|
||||||
|
};
|
||||||
|
const done: GoogleCalendarSyncProgress = {
|
||||||
|
phase: "done",
|
||||||
|
current: 10,
|
||||||
|
total: 10,
|
||||||
|
message: "Done",
|
||||||
|
};
|
||||||
|
|
||||||
|
reportSyncProgress(onProgress, preparing);
|
||||||
|
reportSyncProgress(onProgress, done);
|
||||||
|
|
||||||
|
expect(onProgress).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onProgress).toHaveBeenNthCalledWith(1, preparing);
|
||||||
|
expect(onProgress).toHaveBeenNthCalledWith(2, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throttles upserting progress to at most once per second", () => {
|
||||||
|
const onProgress = jest.fn();
|
||||||
|
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
reportSyncProgress(onProgress, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: i,
|
||||||
|
total: 5,
|
||||||
|
message: `Syncing events (${i}/5)…`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(onProgress).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onProgress).toHaveBeenCalledWith({
|
||||||
|
phase: "upserting",
|
||||||
|
current: 1,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (1/5)…",
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(SYNC_PROGRESS_THROTTLE_MS);
|
||||||
|
|
||||||
|
expect(onProgress).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onProgress).toHaveBeenLastCalledWith({
|
||||||
|
phase: "upserting",
|
||||||
|
current: 5,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (5/5)…",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flushes pending progress before reporting done", () => {
|
||||||
|
const onProgress = jest.fn();
|
||||||
|
|
||||||
|
reportSyncProgress(onProgress, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: 1,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (1/5)…",
|
||||||
|
});
|
||||||
|
|
||||||
|
reportSyncProgress(onProgress, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: 4,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (4/5)…",
|
||||||
|
});
|
||||||
|
|
||||||
|
reportSyncProgress(onProgress, {
|
||||||
|
phase: "done",
|
||||||
|
current: 5,
|
||||||
|
total: 5,
|
||||||
|
message: "Sync complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onProgress).toHaveBeenCalledTimes(3);
|
||||||
|
expect(onProgress).toHaveBeenNthCalledWith(1, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: 1,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (1/5)…",
|
||||||
|
});
|
||||||
|
expect(onProgress).toHaveBeenNthCalledWith(2, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: 4,
|
||||||
|
total: 5,
|
||||||
|
message: "Syncing events (4/5)…",
|
||||||
|
});
|
||||||
|
expect(onProgress).toHaveBeenNthCalledWith(3, {
|
||||||
|
phase: "done",
|
||||||
|
current: 5,
|
||||||
|
total: 5,
|
||||||
|
message: "Sync complete",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resetSyncProgressThrottle clears queued updates", () => {
|
||||||
|
const onProgress = jest.fn();
|
||||||
|
|
||||||
|
reportSyncProgress(onProgress, {
|
||||||
|
phase: "upserting",
|
||||||
|
current: 1,
|
||||||
|
total: 3,
|
||||||
|
message: "Syncing events (1/3)…",
|
||||||
|
});
|
||||||
|
|
||||||
|
resetSyncProgressThrottle(onProgress);
|
||||||
|
jest.advanceTimersByTime(SYNC_PROGRESS_THROTTLE_MS);
|
||||||
|
|
||||||
|
expect(onProgress).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
} from "@/seqta/utils/googleCalendar/types";
|
} from "@/seqta/utils/googleCalendar/types";
|
||||||
|
|
||||||
export const EVENT_MAP_PERSIST_EVERY = 10;
|
export const EVENT_MAP_PERSIST_EVERY = 10;
|
||||||
|
/** Max UI progress refresh rate during bulk delete/upsert (reduces Svelte re-renders). */
|
||||||
|
export const SYNC_PROGRESS_THROTTLE_MS = 1000;
|
||||||
|
|
||||||
export type EventMapRecord = Record<string, string | { id: string; date: string }>;
|
export type EventMapRecord = Record<string, string | { id: string; date: string }>;
|
||||||
|
|
||||||
@@ -23,11 +25,87 @@ export type MappedLessonEvent = {
|
|||||||
startDateTime: string;
|
startDateTime: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProgressThrottleState = {
|
||||||
|
lastReportAt: number;
|
||||||
|
pending: GoogleCalendarSyncProgress | null;
|
||||||
|
timer: ReturnType<typeof setTimeout> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressThrottleByCallback = new WeakMap<
|
||||||
|
NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
|
||||||
|
ProgressThrottleState
|
||||||
|
>();
|
||||||
|
|
||||||
|
function getProgressThrottleState(
|
||||||
|
onProgress: NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
|
||||||
|
): ProgressThrottleState {
|
||||||
|
let state = progressThrottleByCallback.get(onProgress);
|
||||||
|
if (!state) {
|
||||||
|
state = { lastReportAt: 0, pending: null, timer: null };
|
||||||
|
progressThrottleByCallback.set(onProgress, state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushPendingSyncProgress(
|
||||||
|
onProgress: NonNullable<GoogleCalendarSyncOptions["onProgress"]>,
|
||||||
|
state: ProgressThrottleState,
|
||||||
|
) {
|
||||||
|
if (state.timer) {
|
||||||
|
clearTimeout(state.timer);
|
||||||
|
state.timer = null;
|
||||||
|
}
|
||||||
|
if (!state.pending) return;
|
||||||
|
onProgress(state.pending);
|
||||||
|
state.pending = null;
|
||||||
|
state.lastReportAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears any queued progress for a callback (e.g. when a sync run ends). */
|
||||||
|
export function resetSyncProgressThrottle(
|
||||||
|
onProgress: GoogleCalendarSyncOptions["onProgress"],
|
||||||
|
) {
|
||||||
|
if (!onProgress) return;
|
||||||
|
const state = progressThrottleByCallback.get(onProgress);
|
||||||
|
if (!state) return;
|
||||||
|
if (state.timer) {
|
||||||
|
clearTimeout(state.timer);
|
||||||
|
state.timer = null;
|
||||||
|
}
|
||||||
|
state.pending = null;
|
||||||
|
}
|
||||||
|
|
||||||
export function reportSyncProgress(
|
export function reportSyncProgress(
|
||||||
onProgress: GoogleCalendarSyncOptions["onProgress"],
|
onProgress: GoogleCalendarSyncOptions["onProgress"],
|
||||||
progress: GoogleCalendarSyncProgress,
|
progress: GoogleCalendarSyncProgress,
|
||||||
) {
|
) {
|
||||||
onProgress?.(progress);
|
if (!onProgress) return;
|
||||||
|
|
||||||
|
const state = getProgressThrottleState(onProgress);
|
||||||
|
|
||||||
|
if (progress.phase === "preparing" || progress.phase === "done") {
|
||||||
|
flushPendingSyncProgress(onProgress, state);
|
||||||
|
onProgress(progress);
|
||||||
|
state.lastReportAt = Date.now();
|
||||||
|
if (progress.phase === "done") {
|
||||||
|
resetSyncProgressThrottle(onProgress);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pending = progress;
|
||||||
|
const elapsed = Date.now() - state.lastReportAt;
|
||||||
|
if (elapsed >= SYNC_PROGRESS_THROTTLE_MS) {
|
||||||
|
flushPendingSyncProgress(onProgress, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.timer) return;
|
||||||
|
|
||||||
|
state.timer = setTimeout(() => {
|
||||||
|
state.timer = null;
|
||||||
|
flushPendingSyncProgress(onProgress, state);
|
||||||
|
}, SYNC_PROGRESS_THROTTLE_MS - elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lessonDateForEvent(startDateTime: string, seqtaKey: string): string {
|
export function lessonDateForEvent(startDateTime: string, seqtaKey: string): string {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
trailingWeekRange,
|
trailingWeekRange,
|
||||||
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
||||||
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
||||||
|
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||||
import { syncLessonsToGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
|
import { syncLessonsToGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
|
||||||
import type {
|
import type {
|
||||||
GoogleCalendarSyncOptions,
|
GoogleCalendarSyncOptions,
|
||||||
@@ -36,7 +37,7 @@ export async function runGoogleCalendarSync(
|
|||||||
const mode = params.mode ?? "full";
|
const mode = params.mode ?? "full";
|
||||||
const weeksAhead = await getSyncWeeksAhead();
|
const weeksAhead = await getSyncWeeksAhead();
|
||||||
|
|
||||||
params.onProgress?.({
|
reportSyncProgress(params.onProgress, {
|
||||||
phase: "preparing",
|
phase: "preparing",
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 1,
|
total: 1,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
trailingWeekRange,
|
trailingWeekRange,
|
||||||
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
} from "@/seqta/utils/googleCalendar/fetchTimetable";
|
||||||
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
|
||||||
|
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||||
import { syncLessonsToOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
import { syncLessonsToOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
||||||
import type {
|
import type {
|
||||||
GoogleCalendarSyncOptions,
|
GoogleCalendarSyncOptions,
|
||||||
@@ -36,7 +37,7 @@ export async function runOutlookCalendarSync(
|
|||||||
const mode = params.mode ?? "full";
|
const mode = params.mode ?? "full";
|
||||||
const weeksAhead = await getSyncWeeksAhead();
|
const weeksAhead = await getSyncWeeksAhead();
|
||||||
|
|
||||||
params.onProgress?.({
|
reportSyncProgress(params.onProgress, {
|
||||||
phase: "preparing",
|
phase: "preparing",
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 1,
|
total: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user