mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
refactor: consolidate and debloat calendar sync implementation
Merge duplicated Google/Outlook engines, background handlers, and API layers into shared calendarSync modules to cut complexity without changing user-facing sync behavior.
This commit is contained in:
@@ -1,144 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import { portalToBody } from "./calendarSyncPortal";
|
||||
|
||||
let {
|
||||
open = false,
|
||||
busy = false,
|
||||
providerLabel = "Google",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
} = $props<{
|
||||
open?: boolean;
|
||||
busy?: boolean;
|
||||
providerLabel?: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
onCancel: () => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="bsplus-cal-modal-backdrop"
|
||||
use:portalToBody
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget && !busy) onCancel();
|
||||
}}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Escape" && !busy) onCancel();
|
||||
}}
|
||||
role="presentation"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<div
|
||||
class="bsplus-cal-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="bsplus-cal-delete-title"
|
||||
transition:fade={{ duration: 180 }}
|
||||
>
|
||||
<h2 id="bsplus-cal-delete-title" class="bsplus-cal-modal-title">
|
||||
Delete synced classes?
|
||||
</h2>
|
||||
<p class="bsplus-cal-modal-body">
|
||||
Removes every BetterSEQTA+ timetable event from your {providerLabel} Calendar for this school.
|
||||
Your account stays connected — use Update calendar to sync again.
|
||||
</p>
|
||||
<div class="bsplus-cal-modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--ghost"
|
||||
disabled={busy}
|
||||
onclick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--danger"
|
||||
disabled={busy}
|
||||
onclick={() => void onConfirm()}
|
||||
>
|
||||
{busy ? "Deleting…" : "Delete synced classes"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.bsplus-cal-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal {
|
||||
width: min(100%, 400px);
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
background: var(--bsplus-cal-surface, #fff);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid color-mix(in srgb, var(--bsplus-cal-text, #111) 12%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-body {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 72%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 8%, transparent);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 14%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
@@ -1,144 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import { portalToBody } from "./calendarSyncPortal";
|
||||
|
||||
let {
|
||||
open = false,
|
||||
busy = false,
|
||||
providerLabel = "Google",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
} = $props<{
|
||||
open?: boolean;
|
||||
busy?: boolean;
|
||||
providerLabel?: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
onCancel: () => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="bsplus-cal-modal-backdrop"
|
||||
use:portalToBody
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget && !busy) onCancel();
|
||||
}}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Escape" && !busy) onCancel();
|
||||
}}
|
||||
role="presentation"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<div
|
||||
class="bsplus-cal-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="bsplus-cal-disconnect-title"
|
||||
transition:fade={{ duration: 180 }}
|
||||
>
|
||||
<h2 id="bsplus-cal-disconnect-title" class="bsplus-cal-modal-title">
|
||||
Disconnect {providerLabel} Calendar?
|
||||
</h2>
|
||||
<p class="bsplus-cal-modal-body">
|
||||
Stops BetterSEQTA+ from updating your calendar. Synced classes stay in {providerLabel} Calendar
|
||||
until you delete them or connect again.
|
||||
</p>
|
||||
<div class="bsplus-cal-modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--ghost"
|
||||
disabled={busy}
|
||||
onclick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--danger"
|
||||
disabled={busy}
|
||||
onclick={() => void onConfirm()}
|
||||
>
|
||||
{busy ? "Disconnecting…" : "Disconnect account"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.bsplus-cal-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal {
|
||||
width: min(100%, 400px);
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
background: var(--bsplus-cal-surface, #fff);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid color-mix(in srgb, var(--bsplus-cal-text, #111) 12%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-body {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 72%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 8%, transparent);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 14%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
import browser from "webextension-polyfill";
|
||||
import {
|
||||
@@ -7,40 +8,41 @@
|
||||
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
|
||||
} from "@/config/googleCalendar";
|
||||
import { maybeRunDueWeeklySync } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
||||
import { deleteSyncedEventsFromGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
|
||||
import {
|
||||
formatOutlookSyncResultMessage,
|
||||
runOutlookCalendarSync,
|
||||
} from "@/seqta/utils/outlookCalendar/syncRunner";
|
||||
import {
|
||||
formatSyncResultMessage,
|
||||
runGoogleCalendarSync,
|
||||
} from "@/seqta/utils/googleCalendar/syncRunner";
|
||||
deleteSyncedEventsFromGoogleCalendar,
|
||||
deleteSyncedEventsFromOutlookCalendar,
|
||||
} from "@/seqta/utils/calendarSync/syncEngine";
|
||||
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
|
||||
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
|
||||
import type {
|
||||
GoogleCalendarStatus,
|
||||
GoogleCalendarSyncProgress,
|
||||
GoogleCalendarSyncResult,
|
||||
} from "@/seqta/utils/googleCalendar/types";
|
||||
import type { OutlookCalendarStatus } from "@/seqta/utils/outlookCalendar/types";
|
||||
import { deleteSyncedEventsFromOutlookCalendar } from "@/seqta/utils/outlookCalendar/syncEngine";
|
||||
import CalendarDeleteEventsModal from "./CalendarDeleteEventsModal.svelte";
|
||||
import CalendarDisconnectModal from "./CalendarDisconnectModal.svelte";
|
||||
import type { OutlookCalendarStatus } from "@/seqta/utils/outlookCalendar/storage";
|
||||
import OutlookCalendarIcon from "./OutlookCalendarIcon.svelte";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
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";
|
||||
import {
|
||||
formatLastSync,
|
||||
isCalendarSyncModalTarget,
|
||||
portalToBody,
|
||||
providerLabel as calendarProviderLabel,
|
||||
syncCalendarSyncTheme,
|
||||
syncProgressPercent,
|
||||
type CalendarProvider,
|
||||
} from "./calendarSyncUi";
|
||||
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
|
||||
type BusyState = { provider: CalendarProvider; phase: BusyPhase } | null;
|
||||
type ProviderStatus = { configured: boolean; connected: boolean; lastSyncAt?: number };
|
||||
|
||||
function setProviderStatus(provider: CalendarProvider, patch: Partial<ProviderStatus>) {
|
||||
if (provider === "google") googleStatus = { ...googleStatus, ...patch };
|
||||
else outlookStatus = { ...outlookStatus, ...patch };
|
||||
}
|
||||
|
||||
function providerStatus(provider: CalendarProvider): ProviderStatus {
|
||||
return provider === "google" ? googleStatus : outlookStatus;
|
||||
}
|
||||
|
||||
let googleStatus = $state<GoogleCalendarStatus>({ configured: true, connected: false });
|
||||
let outlookStatus = $state<OutlookCalendarStatus>({ configured: true, connected: false });
|
||||
@@ -62,6 +64,11 @@
|
||||
|
||||
const isBusy = $derived(busy !== null);
|
||||
const anyConnected = $derived(googleStatus.connected || outlookStatus.connected);
|
||||
const modalOpen = $derived(showDisconnect || showDeleteEvents);
|
||||
const modalBusy = $derived(
|
||||
showDisconnect ? busy?.phase === "disconnect" : busy?.phase === "delete",
|
||||
);
|
||||
const providerLabel = $derived(calendarProviderLabel(modalProvider ?? "google"));
|
||||
const showTriggerProgress = $derived(
|
||||
isBusy &&
|
||||
(busy?.phase === "sync" ||
|
||||
@@ -96,14 +103,22 @@
|
||||
});
|
||||
const accent = "var(--bsplus-cal-accent, var(--better-main, #3b82f6))";
|
||||
|
||||
function isProviderBusy(provider: CalendarProvider): boolean {
|
||||
return busy?.provider === provider;
|
||||
}
|
||||
|
||||
function providerPhase(provider: CalendarProvider): BusyPhase {
|
||||
return busy?.provider === provider ? busy.phase : null;
|
||||
}
|
||||
|
||||
function providerStatusText(status: ProviderStatus, notConfigured: string): string {
|
||||
if (!status.configured) return notConfigured;
|
||||
if (!status.connected) return "Not connected";
|
||||
const lastSync = formatLastSync(status.lastSyncAt);
|
||||
return lastSync ? `Connected · ${lastSync}` : "Connected";
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (showDisconnect && busy?.phase !== "disconnect") showDisconnect = false;
|
||||
if (showDeleteEvents && busy?.phase !== "delete") showDeleteEvents = false;
|
||||
}
|
||||
|
||||
function showToastMessage(message: string, isError = false) {
|
||||
toast = { message, error: isError };
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
@@ -159,7 +174,11 @@
|
||||
mode: "full" | "incremental" = "full",
|
||||
): Promise<boolean> {
|
||||
const run = provider === "google" ? runGoogleCalendarSync : runOutlookCalendarSync;
|
||||
const format = provider === "google" ? formatSyncResultMessage : formatOutlookSyncResultMessage;
|
||||
const format = (result: GoogleCalendarSyncResult) =>
|
||||
formatLessonSyncResultMessage(
|
||||
result,
|
||||
`${calendarProviderLabel(provider)} Calendar`,
|
||||
);
|
||||
|
||||
const result = await run({ mode, onProgress: handleSyncProgress });
|
||||
syncProgress = null;
|
||||
@@ -169,26 +188,17 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (provider === "google") {
|
||||
googleStatus = {
|
||||
...googleStatus,
|
||||
connected: true,
|
||||
lastSyncAt: result.lastSyncAt ?? googleStatus.lastSyncAt,
|
||||
};
|
||||
} else {
|
||||
outlookStatus = {
|
||||
...outlookStatus,
|
||||
connected: true,
|
||||
lastSyncAt: result.lastSyncAt ?? outlookStatus.lastSyncAt,
|
||||
};
|
||||
}
|
||||
setProviderStatus(provider, {
|
||||
connected: true,
|
||||
lastSyncAt: result.lastSyncAt ?? providerStatus(provider).lastSyncAt,
|
||||
});
|
||||
|
||||
showToastMessage(format(result));
|
||||
return true;
|
||||
}
|
||||
|
||||
async function connectProvider(provider: CalendarProvider) {
|
||||
const status = provider === "google" ? googleStatus : outlookStatus;
|
||||
const status = providerStatus(provider);
|
||||
if (!status.configured || isBusy) return;
|
||||
menuOpen = false;
|
||||
busy = { provider, phase: "connect" };
|
||||
@@ -199,15 +209,13 @@
|
||||
type: connectType,
|
||||
})) as GoogleCalendarSyncResult;
|
||||
if (!result.success) {
|
||||
const label = provider === "google" ? "Google" : "Outlook";
|
||||
showToastMessage(result.error ?? `Could not connect to ${label} Calendar.`, true);
|
||||
showToastMessage(
|
||||
result.error ?? `Could not connect to ${calendarProviderLabel(provider)} Calendar.`,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (provider === "google") {
|
||||
googleStatus = { ...googleStatus, connected: true };
|
||||
} else {
|
||||
outlookStatus = { ...outlookStatus, connected: true };
|
||||
}
|
||||
setProviderStatus(provider, { connected: true });
|
||||
busy = { provider, phase: "sync" };
|
||||
await performSync(provider);
|
||||
} catch (err) {
|
||||
@@ -219,7 +227,7 @@
|
||||
}
|
||||
|
||||
async function syncProvider(provider: CalendarProvider) {
|
||||
const status = provider === "google" ? googleStatus : outlookStatus;
|
||||
const status = providerStatus(provider);
|
||||
if (!status.configured || isBusy) return;
|
||||
if (!status.connected) {
|
||||
await connectProvider(provider);
|
||||
@@ -266,7 +274,7 @@
|
||||
|
||||
const removed = result.deleted ?? 0;
|
||||
modalProvider = null;
|
||||
const label = provider === "google" ? "Google" : "Outlook";
|
||||
const label = calendarProviderLabel(provider);
|
||||
if (removed === 0) {
|
||||
showToastMessage("No synced events to remove.");
|
||||
} else {
|
||||
@@ -298,7 +306,7 @@
|
||||
busy = { provider, phase: "disconnect" };
|
||||
const disconnectType =
|
||||
provider === "google" ? "googleCalendarDisconnect" : "outlookCalendarDisconnect";
|
||||
const label = provider === "google" ? "Google" : "Outlook";
|
||||
const label = calendarProviderLabel(provider);
|
||||
try {
|
||||
const result = (await browser.runtime.sendMessage({
|
||||
type: disconnectType,
|
||||
@@ -307,11 +315,7 @@
|
||||
showToastMessage(`Could not disconnect ${label} Calendar.`, true);
|
||||
return;
|
||||
}
|
||||
if (provider === "google") {
|
||||
googleStatus = { ...googleStatus, connected: false, lastSyncAt: undefined };
|
||||
} else {
|
||||
outlookStatus = { ...outlookStatus, connected: false, lastSyncAt: undefined };
|
||||
}
|
||||
setProviderStatus(provider, { connected: false, lastSyncAt: undefined });
|
||||
showDisconnect = false;
|
||||
menuOpen = false;
|
||||
modalProvider = null;
|
||||
@@ -323,56 +327,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteModal(provider: CalendarProvider) {
|
||||
function openModal(provider: CalendarProvider, kind: "delete" | "disconnect") {
|
||||
if (isBusy) return;
|
||||
modalProvider = provider;
|
||||
menuOpen = false;
|
||||
showDeleteEvents = true;
|
||||
}
|
||||
|
||||
function openDisconnectModal(provider: CalendarProvider) {
|
||||
if (isBusy) return;
|
||||
modalProvider = provider;
|
||||
menuOpen = false;
|
||||
showDisconnect = true;
|
||||
showDisconnect = kind === "disconnect";
|
||||
showDeleteEvents = kind === "delete";
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuOpen = !menuOpen;
|
||||
if (menuOpen) {
|
||||
queueMicrotask(() => syncMenuTheme());
|
||||
}
|
||||
}
|
||||
|
||||
function formatLastSync(ts?: number): string | null {
|
||||
if (!ts) return null;
|
||||
const diff = Date.now() - ts;
|
||||
if (diff < 60_000) return "Synced just now";
|
||||
if (diff < 3_600_000) return `Synced ${Math.floor(diff / 60_000)}m ago`;
|
||||
if (diff < 86_400_000) return `Synced ${Math.floor(diff / 3_600_000)}h ago`;
|
||||
return `Synced ${new Date(ts).toLocaleDateString()}`;
|
||||
if (menuOpen) queueMicrotask(syncHostTheme);
|
||||
}
|
||||
|
||||
function updateMenuPosition() {
|
||||
if (!triggerEl) return;
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
menuStyle = `top:${rect.bottom + 8}px;right:${window.innerWidth - rect.right}px;`;
|
||||
syncMenuTheme();
|
||||
syncHostTheme();
|
||||
}
|
||||
|
||||
function syncMenuTheme() {
|
||||
if (!menuEl) return;
|
||||
syncCalendarSyncTheme(menuEl);
|
||||
}
|
||||
|
||||
function syncMountedTheme() {
|
||||
const themeHost = rootEl?.closest(".bsplus-calendar-sync-mount") as HTMLElement | null;
|
||||
if (themeHost) syncCalendarSyncTheme(themeHost);
|
||||
if (menuOpen) syncMenuTheme();
|
||||
}
|
||||
|
||||
function portalMenu(node: HTMLElement) {
|
||||
return portalToBody(node);
|
||||
function syncHostTheme() {
|
||||
const themeHost = rootEl?.closest(".bsplus-calendar-sync-mount");
|
||||
if (themeHost instanceof HTMLElement) syncCalendarSyncTheme(themeHost);
|
||||
if (menuEl) syncCalendarSyncTheme(menuEl);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -382,7 +360,7 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (menuOpen && menuEl) syncMenuTheme();
|
||||
if (menuOpen && menuEl) syncHostTheme();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -413,7 +391,7 @@
|
||||
"adaptiveThemeGradient",
|
||||
"selectedTheme",
|
||||
] as const;
|
||||
const onThemeChange = () => syncMountedTheme();
|
||||
const onThemeChange = () => syncHostTheme();
|
||||
for (const key of themeKeys) {
|
||||
settingsState.register(key, onThemeChange);
|
||||
}
|
||||
@@ -445,6 +423,72 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet providerPanel(
|
||||
provider: CalendarProvider,
|
||||
status: ProviderStatus,
|
||||
notConfiguredMsg: string,
|
||||
icon: Snippet,
|
||||
name: Snippet,
|
||||
)}
|
||||
<div class="bsplus-cal-provider" role="none">
|
||||
<div class="bsplus-cal-provider-row">
|
||||
<span class="bsplus-cal-provider-icon" aria-hidden="true">
|
||||
{@render icon()}
|
||||
</span>
|
||||
<div class="bsplus-cal-provider-copy">
|
||||
<span class="bsplus-cal-provider-name">{@render name()}</span>
|
||||
<span class="bsplus-cal-provider-status">
|
||||
{providerStatusText(status, notConfiguredMsg)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-cal-provider-actions">
|
||||
{#if !status.connected}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={!status.configured || isBusy}
|
||||
onclick={() => void connectProvider(provider)}
|
||||
>
|
||||
{providerPhase(provider) === "connect" ? "Connecting…" : "Connect & sync"}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => void syncProvider(provider)}
|
||||
>
|
||||
{providerPhase(provider) === "sync" ? "Updating…" : "Update calendar"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openModal(provider, "delete")}
|
||||
>
|
||||
{providerPhase(provider) === "delete" ? "Deleting…" : "Delete synced classes"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openModal(provider, "disconnect")}
|
||||
>
|
||||
Disconnect account
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="bsplus-cal-sync" bind:this={rootEl}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -473,7 +517,7 @@
|
||||
role="menu"
|
||||
bind:this={menuEl}
|
||||
style={menuStyle}
|
||||
use:portalMenu
|
||||
use:portalToBody
|
||||
transition:fly={{ y: -6, duration: 160 }}
|
||||
>
|
||||
<div class="bsplus-cal-menu-header">
|
||||
@@ -481,214 +525,136 @@
|
||||
<span class="bsplus-cal-menu-sub">Copy your SEQTA timetable classes to Google or Outlook</span>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-cal-provider" role="none">
|
||||
<div class="bsplus-cal-provider-row">
|
||||
<span class="bsplus-cal-provider-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22 12c0-.96-.08-1.88-.24-2.76H12v5.22h5.68c-.24 1.28-.96 2.44-2.04 3.18v2.64h3.3c1.92-1.76 3.06-4.36 3.06-7.28z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 22c2.76 0 5.08-.92 6.78-2.5l-3.3-2.64c-.92.62-2.1.98-3.48.98-2.68 0-4.96-1.8-5.78-4.22H2.18v2.72A10 10 0 0 0 12 22z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M6.22 13.62A5.98 5.98 0 0 1 5.82 12c0-.56.1-1.1.28-1.62V7.66H2.18A10 10 0 0 0 2 12c0 1.62.38 3.16 1.06 4.52l3.16-2.9z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.5 0 2.84.52 3.9 1.54l2.92-2.92C17.08 2.34 14.76 1.2 12 1.2 7.54 1.2 3.72 3.94 2.18 7.66l4.04 3.14c.82-2.42 3.1-4.22 5.78-4.22z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="bsplus-cal-provider-copy">
|
||||
<span class="bsplus-cal-provider-name">
|
||||
<span class="bsplus-google-word bsplus-google-word--sm">
|
||||
<span class="bsplus-google-g">G</span><span class="bsplus-google-o1">o</span><span class="bsplus-google-o2">o</span><span class="bsplus-google-g2">g</span><span class="bsplus-google-l">l</span><span class="bsplus-google-e">e</span>
|
||||
</span>
|
||||
<span> Calendar</span>
|
||||
</span>
|
||||
<span class="bsplus-cal-provider-status">
|
||||
{#if !googleStatus.configured}
|
||||
Not available in this build
|
||||
{:else if googleStatus.connected}
|
||||
Connected{formatLastSync(googleStatus.lastSyncAt) ? ` · ${formatLastSync(googleStatus.lastSyncAt)}` : ""}
|
||||
{:else}
|
||||
Not connected
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-cal-provider-actions">
|
||||
{#if !googleStatus.connected}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={!googleStatus.configured || isBusy}
|
||||
onclick={() => void connectProvider("google")}
|
||||
>
|
||||
{providerPhase("google") === "connect" ? "Connecting…" : "Connect & sync"}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => void syncProvider("google")}
|
||||
>
|
||||
{providerPhase("google") === "sync" ? "Updating…" : "Update calendar"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openDeleteModal("google")}
|
||||
>
|
||||
{providerPhase("google") === "delete" ? "Deleting…" : "Delete synced classes"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openDisconnectModal("google")}
|
||||
>
|
||||
Disconnect account
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-cal-provider" role="none">
|
||||
<div class="bsplus-cal-provider-row">
|
||||
<span class="bsplus-cal-provider-icon" aria-hidden="true">
|
||||
<OutlookCalendarIcon />
|
||||
</span>
|
||||
<div class="bsplus-cal-provider-copy">
|
||||
<span class="bsplus-cal-provider-name">Outlook Calendar</span>
|
||||
<span class="bsplus-cal-provider-status">
|
||||
{#if !outlookStatus.configured}
|
||||
Set OUTLOOK_OAUTH_CLIENT_ID to enable
|
||||
{:else if outlookStatus.connected}
|
||||
Connected{formatLastSync(outlookStatus.lastSyncAt) ? ` · ${formatLastSync(outlookStatus.lastSyncAt)}` : ""}
|
||||
{:else}
|
||||
Not connected
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bsplus-cal-provider-actions">
|
||||
{#if !outlookStatus.connected}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={!outlookStatus.configured || isBusy}
|
||||
onclick={() => void connectProvider("outlook")}
|
||||
>
|
||||
{providerPhase("outlook") === "connect" ? "Connecting…" : "Connect & sync"}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--primary"
|
||||
style:--bsplus-cal-accent={accent}
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => void syncProvider("outlook")}
|
||||
>
|
||||
{providerPhase("outlook") === "sync" ? "Updating…" : "Update calendar"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openDeleteModal("outlook")}
|
||||
>
|
||||
{providerPhase("outlook") === "delete" ? "Deleting…" : "Delete synced classes"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-action bsplus-cal-action--ghost"
|
||||
role="menuitem"
|
||||
disabled={isBusy}
|
||||
onclick={() => openDisconnectModal("outlook")}
|
||||
>
|
||||
Disconnect account
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{@render providerPanel(
|
||||
"google",
|
||||
googleStatus,
|
||||
"Not available in this build",
|
||||
googleIcon,
|
||||
googleName,
|
||||
)}
|
||||
{@render providerPanel(
|
||||
"outlook",
|
||||
outlookStatus,
|
||||
"Set OUTLOOK_OAUTH_CLIENT_ID to enable",
|
||||
outlookIcon,
|
||||
outlookName,
|
||||
)}
|
||||
|
||||
{#if anyConnected}
|
||||
<div class="bsplus-cal-settings" role="group" aria-label="Sync options">
|
||||
<label class="bsplus-cal-setting">
|
||||
<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
|
||||
type="number"
|
||||
class="bsplus-cal-setting-input"
|
||||
min={GOOGLE_CALENDAR_SYNC_WEEKS_MIN}
|
||||
max={GOOGLE_CALENDAR_SYNC_WEEKS_MAX}
|
||||
value={syncWeeksAhead}
|
||||
disabled={isBusy}
|
||||
onchange={(e) => void onWeeksAheadChange(e)}
|
||||
/>
|
||||
</label>
|
||||
<label class="bsplus-cal-setting bsplus-cal-setting--toggle">
|
||||
<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
|
||||
type="checkbox"
|
||||
class="bsplus-cal-setting-checkbox"
|
||||
checked={autoSyncWeekly}
|
||||
disabled={isBusy}
|
||||
onchange={(e) => void onAutoSyncToggle(e)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
<label class="bsplus-cal-setting">
|
||||
<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
|
||||
type="number"
|
||||
class="bsplus-cal-setting-input"
|
||||
min={GOOGLE_CALENDAR_SYNC_WEEKS_MIN}
|
||||
max={GOOGLE_CALENDAR_SYNC_WEEKS_MAX}
|
||||
value={syncWeeksAhead}
|
||||
disabled={isBusy}
|
||||
onchange={(e) => void onWeeksAheadChange(e)}
|
||||
/>
|
||||
</label>
|
||||
<label class="bsplus-cal-setting bsplus-cal-setting--toggle">
|
||||
<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
|
||||
type="checkbox"
|
||||
class="bsplus-cal-setting-checkbox"
|
||||
checked={autoSyncWeekly}
|
||||
disabled={isBusy}
|
||||
onchange={(e) => void onAutoSyncToggle(e)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CalendarDeleteEventsModal
|
||||
open={showDeleteEvents}
|
||||
busy={busy?.phase === "delete"}
|
||||
providerLabel={modalProvider === "outlook" ? "Outlook" : "Google"}
|
||||
onCancel={() => {
|
||||
if (busy?.phase !== "delete") showDeleteEvents = false;
|
||||
}}
|
||||
onConfirm={confirmDeleteEvents}
|
||||
/>
|
||||
|
||||
<CalendarDisconnectModal
|
||||
open={showDisconnect}
|
||||
busy={busy?.phase === "disconnect"}
|
||||
providerLabel={modalProvider === "outlook" ? "Outlook" : "Google"}
|
||||
onCancel={() => {
|
||||
if (busy?.phase !== "disconnect") showDisconnect = false;
|
||||
}}
|
||||
onConfirm={confirmDisconnect}
|
||||
/>
|
||||
{#if modalOpen}
|
||||
<div
|
||||
class="bsplus-cal-modal-backdrop"
|
||||
use:portalToBody
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget && !modalBusy) closeModal();
|
||||
}}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Escape" && !modalBusy) closeModal();
|
||||
}}
|
||||
role="presentation"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<div
|
||||
class="bsplus-cal-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="bsplus-cal-modal-title"
|
||||
transition:fade={{ duration: 180 }}
|
||||
>
|
||||
{#if showDisconnect}
|
||||
<h2 id="bsplus-cal-modal-title" class="bsplus-cal-modal-title">
|
||||
Disconnect {providerLabel} Calendar?
|
||||
</h2>
|
||||
<p class="bsplus-cal-modal-body">
|
||||
Stops BetterSEQTA+ from updating your calendar. Synced classes stay in {providerLabel} Calendar
|
||||
until you delete them or connect again.
|
||||
</p>
|
||||
<div class="bsplus-cal-modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--ghost"
|
||||
disabled={modalBusy}
|
||||
onclick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--danger"
|
||||
disabled={modalBusy}
|
||||
onclick={() => void confirmDisconnect()}
|
||||
>
|
||||
{modalBusy ? "Disconnecting…" : "Disconnect account"}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<h2 id="bsplus-cal-modal-title" class="bsplus-cal-modal-title">
|
||||
Delete synced classes?
|
||||
</h2>
|
||||
<p class="bsplus-cal-modal-body">
|
||||
Removes every BetterSEQTA+ timetable event from your {providerLabel} Calendar for this school.
|
||||
Your account stays connected — use Update calendar to sync again.
|
||||
</p>
|
||||
<div class="bsplus-cal-modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--ghost"
|
||||
disabled={modalBusy}
|
||||
onclick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bsplus-cal-btn bsplus-cal-btn--danger"
|
||||
disabled={modalBusy}
|
||||
onclick={() => void confirmDeleteEvents()}
|
||||
>
|
||||
{modalBusy ? "Deleting…" : "Delete synced classes"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toast}
|
||||
<div
|
||||
@@ -702,6 +668,39 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#snippet googleIcon()}
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22 12c0-.96-.08-1.88-.24-2.76H12v5.22h5.68c-.24 1.28-.96 2.44-2.04 3.18v2.64h3.3c1.92-1.76 3.06-4.36 3.06-7.28z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 22c2.76 0 5.08-.92 6.78-2.5l-3.3-2.64c-.92.62-2.1.98-3.48.98-2.68 0-4.96-1.8-5.78-4.22H2.18v2.72A10 10 0 0 0 12 22z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M6.22 13.62A5.98 5.98 0 0 1 5.82 12c0-.56.1-1.1.28-1.62V7.66H2.18A10 10 0 0 0 2 12c0 1.62.38 3.16 1.06 4.52l3.16-2.9z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.5 0 2.84.52 3.9 1.54l2.92-2.92C17.08 2.34 14.76 1.2 12 1.2 7.54 1.2 3.72 3.94 2.18 7.66l4.04 3.14c.82-2.42 3.1-4.22 5.78-4.22z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
{#snippet googleName()}
|
||||
Google Calendar
|
||||
{/snippet}
|
||||
|
||||
{#snippet outlookIcon()}
|
||||
<OutlookCalendarIcon />
|
||||
{/snippet}
|
||||
|
||||
{#snippet outlookName()}
|
||||
Outlook Calendar
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
.bsplus-cal-sync {
|
||||
position: relative;
|
||||
@@ -765,35 +764,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bsplus-google-word {
|
||||
display: inline-flex;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.bsplus-google-word--sm {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bsplus-google-g,
|
||||
.bsplus-google-g2 {
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
.bsplus-google-o1,
|
||||
.bsplus-google-e {
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.bsplus-google-o2 {
|
||||
color: #fbbc05;
|
||||
}
|
||||
|
||||
.bsplus-google-l {
|
||||
color: #34a853;
|
||||
}
|
||||
|
||||
.bsplus-cal-trigger:hover:not(.bsplus-cal-trigger--busy) {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
@@ -999,6 +969,81 @@
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text) 14%, var(--bsplus-cal-surface));
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal {
|
||||
width: min(100%, 400px);
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
background: var(--bsplus-cal-surface, #fff);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid color-mix(in srgb, var(--bsplus-cal-text, #111) 12%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-body {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 72%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 8%, transparent);
|
||||
color: var(--bsplus-cal-text, #111);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--ghost:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 14%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bsplus-cal-btn--danger:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.bsplus-cal-toast {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||
|
||||
let {
|
||||
progress = null,
|
||||
} = $props<{
|
||||
progress?: GoogleCalendarSyncProgress | null;
|
||||
}>();
|
||||
|
||||
const percent = $derived(
|
||||
progress && progress.total > 0
|
||||
? Math.min(100, Math.round((progress.current / progress.total) * 100))
|
||||
: progress?.phase === "preparing"
|
||||
? 8
|
||||
: 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if progress && progress.phase !== "done"}
|
||||
<div class="bsplus-cal-progress" role="status" aria-live="polite" aria-busy="true">
|
||||
<div class="bsplus-cal-progress-label">{progress.message}</div>
|
||||
<div class="bsplus-cal-progress-track" aria-hidden="true">
|
||||
<div class="bsplus-cal-progress-bar" style:width={`${percent}%`}></div>
|
||||
</div>
|
||||
{#if progress.total > 0}
|
||||
<div class="bsplus-cal-progress-meta">{progress.current} / {progress.total}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.bsplus-cal-progress {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 22%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-progress-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bsplus-cal-progress-track {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--bsplus-cal-text, #111) 10%, transparent);
|
||||
}
|
||||
|
||||
.bsplus-cal-progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--bsplus-cal-accent, var(--better-main, #3b82f6));
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.bsplus-cal-progress-meta {
|
||||
margin-top: 6px;
|
||||
font-size: 10px;
|
||||
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 62%, transparent);
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -25,5 +25,4 @@
|
||||
|
||||
.bsplus-calendar-sync-mount {
|
||||
display: inline-flex;
|
||||
font-family: var(--bsplus-cal-font-family, var(--betterseqta-font-family, Rubik), sans-serif);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/** 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"));
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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";
|
||||
|
||||
export const CALENDAR_THEME_CSS_VARS = [
|
||||
"--better-main",
|
||||
"--better-pale",
|
||||
"--better-light",
|
||||
"--text-color",
|
||||
"--background-primary",
|
||||
"--background-secondary",
|
||||
"--text-primary",
|
||||
"--theme-offset-bg",
|
||||
"--better-sub",
|
||||
] as const;
|
||||
|
||||
const ACCENT_CSS_VARS = [
|
||||
"--better-main",
|
||||
"--accent-color-value",
|
||||
"--accentColor",
|
||||
"--colour-betterseqta-blue",
|
||||
] as const;
|
||||
|
||||
export function isCalendarDarkMode(): boolean {
|
||||
return !!settingsState.DarkMode || document.documentElement.classList.contains("dark");
|
||||
}
|
||||
|
||||
function resolvePageAccentColor(): string {
|
||||
const computed = getComputedStyle(document.documentElement);
|
||||
for (const name of ACCENT_CSS_VARS) {
|
||||
const solid = extractSolidColor(computed.getPropertyValue(name));
|
||||
if (solid) return solid;
|
||||
}
|
||||
const fromSettings = settingsState.selectedColor?.trim();
|
||||
if (fromSettings) {
|
||||
const solid = extractSolidColor(fromSettings);
|
||||
if (solid) return solid;
|
||||
}
|
||||
return "#3b82f6";
|
||||
}
|
||||
|
||||
/** Sync extension theme (including dark mode) onto a calendar UI host or portaled menu. */
|
||||
export function syncCalendarSyncTheme(target: HTMLElement): void {
|
||||
const computed = getComputedStyle(document.documentElement);
|
||||
const dark = isCalendarDarkMode();
|
||||
const fontPreset = getFontPreset(settingsState.selectedFont);
|
||||
|
||||
ensureFontLoaded(fontPreset);
|
||||
target.style.setProperty("--bsplus-cal-font-family", fontPreset.stack);
|
||||
|
||||
for (const name of CALENDAR_THEME_CSS_VARS) {
|
||||
const value =
|
||||
document.documentElement.style.getPropertyValue(name).trim() ||
|
||||
computed.getPropertyValue(name).trim();
|
||||
if (value) target.style.setProperty(name, value);
|
||||
}
|
||||
|
||||
const accent = resolvePageAccentColor();
|
||||
target.style.setProperty("--bsplus-cal-accent", accent);
|
||||
target.style.setProperty("--better-main", accent);
|
||||
target.classList.toggle("dark", dark);
|
||||
|
||||
const textPrimary =
|
||||
computed.getPropertyValue("--text-primary").trim() ||
|
||||
computed.getPropertyValue("--text-color").trim();
|
||||
const bgPrimary =
|
||||
computed.getPropertyValue("--background-primary").trim() ||
|
||||
computed.getPropertyValue("--background-secondary").trim() ||
|
||||
computed.getPropertyValue("--theme-offset-bg").trim();
|
||||
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-text",
|
||||
textPrimary || (dark ? "#f4f4f5" : "#18181b"),
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-surface",
|
||||
bgPrimary || (dark ? "#27272a" : "#ffffff"),
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-surface-muted",
|
||||
dark ? "#3f3f46" : "color-mix(in srgb, var(--bsplus-cal-text) 5%, var(--bsplus-cal-surface))",
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-border",
|
||||
dark ? "color-mix(in srgb, #ffffff 14%, transparent)" : "color-mix(in srgb, var(--bsplus-cal-text) 12%, transparent)",
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,133 @@
|
||||
import { mount, unmount } from "svelte";
|
||||
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 type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
|
||||
import CalendarSyncControl from "./CalendarSyncControl.svelte";
|
||||
import { syncCalendarSyncTheme } from "./calendarSyncTheme";
|
||||
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
||||
import hostStyles from "./calendarSyncHost.css?inline";
|
||||
|
||||
export type CalendarProvider = "google" | "outlook";
|
||||
|
||||
export function providerLabel(provider: CalendarProvider): string {
|
||||
return provider === "google" ? "Google" : "Outlook";
|
||||
}
|
||||
|
||||
export function formatLastSync(ts?: number): string | null {
|
||||
if (!ts) return null;
|
||||
const diff = Date.now() - ts;
|
||||
if (diff < 60_000) return "Synced just now";
|
||||
if (diff < 3_600_000) return `Synced ${Math.floor(diff / 60_000)}m ago`;
|
||||
if (diff < 86_400_000) return `Synced ${Math.floor(diff / 3_600_000)}h ago`;
|
||||
return `Synced ${new Date(ts).toLocaleDateString()}`;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
const CONTROLS_CLASS = "timetable-calendar-controls";
|
||||
const HOST_STYLE_ID = "bsplus-calendar-sync-host-styles";
|
||||
|
||||
const THEME_CSS_VARS = [
|
||||
"--better-main",
|
||||
"--better-pale",
|
||||
"--better-light",
|
||||
"--text-color",
|
||||
"--background-primary",
|
||||
"--background-secondary",
|
||||
"--text-primary",
|
||||
"--theme-offset-bg",
|
||||
"--better-sub",
|
||||
] as const;
|
||||
|
||||
const ACCENT_CSS_VARS = [
|
||||
"--better-main",
|
||||
"--accent-color-value",
|
||||
"--accentColor",
|
||||
"--colour-betterseqta-blue",
|
||||
] as const;
|
||||
|
||||
let currentApp: ReturnType<typeof mount> | null = null;
|
||||
let mountRoot: HTMLElement | null = null;
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/** Sync extension theme (including dark mode) onto a calendar UI host or portaled menu. */
|
||||
export function syncCalendarSyncTheme(target: HTMLElement): void {
|
||||
const computed = getComputedStyle(document.documentElement);
|
||||
const dark = !!settingsState.DarkMode || document.documentElement.classList.contains("dark");
|
||||
const fontPreset = getFontPreset(settingsState.selectedFont);
|
||||
|
||||
ensureFontLoaded(fontPreset);
|
||||
target.style.setProperty("--bsplus-cal-font-family", fontPreset.stack);
|
||||
|
||||
for (const name of THEME_CSS_VARS) {
|
||||
const value =
|
||||
document.documentElement.style.getPropertyValue(name).trim() ||
|
||||
computed.getPropertyValue(name).trim();
|
||||
if (value) target.style.setProperty(name, value);
|
||||
}
|
||||
|
||||
let accent = "#3b82f6";
|
||||
for (const name of ACCENT_CSS_VARS) {
|
||||
const solid = extractSolidColor(computed.getPropertyValue(name));
|
||||
if (solid) {
|
||||
accent = solid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (accent === "#3b82f6") {
|
||||
const fromSettings = settingsState.selectedColor?.trim();
|
||||
if (fromSettings) {
|
||||
const solid = extractSolidColor(fromSettings);
|
||||
if (solid) accent = solid;
|
||||
}
|
||||
}
|
||||
target.style.setProperty("--bsplus-cal-accent", accent);
|
||||
target.style.setProperty("--better-main", accent);
|
||||
target.classList.toggle("dark", dark);
|
||||
|
||||
const textPrimary =
|
||||
computed.getPropertyValue("--text-primary").trim() ||
|
||||
computed.getPropertyValue("--text-color").trim();
|
||||
const bgPrimary =
|
||||
computed.getPropertyValue("--background-primary").trim() ||
|
||||
computed.getPropertyValue("--background-secondary").trim() ||
|
||||
computed.getPropertyValue("--theme-offset-bg").trim();
|
||||
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-text",
|
||||
textPrimary || (dark ? "#f4f4f5" : "#18181b"),
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-surface",
|
||||
bgPrimary || (dark ? "#27272a" : "#ffffff"),
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-surface-muted",
|
||||
dark ? "#3f3f46" : "color-mix(in srgb, var(--bsplus-cal-text) 5%, var(--bsplus-cal-surface))",
|
||||
);
|
||||
target.style.setProperty(
|
||||
"--bsplus-cal-border",
|
||||
dark ? "color-mix(in srgb, #ffffff 14%, transparent)" : "color-mix(in srgb, var(--bsplus-cal-text) 12%, transparent)",
|
||||
);
|
||||
}
|
||||
|
||||
function ensureHostStyles() {
|
||||
if (document.getElementById(HOST_STYLE_ID)) return;
|
||||
@@ -23,8 +142,6 @@ function teardown() {
|
||||
unmount(currentApp);
|
||||
currentApp = null;
|
||||
}
|
||||
|
||||
mountRoot = null;
|
||||
document.querySelector(`.${CONTROLS_CLASS}`)?.remove();
|
||||
document.getElementById(HOST_STYLE_ID)?.remove();
|
||||
}
|
||||
@@ -42,7 +159,7 @@ export async function mountGoogleCalendarButton(): Promise<void> {
|
||||
controls.className = `${CONTROLS_CLASS} bsplus-timetable-control`;
|
||||
toolbar.appendChild(controls);
|
||||
|
||||
mountRoot = document.createElement("div");
|
||||
const mountRoot = document.createElement("div");
|
||||
mountRoot.className = "bsplus-calendar-sync-mount";
|
||||
syncCalendarSyncTheme(mountRoot);
|
||||
controls.appendChild(mountRoot);
|
||||
|
||||
Reference in New Issue
Block a user