diff --git a/docs/CLOUD_SETTINGS_SYNC_OPTIONS.md b/docs/CLOUD_SETTINGS_SYNC_OPTIONS.md new file mode 100644 index 00000000..34d88bbc --- /dev/null +++ b/docs/CLOUD_SETTINGS_SYNC_OPTIONS.md @@ -0,0 +1,1230 @@ +# Cloud settings sync — complete options & format reference + +Exhaustive reference for every value that **is** or **would be** included in a BetterSEQTA Cloud settings backup, the exact JSON shapes stored in `chrome.storage.local`, and how those shapes appear on the wire. + +**Related docs** + +- Server HTTP contract: [CLOUD_SETTINGS_SYNC_SERVER.md](./CLOUD_SETTINGS_SYNC_SERVER.md) +- Client upload/download: `src/seqta/utils/cloudSettingsSync.ts` +- Auto-sync (debounce, poll, triggers): `src/background/cloudSettingsAutoSync.ts` +- Core defaults: `src/seqta/utils/defaultSettings.ts` +- Full-schema initializer: `src/seqta/utils/ensureSyncableStorageDefaults.ts` +- In-memory settings + persistence: `src/seqta/utils/listeners/SettingsState.ts` +- Type definitions: `src/types/storage.ts` + +--- + +## Table of contents + +1. [Architecture](#architecture) +2. [Sync lifecycle](#sync-lifecycle) +3. [Wire format (HTTP body)](#wire-format-http-body) +4. [Local storage model](#local-storage-model) +5. [Exclusion rules](#exclusion-rules) +6. [Legacy key migration](#legacy-key-migration) +7. [Core extension settings](#core-extension-settings) +8. [Plugin settings objects](#plugin-settings-objects) +9. [Plugin runtime storage keys](#plugin-runtime-storage-keys) +10. [Excluded caches (local-only schemas)](#excluded-caches-local-only-schemas) +11. [Data outside `chrome.storage.local`](#data-outside-chromestoragelocal) +12. [Catch-all & forward compatibility](#catch-all--forward-compatibility) +13. [UI accessibility](#ui-accessibility) +14. [Default schema initialization](#default-schema-initialization) + +--- + +## Architecture + +Cloud settings sync is a **whole-snapshot backup** of extension local storage: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ chrome.storage.local (flat string-keyed JSON values) │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │ +│ │ Core keys │ │ plugin.* │ │ Excluded (see §5) │ │ +│ │ onoff, │ │ .settings │ │ OAuth, device caches, │ │ +│ │ DarkMode, │ │ .storage.* │ │ client watermarks │ │ +│ │ menuitems… │ │ │ │ │ │ +│ └─────────────┘ └──────────────┘ └────────────────────────┘ │ +└────────────────────────────┬────────────────────────────────────┘ + │ buildUploadPayload() + │ • filter exclusions + │ • migrateLegacyToPluginSettings() + ▼ + PUT /api/bsplus/settings/sync + { schemaVersion, themeId, data: { … } } + │ + ▼ + accounts.betterseqta.org (one row per user) +``` + +**Design principle:** inclusive by default. Any new key written to `chrome.storage.local` is uploaded unless explicitly added to an omit list in `cloudSettingsSync.ts`. + +**Not included:** IndexedDB, `localStorage`, `sessionStorage`, or SEQTA page DOM state. + +--- + +## Sync lifecycle + +### When upload runs + +| Trigger | Behaviour | +|---------|-----------| +| Any **included** `chrome.storage.local` key changes | Debounced upload after **2000 ms** (`UPLOAD_DEBOUNCE_MS`) | +| Manual upload (settings UI) | Immediate `PUT` | +| First poll with no cloud backup and no local watermark | Baseline upload | +| `requestCloudSettingsDebouncedUpload()` | Same debounced path (e.g. after theme install) | + +Upload is skipped when: + +- `autoCloudSettingsSync === false` +- No `bsplus_token` (not signed in) +- `suppressAutoUploadDuringRestore` is true (during download) + +### When download runs + +| Trigger | Behaviour | +|---------|-----------| +| Manual restore (settings UI) | `GET` → `applyDownloadedEnvelope` → theme prefetch → reload SEQTA tabs | +| Auto poll | If `bsplus.updated_at` from `GET /api/user/cloud-summary` is newer than local watermark | +| First poll with cloud backup but no watermark | Full download | + +Download is skipped when server `schemaVersion` > client `CLOUD_SETTINGS_SYNC_SCHEMA_VERSION` (currently `1`). + +### Poll throttle + +- Key: `bsplus_lastCloudPoll` — **never uploaded** +- Value: `number` (Unix ms timestamp) +- Minimum interval between poll runs: **24 hours** (`POLL_THROTTLE_MS`) + +### Restore semantics + +`applyDownloadedEnvelope` calls `browser.storage.local.set(remoteSanitized)` with **only** keys from the server (after migration + strip). It does **not** wipe storage: + +- **OAuth keys** remain because they are stripped from the remote blob and never overwritten. +- **Excluded device caches** remain for the same reason. +- **Client-only keys** (`bsplus_cloud_settings_known_remote_updated_at`, etc.) remain unless accidentally present in an old server payload (stripped defensively). +- Keys present locally but **absent** from an older cloud snapshot are **not deleted**. + +After download, if `themeId` / `selectedTheme` is non-empty, the service worker sets `bsplus_pending_theme_ensure_after_cloud` so the page `ThemeManager` can download missing theme assets from the store. + +### Full schema before upload + +`ensureSyncableStorageDefaults()` (`src/seqta/utils/ensureSyncableStorageDefaults.ts`) ensures every **cloud-syncable** key exists in `chrome.storage.local` with its default value if it was previously absent. This makes uploads and dev JSON exports contain a complete schema (e.g. `customshortcuts: []`, `shortcuts: [...]`, every `plugin.{id}.settings` object) instead of omitting keys the user never touched. + +| When it runs | Context | +|--------------|---------| +| `browser.runtime.onInstalled` | Service worker (install + update) | +| `browser.runtime.onStartup` | Service worker | +| Service worker load | `background.ts` (once at startup) | +| `initializeSettingsState()` | SEQTA content script + extension settings page (first init) | + +Rules: + +- Builds defaults from `getDefaultSettingsState()` plus each plugin’s `plugin.{id}.settings` defaults from `getAllPluginSettings()`. +- **Does not backfill legacy keys** (`animatedbk`, `bksliderinput`, etc.) — missing `plugin.*.settings` are derived from legacy via `migrateLegacyToPluginSettings(existing)` when patched. +- **Does not backfill optional keys** where `undefined` is intentional (`timeFormat`, `selectedFont`, dev/privacy/announcement flags, etc.) so existing behaviour is unchanged. +- Skips keys in the cloud omit lists (`isKeyIncludedInCloudUploadPayload`). +- **Never overwrites** existing storage values — only patches keys absent from storage (`key in existing` is false). +- Settings writes during bootstrap do not fire UI listeners (`SettingsState.bootstrapping`); user edits persist **only the changed key**, not the whole in-memory object. + +--- + +## Wire format (HTTP body) + +### `PUT /api/bsplus/settings/sync` request + +```http +PUT /api/bsplus/settings/sync HTTP/1.1 +Host: accounts.betterseqta.org +Authorization: Bearer +Content-Type: application/json +``` + +```json +{ + "schemaVersion": 1, + "themeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "data": { + "onoff": true, + "DarkMode": true, + "selectedTheme": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "selectedColor": "linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)", + "plugin.global-search.settings": { + "enabled": true, + "searchHotkey": "ctrl+k", + "showRecentFirst": true, + "transparencyEffects": true, + "runIndexingOnLoad": true, + "passiveIndexing": true + } + } +} +``` + +| Top-level field | JSON type | Rules | +|-----------------|-----------|-------| +| `schemaVersion` | `number` | Always `1` today (`CLOUD_SETTINGS_SYNC_SCHEMA_VERSION`) | +| `themeId` | `string` | `normalizeThemeIdForSync(selectedTheme)` — trimmed; `""` if unset/invalid | +| `data` | `object` | Flat map: storage key → value. Same types as `chrome.storage.local`. No nesting convention beyond whatever each key stores. | + +### `GET /api/bsplus/settings/sync` response + +Same shape as the PUT body, plus optional: + +```json +{ + "schemaVersion": 1, + "themeId": "…", + "data": { }, + "updated_at": "2026-04-07T12:00:00.000Z" +} +``` + +- `updated_at`: ISO 8601 UTC string; written to `bsplus_cloud_settings_known_remote_updated_at` locally (never re-uploaded). + +### JSON encoding notes + +- All values must be JSON-serializable (`boolean`, `number`, `string`, `null`, arrays, plain objects). +- `undefined` is never stored by the WebExtension storage API. +- Dates are stored as **strings** (ISO or calendar formats), not `Date` objects. +- `chrome.storage.local` may historically stringify some booleans in edge cases; the client treats them as booleans after read. + +--- + +## Local storage model + +### Flat key namespace + +Every persisted setting is a **top-level key** in `chrome.storage.local`: + +| Pattern | Example | Value shape | +|---------|---------|---------------| +| Core setting | `DarkMode` | scalar or structured JSON | +| Plugin settings | `plugin.global-search.settings` | single JSON **object** with all plugin prefs | +| Plugin storage | `plugin.messageFolders.storage.folders` | one JSON value per property | +| Analytics cache | `bsplus.analytics.v2.https://school.seqta.com.au.12345` | structured cache (excluded) | + +Plugin settings use `plugin.{pluginId}.settings` where `pluginId` matches the plugin registration id (e.g. `messageFolders`, not `message-folders`). + +Plugin runtime storage uses `plugin.{pluginId}.storage.{propertyName}` — each property is a **separate** storage key, not nested under one object. + +### Settings vs storage + +| Mechanism | API | Persisted keys | Synced? | +|-----------|-----|----------------|---------| +| `settingsState` / `SettingsState` | Proxy over in-memory + `storage.local` | Top-level keys (`onoff`, `menuitems`, …) | Yes (unless excluded) | +| Plugin `api.settings` | Proxy; one object per plugin | `plugin.{id}.settings` | Yes | +| Plugin `api.storage` | Proxy; one key per property | `plugin.{id}.storage.{prop}` | Yes, except excluded prefixes | + +Component/button plugin settings (`type: "component"` | `"button"`) are **not** written by the settings proxy loop; only scalar settings defined in `plugin.settings` are persisted automatically. The settings UI may still write `enabled` and other keys via `browser.storage.local.set` directly. + +--- + +## Exclusion rules + +Implemented in `shouldOmitKeyFromCloudPayload(key)`: + +### Exact key exclusions + +| Key | Format if present locally | On restore | +|-----|---------------------------|------------| +| `bsplus_token` | `string` JWT | Keep device value | +| `bsplus_refresh_token` | `string` | Keep device value | +| `bsplus_client_id` | `string` UUID | Keep device value | +| `bsplus_user` | `CloudUser` object (see below) | Keep device value | +| `cloudAccessToken` | `string` (legacy) | Keep device value | +| `cloudUsername` | `string` (legacy) | Keep device value | +| `plugin.assessments-average.storage.assessments` | object | Keep device value | +| `plugin.assessments-average.storage.weightings` | object | Keep device value | +| `bsplus_cloud_settings_known_remote_updated_at` | ISO `string` | Keep device value | +| `bsplus_lastCloudPoll` | `number` (ms) | Keep device value | +| `bsplus_pending_theme_ensure_after_cloud` | `string` (theme id) | Keep device value | + +#### `bsplus_user` shape (local only, never uploaded) + +```json +{ + "id": "uuid", + "email": "user@example.com", + "username": "optional", + "displayName": "optional", + "pfpUrl": "https://…", + "pfpHash": "abc123" , + "admin_level": 0 +} +``` + +All fields except `id` are optional. + +### Prefix exclusions + +| Prefix | Matches | +|--------|---------| +| `plugin.global-search.storage.` | Any Global Search device cache key | +| `bsplus.analytics.` | Grade Analytics caches and chart mode prefs | + +Prefix check: `key.startsWith(prefix)`. + +--- + +## Legacy key migration + +Runs in `migrateLegacyToPluginSettings()` on **both** upload and download. Legacy keys are **removed** from `data` after migration. Migration only fills plugin settings fields that are still `undefined`. + +| Legacy key | Legacy format | Target key | Target field | Conversion | +|------------|---------------|------------|--------------|------------| +| `animatedbk` | `boolean` | `plugin.animated-background.settings` | `enabled` | `!!animatedbk` | +| `bksliderinput` | `string` `"0"`–`"100"` | `plugin.animated-background.settings` | `speed` | `speed = round((0.1 + (parseFloat(s) / 100) * 1.9) * 100) / 100` → range **0.1–2.0** | +| `assessmentsAverage` | `boolean` | `plugin.assessments-average.settings` | `enabled` | `!!assessmentsAverage` | +| `lettergrade` | `boolean` | `plugin.assessments-average.settings` | `lettergrade` | `!!lettergrade` | +| `notificationCollector` | `boolean` only | `plugin.notificationCollector.settings` | `enabled` | copy boolean | + +**Example:** legacy `bksliderinput: "50"` → `speed: 1.05` because `0.1 + 0.5 * 1.9 = 1.05`. + +Modern clients should only see plugin-format keys in uploaded payloads; legacy keys may still exist on very old profiles until first sync. + +--- + +## Core extension settings + +Top-level `chrome.storage.local` keys from `SettingsState` (`src/types/storage.ts`). Defaults from `getDefaultSettingsState()` in `src/seqta/utils/defaultSettings.ts` unless noted. Missing keys are backfilled by `ensureSyncableStorageDefaults()` (see [Default schema initialization](#default-schema-initialization)). + +### Master & appearance + +#### `onoff` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` | +| **UI** | Settings → “BetterSEQTA+” master switch | +| **Example** | `true` | + +#### `DarkMode` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` | +| **UI** | SEQTA UI / theme system | +| **Example** | `true` | + +#### `selectedTheme` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | `""` | +| **UI** | Theme selector / store install | +| **Format** | BetterSEQTA store theme UUID, **flavour (slave) variant** id, or empty string for no store theme | +| **Wire** | Duplicated as top-level `themeId` on upload (trimmed) | +| **Example** | `"f47ac10b-58cc-4372-a567-0e02b2c3d479"` or `""` | + +Theme **asset blobs** (CSS, images) live in **localforage/IndexedDB**, not in this key. After restore, only the id is synced; assets are re-fetched if missing. + +#### `selectedColor` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | `"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)"` | +| **UI** | Colour picker | +| **Format** | Any valid CSS `background` value: hex (`#c93d00`), `rgb()`, `rgba()`, `linear-gradient(...)`, etc. | +| **Example** | `"#c93d00"` or `"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)"` | + +#### `originalSelectedColor` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | `""` | +| **Purpose** | Colour before theme preview; restored when clearing theme | +| **Example** | `"#c93d00"` | + +#### `originalDarkMode` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | undefined until theme preview | +| **Purpose** | Dark mode before theme preview | +| **Example** | `true` | + +#### `transparencyEffects` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `false` | +| **UI** | Settings → Transparency Effects | +| **Example** | `false` | + +#### `animations` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` on normal devices; `false` if low-end (`hardwareConcurrency < 4` or `deviceMemory <= 2`) | +| **UI** | Settings → Animations | +| **Example** | `true` | + +#### `iconOnlySidebar` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `false` | +| **UI** | Settings → Icon Only Sidebar | +| **Example** | `false` | + +#### `selectedFont` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | undefined → runtime treats as `"rubik"` | +| **UI** | Settings → Interface Font | +| **Format** | Font preset id from `src/seqta/ui/fonts/presets.ts` | + +Allowed ids: + +`rubik`, `inter`, `poppins`, `nunito`, `montserrat`, `open-sans`, `lato`, `source-sans-3`, `raleway`, `dm-sans`, `plus-jakarta-sans`, `outfit`, `roboto`, `work-sans`, `manrope`, `figtree`, `lexend`, `ubuntu`, `karla`, `quicksand`, `ibm-plex-sans`, `space-grotesk`, `mulish`, `cabin`, `oswald`, `merriweather`, `playfair-display`, `lora`, `crimson-pro`, `libre-baskerville`, `system` + +**Example:** `"inter"` + +#### `adaptiveThemeColour` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `false` | +| **Example** | `false` | + +#### `adaptiveThemeGradient` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `false` | +| **Example** | `false` | + +#### `adaptiveThemeColourTransition` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` | +| **Example** | `true` | + +### Navigation & layout + +#### `defaultPage` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | `"home"` | +| **UI** | Settings → Default Page | +| **Allowed** | `"home"`, `"dashboard"`, `"timetable"`, `"welcome"`, `"messages"`, `"documents"`, `"reports"` | +| **Example** | `"home"` | + +#### `timeFormat` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | undefined (24-hour behaviour) | +| **UI** | Settings → “12 Hour Time” switch | +| **Values** | `"12"` when enabled; `"24"` when disabled | +| **Example** | `"12"` | + +#### `lessonalert` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` | +| **Purpose** | Lesson alert on home page | +| **Example** | `true` | + +#### `menuitems` + +| | | +|-|-| +| **Type** | `object` | +| **Default** | All sidebar items `{ toggle: true }` | +| **UI** | Menu editor overlay | +| **Shape** | `{ [menuKey]: { toggle: boolean } }` | + +Keys (from `SettingsState`): + +`assessments`, `courses`, `dashboard`, `documents`, `forums`, `goals`, `home`, `messages`, `myed`, `news`, `notices`, `portals`, `reports`, `settings`, `timetable`, `welcome` + +**Example:** + +```json +{ + "home": { "toggle": true }, + "timetable": { "toggle": false }, + "messages": { "toggle": true } +} +``` + +#### `menuorder` + +| | | +|-|-| +| **Type** | `array` | +| **Default** | `[]` | +| **UI** | Menu editor (drag order) | +| **Item type** | `string` — SEQTA menu `data-key` values (e.g. `"home"`, `"timetable"`, `"analytics"` for plugin-injected items) | +| **Example** | `["home", "timetable", "assessments", "messages"]` | + +#### `defaultmenuorder` + +| | | +|-|-| +| **Type** | `array` | +| **Default** | `[]` (populated from DOM on first menu edit) | +| **Item type** | Same as `menuorder` — snapshot of school default order | +| **Example** | `["home", "dashboard", "timetable"]` | + +### Shortcuts + +#### `shortcuts` + +| | | +|-|-| +| **Type** | `array` | +| **Default** | Outlook, Office, Google — each `enabled: true` | +| **UI** | Settings → Shortcuts | +| **Item shape** | `{ "name": string, "enabled": boolean }` | +| **Notes** | `name` matches keys in `src/seqta/content/links.json` (e.g. `"Outlook"`, `"YouTube"`) | + +**Example:** + +```json +[ + { "name": "Outlook", "enabled": true }, + { "name": "Office", "enabled": false }, + { "name": "Google", "enabled": true } +] +``` + +#### `customshortcuts` + +| | | +|-|-| +| **Type** | `array` | +| **Default** | `[]` | +| **UI** | Settings → Shortcuts → add custom | +| **Item shape** | `{ "name": string, "url": string, "icon": string }` | +| **`url`** | Full URL with protocol, e.g. `"https://example.com"` | +| **`icon`** | Inline **SVG markup string** OR single fallback character (first letter of title) | + +**Example:** + +```json +[ + { + "name": "School Portal", + "url": "https://portal.school.edu.au", + "icon": "" + } +] +``` + +### Subjects & news + +#### `subjectfilters` + +| | | +|-|-| +| **Type** | `object` | +| **Default** | `{}` | +| **UI** | Home / assessments subject filters | +| **Shape** | `{ [subjectCode: string]: boolean }` — `false` hides subject; missing key = visible | +| **Example** | `{ "10MAT": false, "10ENG": true }` | + +#### `newsSource` + +| | | +|-|-| +| **Type** | `string` | +| **Default** | `"australia"` | +| **UI** | Settings → News Feed Source | +| **Allowed** | `australia`, `usa`, `uk`, `taiwan`, `hong_kong`, `panama`, `canada`, `singapore`, `japan`, `netherlands` | +| **Example** | `"australia"` | + +### Cloud sync preference + +#### `autoCloudSettingsSync` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `true` | +| **UI** | Settings → Cloud Settings Sync | +| **Semantics** | Sync runs when **not** strictly `false` (default-on) | +| **Example** | `true` | + +### Theme of the Month + +#### `themeOfTheMonthDisabled` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | `false` | +| **UI** | Global Search plugin section → Theme of the Month switch (inverted in UI) | +| **Example** | `false` | + +#### `themeOfTheMonthDismissedMonth` + +| | | +|-|-| +| **Type** | `string` | +| **Format** | `"YYYY-MM"` calendar month | +| **Example** | `"2026-06"` | + +#### `themeOfTheMonthLastSeenId` + +| | | +|-|-| +| **Type** | `string` | +| **Status** | Deprecated; may still exist in old profiles | +| **Synced** | Yes if present | + +### One-time announcements & privacy + +#### `privacyStatementShown` + +| | | +|-|-| +| **Type** | `boolean` | +| **Example** | `true` | + +#### `privacyStatementLastUpdated` + +| | | +|-|-| +| **Type** | `string` | +| **Format** | ISO date, e.g. `"2025-12-20"` | + +#### `engageParentsAnnouncementShown` + +| | | +|-|-| +| **Type** | `boolean` | +| **Purpose** | SEQTA Engage parents announcement dismissed | + +#### `bsCloudAutoSyncAnnouncementShown` + +| | | +|-|-| +| **Type** | `boolean` | +| **Purpose** | Cloud auto-sync announcement dismissed | + +### Update & developer flags + +#### `justupdated` + +| | | +|-|-| +| **Type** | `boolean` | +| **Purpose** | Set `true` after extension update; drives startup popup queue | +| **Example** | `true` | + +#### `devMode` + +| | | +|-|-| +| **Type** | `boolean` | +| **Default** | undefined / false | +| **UI** | Hidden unlock in settings | +| **Example** | `true` | + +#### `hideSensitiveContent` + +| | | +|-|-| +| **Type** | `boolean` | +| **UI** | Dev mode → Sensitive Hider | +| **Example** | `false` | + +#### `mockNotices` + +| | | +|-|-| +| **Type** | `boolean` | +| **UI** | Dev mode → Mock Notices | +| **Example** | `false` | + +#### `devGhReleaseVersionOverride` + +| | | +|-|-| +| **Type** | `string` | +| **UI** | Dev mode → version override field | +| **Format** | Semver string, e.g. `"3.7.0"` | +| **Example** | `"99.0.0"` | + +#### `lastSeenNightlyPublishedAt` + +| | | +|-|-| +| **Type** | `string` | +| **Format** | ISO 8601 timestamp from GitHub release `published_at` | +| **Example** | `"2026-06-01T04:30:00Z"` | + +### Profile picture helper + +#### `profile_picture_revision` + +| | | +|-|-| +| **Type** | `number` | +| **Purpose** | Incremented when cloud/local profile picture changes; triggers UI refresh | +| **Not** | The image itself (stored in localforage `profile-picture-store`) | +| **Example** | `3` | + +--- + +## Plugin settings objects + +Each plugin stores one object at `plugin.{pluginId}.settings`. Plugins with `disableToggle: true` also store `enabled: boolean` (written from settings UI, not the settings proxy). + +Settings of type `component` or `button` are **not** auto-persisted by the plugin settings proxy. + +### `plugin.animated-background.settings` + +| Field | Type | Default | Range / notes | +|-------|------|---------|---------------| +| `enabled` | `boolean` | `true` | From `disableToggle` UI | +| `speed` | `number` | `1` | **0.1–2.0**, step 0.05 in UI | + +```json +{ "enabled": true, "speed": 1.05 } +``` + +### `plugin.assessments-average.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `false` | +| `lettergrade` | `boolean` | `false` | + +```json +{ "enabled": true, "lettergrade": false } +``` + +### `plugin.notificationCollector.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `true` | + +```json +{ "enabled": true } +``` + +### `plugin.timetable.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `true` | + +```json +{ "enabled": true } +``` + +### `plugin.timetableEdit.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `true` | + +```json +{ "enabled": true } +``` + +### `plugin.global-search.settings` + +| Field | Type | Default | Notes | +|-------|------|---------|-------| +| `enabled` | `boolean` | `false` | Plugin default off | +| `searchHotkey` | `string` | `"ctrl+k"` or `"cmd+k"` | See hotkey format below | +| `showRecentFirst` | `boolean` | `true` | | +| `transparencyEffects` | `boolean` | `true` | Search bar blur | +| `runIndexingOnLoad` | `boolean` | `true` | | +| `passiveIndexing` | `boolean` | `true` | Index visited pages | + +```json +{ + "enabled": true, + "searchHotkey": "ctrl+shift+f", + "showRecentFirst": true, + "transparencyEffects": true, + "runIndexingOnLoad": true, + "passiveIndexing": true +} +``` + +#### Hotkey string format (`searchHotkey`) + +- Lowercase, `+`-separated: `modifier[+modifier]+key` +- Modifiers: `ctrl`/`control`, `cmd`/`meta`/`command`, `alt`/`option`, `shift` +- Key: single character or name (`k`, `f`, etc.) — matched against `event.key` case-insensitively +- Valid iff at least one non-modifier key is present (`isValidHotkey`) +- **Examples:** `"ctrl+k"`, `"cmd+shift+p"`, `"alt+f"` + +### `plugin.profile-picture.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `false` | +| `useCloudPfp` | `boolean` | `false` | + +```json +{ "enabled": true, "useCloudPfp": true } +``` + +When `useCloudPfp` is true, avatar URL comes from `bsplus_user.pfpUrl` (local auth, not synced) — other devices need their own login. + +### `plugin.messageFolders.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `true` | +| `showTagsInAllMessages` | `boolean` | `true` | +| `hideFolderedMessagesInAll` | `boolean` | `true` | + +```json +{ + "enabled": true, + "showTagsInAllMessages": true, + "hideFolderedMessagesInAll": true +} +``` + +### `plugin.enhanced-navigation.settings` + +| Field | Type | Default | +|-------|------|---------| +| `enabled` | `boolean` | `true` | +| `autoScrollOnClick` | `boolean` | `false` | + +```json +{ "enabled": true, "autoScrollOnClick": false } +``` + +### `plugin.background-music.settings` + +| Field | Type | Default | Range | +|-------|------|---------|-------| +| `enabled` | `boolean` | `false` | | +| `volume` | `number` | `0.5` | 0–1 | +| `pauseOnHidden` | `boolean` | `true` | | + +```json +{ "enabled": true, "volume": 0.75, "pauseOnHidden": true } +``` + +Audio blob: localforage `background-music-store` / `music` / key `audio-blob` — **not synced**. + +### `plugin.grade-analytics.settings` + +| Field | Type | Default | Range | +|-------|------|---------|-------| +| `cacheTtlHours` | `number` | `24` | 1–168 | + +```json +{ "cacheTtlHours": 48 } +``` + +No `enabled` field — plugin uses `disableToggle: false` and always runs when registered (except Engage). + +### Unused plugin settings keys + +- `plugin.themes.settings` — not written (empty plugin settings) +- `plugin.assessments-overview.settings` — not written + +--- + +## Plugin runtime storage keys + +Separate top-level keys per property: `plugin.{pluginId}.storage.{propertyName}`. + +### Message Folders (`plugin.messageFolders`) + +#### `plugin.messageFolders.storage.folders` + +```json +[ + { + "id": "m1abc2def", + "name": "Important", + "color": "#3b82f6", + "emoji": "" + } +] +``` + +| Field | Type | Notes | +|-------|------|-------| +| `id` | `string` | `Date.now().toString(36) + random` | +| `name` | `string` | Display name | +| `color` | `string` | Hex colour from preset palette | +| `emoji` | `string` | Inline SVG icon markup (not unicode emoji) | + +#### `plugin.messageFolders.storage.messageAssignments` + +```json +{ + "msg-uuid-1": ["folderId1"], + "msg-uuid-2": ["folderId1", "folderId2"] +} +``` + +Keys: SEQTA message identifiers. Values: arrays of folder `id` strings. + +### Timetable Edit (`plugin.timetableEdit`) + +#### `plugin.timetableEdit.storage.timetableOverrides` + +Keyed by class instance id (`ci` as string): + +```json +{ + "42": { "room": "B204", "staff": "Mr Smith" }, + "17": { "staff": "Ms Jones" } +} +``` + +#### `plugin.timetableEdit.storage.timetableOverridesBySubject` + +Keyed by subject description string: + +```json +{ + "10 Mathematics": { "room": "MA1" } +} +``` + +Entry shape: `{ "room"?: string, "staff"?: string }` — either field optional. + +### Assessment Averages (`plugin.assessments-average`) + +#### `plugin.assessments-average.storage.weightingOverrides` — **synced** + +User manual weight overrides by assessment id: + +```json +{ + "123456": "25", + "123457": "N/A" +} +``` + +Values are weight strings as shown in UI (percentage text or `"N/A"`). + +#### `plugin.assessments-average.storage.assessments` — **NOT synced** + +Title → assessment id map (school data): + +```json +{ + "Semester 1 Exam": "123456" +} +``` + +#### `plugin.assessments-average.storage.weightings` — **NOT synced** + +```json +{ + "123456": { + "weight": "25", + "fingerprint": "[\"GRADED\",true,\"…\"]", + "pluginVersion": 1, + "refreshing": false + } +} +``` + +| Field | Type | Notes | +|-------|------|-------| +| `weight` | `string` | e.g. `"25"`, `"processing"`, `"N/A"` | +| `fingerprint` | `string` | JSON-stringified assessment state fingerprint | +| `pluginVersion` | `number` | `WEIGHTING_SCHEMA_VERSION` (= 1) | +| `refreshing` | `boolean` | optional; background refetch in progress | + +### Notification Collector (`plugin.notificationCollector`) + +| Key | Type | Example | +|-----|------|---------| +| `plugin.notificationCollector.storage.lastNotificationCount` | `number` | `3` | +| `plugin.notificationCollector.storage.consecutiveErrors` | `number` | `0` | +| `plugin.notificationCollector.storage.lastCheckedTime` | `string` | `"2026-06-09T14:30:00.000Z"` | + +All **synced** (not under an excluded prefix). + +--- + +## Excluded caches (local-only schemas) + +### Grade Analytics — prefix `bsplus.analytics.` + +#### `bsplus.analytics.v2.{origin}.{studentId}` + +- `origin`: full school origin, e.g. `https://school.seqta.com.au` (dots in hostname appear in key) +- `studentId`: numeric SEQTA student id + +```json +{ + "updatedAt": 1717939200000, + "assessments": [ + { + "id": 1, + "title": "Assignment 1", + "subject": "10 Mathematics", + "status": "MARKS_RELEASED", + "due": "2026-03-15", + "code": "10MAT", + "metaclassID": 100, + "programmeID": 10, + "graded": true, + "overdue": false, + "hasFeedback": true, + "expectationsEnabled": false, + "expectationsCompleted": false, + "reflectionsEnabled": false, + "reflectionsCompleted": false, + "availability": "FULL", + "finalGrade": 85, + "letterGrade": "A" + } + ] +} +``` + +`status` enum: `"OVERDUE"` | `"MARKS_RELEASED"` | `"PENDING"`. + +#### `bsplus.analytics.distMode.v1.{origin}.{studentId}` + +Scalar string: `"auto"` | `"letter"` | `"percent"`. + +### Global Search — prefix `plugin.global-search.storage.` + +Reserved for future device-local caches. No keys in use today; any key matching the prefix is excluded. + +--- + +## Data outside `chrome.storage.local` + +These affect UX but **never** appear in the cloud `data` blob: + +| Store | Location | Contents | +|-------|----------|----------| +| Global Search structured index | IndexedDB `betterseqta-index` | Searchable page text | +| Global Search vectors | IndexedDB `embeddiaDB` | Embedding index | +| Global Search schema version | `localStorage` key `betterseqta-index-version` | Index migration | +| Installed themes | localforage (default instance) | `CustomTheme` objects keyed by theme id; index `customThemes` | +| Profile picture upload | localforage `profile-picture-store` / `profilePicture` | Image blob | +| Cloud PFP cache | localforage `cloud-pfp-store` / `cloudPfp` | Cached avatar blobs | +| Background music | localforage `background-music-store` / `music` / `audio-blob` | Audio blob | +| Dev API override | `sessionStorage` `bsplus_dev_api_base` | Staging server URL | +| Engage student context | `localStorage` `bsplus.engageTimetable.student.{origin}` | Current student id | + +After cloud restore, devices may need to **rebuild** indexes, **re-upload** media, or **re-download** themes even when synced settings reference them. + +--- + +## Catch-all & forward compatibility + +### Inclusion test + +```ts +isKeyIncludedInCloudUploadPayload(key) === !shouldOmitKeyFromCloudPayload(key) +``` + +Any new `chrome.storage.local` key syncs automatically unless added to: + +- `KEYS_OMITTED_FROM_CLOUD_UPLOAD` +- `SENSITIVE_DEVICE_STORAGE_KEYS_EXACT` +- `CLIENT_ONLY_CLOUD_KEYS_EXACT` +- `SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES` + +### Upload trigger flow + +1. `browser.storage.onChanged` (area `local`) +2. At least one changed key passes `isKeyIncludedInCloudUploadPayload` +3. `autoCloudSettingsSync !== false` +4. Valid access token +5. Not during restore → schedule 2 s debounce → `PUT` full snapshot + +### Server storage suggestion + +Store the full PUT body (or at minimum `{ schemaVersion, themeId, data }`) as JSONB per user. `data` alone is sufficient for restore if `themeId` is duplicated inside `data.selectedTheme`. + +### Versioning + +- Client `schemaVersion`: **1** +- If server returns higher `schemaVersion` in cloud-summary, auto-download is skipped until client is updated. + +--- + +## UI accessibility + +Which synced keys can be changed directly by the user in the extension **settings popup** (SEQTA → BetterSEQTA+ settings) or other in-product UI — vs keys that are written only by the app, popups, or plugins at runtime. + +**Settings popup tabs:** Settings · Shortcuts · Themes (`src/interface/pages/settings.svelte`). + +### Extension popup — Settings tab (`general.svelte`) + +| Storage key / path | UI label | Control | +|--------------------|----------|---------| +| `iconOnlySidebar` | Icon Only Sidebar | Switch | +| `animations` | Animations | Switch | +| `timeFormat` | 12 Hour Time | Switch (`"12"` / `"24"`) | +| `transparencyEffects` | Transparency Effects | Switch | +| `defaultPage` | Default Page | Select | +| `newsSource` | News Feed Source | Select | +| `selectedColor` | Custom Theme Colour | Button → colour picker modal | +| `selectedFont` | Interface Font | Button → font picker modal | +| `adaptiveThemeColour` | Adaptive Theme Colour | Switch | +| `adaptiveThemeGradient` | Soft Gradient | Switch (when adaptive colour on) | +| `adaptiveThemeColourTransition` | Smooth colour transition | Switch (when adaptive colour on) | +| `menuorder`, `menuitems`, `defaultmenuorder` | Edit Sidebar Layout | Button → opens editor **on the SEQTA tab** (not inside the popup) | +| `themeOfTheMonthDisabled` | Theme of the Month | Switch (under Global Search plugin block) | +| `autoCloudSettingsSync` | Automatic sync | Switch (BetterSEQTA Cloud card, signed in only) | +| `onoff` | BetterSEQTA+ | Switch (bottom of tab) | + +#### Plugin blocks on Settings tab + +Each row is `plugin.{id}.settings.{field}` unless noted. Component settings (upload UIs) change synced **preference** keys but not blob data in IndexedDB. + +| Plugin | UI | Synced fields from UI | +|--------|-----|------------------------| +| **Animated Background** | Enable + Animation Speed slider | `enabled`, `speed` | +| **Assessment Averages** | Enable (+ disclaimer) + Letter Grades | `enabled`, `lettergrade` | +| **Notification Collector** | Enable only | `enabled` | +| **Timetable Enhancer** | Enable only | `enabled` | +| **Edit Rooms & Teachers** | Enable only | `enabled` | +| **Global Search** | Enable + hotkey + 4 toggles + Reset Index button | `enabled`, `searchHotkey`, `showRecentFirst`, `transparencyEffects`, `runIndexingOnLoad`, `passiveIndexing` — Reset Index does **not** sync index data | +| **Custom Profile Picture** | Enable + Use cloud PFP (if signed in) + upload/remove | `enabled`, `useCloudPfp`; upload updates `profile_picture_revision` (image blob **not** synced) | +| **Message Folders** | Enable + 2 toggles | `enabled`, `showTagsInAllMessages`, `hideFolderedMessagesInAll` | +| **Enhanced Navigation** | Enable + Auto-scroll | `enabled`, `autoScrollOnClick` | +| **Background Music** | Enable + volume + pause on hidden + upload | `enabled`, `volume`, `pauseOnHidden`; audio blob **not** synced | +| **Grade Analytics** | Cache duration (hours) slider only | `cacheTtlHours` | + +**Not shown on Settings tab** (empty `settings` + no enable toggle → card hidden): + +- **Themes** plugin — use **Themes** tab instead +- **Assessments Overview** plugin — no settings UI + +#### Dev mode only (Settings tab, after typing “dev” on logo) + +| Storage key | UI label | +|-------------|----------| +| `devMode` | Developer Mode | +| `hideSensitiveContent` | Sensitive Hider | +| `mockNotices` | Mock Notices | +| `privacyStatementShown`, `privacyStatementLastUpdated` | Show Privacy Notification (resets to show popup) | +| `devGhReleaseVersionOverride` | GitHub latest version override (text field) | + +Dev **Export cloud settings JSON** downloads the upload payload; it does not change storage. + +### Extension popup — Shortcuts tab (`shortcuts.svelte`) + +| Storage key | UI | +|-------------|-----| +| `shortcuts` | Toggle built-in shortcuts (Outlook, Office, Google, … from `links.json`) | +| `customshortcuts` | Add / delete custom shortcuts (name, URL, optional SVG icon) | + +Changes use `settingsState.setKey()` so the SEQTA home page updates immediately via `StorageChangeHandler` → `renderShortcuts()` (embedded settings run in the content script, where `storage.onChanged` does not fire for local writes). Empty arrays are persisted explicitly (`customshortcuts: []`) so cloud restore and other devices clear removed shortcuts. + +### Extension popup — Themes tab (`theme.svelte`) + +| Storage key | UI | +|-------------|-----| +| `selectedTheme` | Install / select / clear store themes (`ThemeSelector`) | +| `selectedColor` | May change when applying a theme with a default colour | +| `originalSelectedColor`, `originalDarkMode` | Set internally during theme **preview** in theme manager (not dedicated controls) | + +Animated background selection uses `BackgroundSelector` (local/custom backgrounds); store theme id still goes to `selectedTheme`. + +### SEQTA page UI (outside settings popup) + +| Storage key / path | Where | Control | +|--------------------|-------|---------| +| `DarkMode` | SEQTA top bar | Sun/moon **Light/Dark** button | +| `menuorder`, `menuitems` | SEQTA sidebar | **Edit Sidebar Layout** overlay (drag order + per-item toggles) | +| `subjectfilters` | Home → upcoming assessments | Per-subject checkboxes (`#upcoming-filters`) | +| `subjectfilters` | Assessments → Overview | Subject filter UI in overview grid | +| `plugin.messageFolders.storage.folders` | Messages page | Create/edit/delete folders | +| `plugin.messageFolders.storage.messageAssignments` | Messages page | Assign messages to folders | +| `plugin.timetableEdit.storage.timetableOverrides` | Timetable | Edit room/teacher on a class | +| `plugin.timetableEdit.storage.timetableOverridesBySubject` | Timetable | Subject-level overrides | +| `plugin.assessments-average.storage.weightingOverrides` | Assessments page | Per-assessment “Override %” inputs | +| Global Search | SEQTA | Hotkey opens search bar (hotkey set in Settings) | + +### Automatic / popup-only (synced, no dedicated settings control) + +| Storage key / path | How it changes | +|--------------------|----------------| +| `lessonalert` | Default only — **no UI** (read by home loader) | +| `privacyStatementShown`, `privacyStatementLastUpdated` | Privacy popup on first run (dev can reset) | +| `engageParentsAnnouncementShown` | Dismiss Engage parents announcement | +| `bsCloudAutoSyncAnnouncementShown` | Dismiss cloud sync announcement | +| `themeOfTheMonthDismissedMonth` | Dismiss Theme of the Month popup | +| `justupdated` | Set `true` after extension update | +| `lastSeenNightlyPublishedAt` | Dismiss / acknowledge GitHub nightly update badge | +| `profile_picture_revision` | Incremented when profile picture changes | +| `plugin.notificationCollector.storage.*` | Written by notification poll loop | +| Legacy `animatedbk`, `bksliderinput`, etc. | Migrated to plugin settings; no longer shown in UI | + +### Synced plugin prefs with UI vs storage-only side effects + +| User action in UI | Synced | Not synced | +|-------------------|--------|------------| +| Upload profile picture | `useCloudPfp`, `profile_picture_revision` | Image blob (localforage) | +| Upload background music | `enabled`, `volume`, `pauseOnHidden` | Audio blob (localforage) | +| Install store theme | `selectedTheme`, often `selectedColor` / `DarkMode` | Theme assets (localforage) | +| Global Search indexing | Plugin settings toggles | IndexedDB `betterseqta-index`, `embeddiaDB` | +| Assessment averages | `lettergrade`, overrides | `storage.assessments`, `storage.weightings` caches | +| Grade Analytics TTL slider | `cacheTtlHours` | `bsplus.analytics.*` caches | + +### Never in settings UI (excluded from sync entirely) + +OAuth keys, analytics caches, assessment weighting caches, global-search storage prefix, client watermarks — see [Exclusion rules](#exclusion-rules). + +--- + +## Default schema initialization + +Implementation: `ensureSyncableStorageDefaults()` + `getSyncableStorageDefaults()`. + +### What gets written when a key is missing + +1. **All core `SettingsState` fields** from `getDefaultSettingsState()` — including `shortcuts`, `customshortcuts: []`, `menuitems`, `selectedFont: "rubik"`, `timeFormat: "24"`, announcement flags defaulting to `false`, etc. +2. **Every registered plugin’s** `plugin.{pluginId}.settings` object (defaults from plugin definitions; `enabled` included for `disableToggle` plugins). +3. **Legacy migration** on that flat map so upload-shaped storage uses `plugin.animated-background.settings` rather than `animatedbk` / `bksliderinput`. + +### What is not auto-initialized + +| Category | Reason | +|----------|--------| +| OAuth / session keys | Excluded from cloud; device-local | +| `plugin.*.storage.*` runtime data | Created when plugins run (folders, timetable overrides, etc.) | +| `bsplus.analytics.*` | Excluded device/school caches | +| `plugin.assessments-average.storage.assessments` / `.weightings` | Excluded school caches | +| `plugin.global-search.storage.*` | Excluded prefix | +| Client-only watermarks | Never uploaded | + +### Settings persistence and live UI + +`SettingsState` (`src/seqta/utils/listeners/SettingsState.ts`): + +- Assignments (`settingsState.foo = …`) and `setKey()` persist to `chrome.storage.local` and **notify registered listeners** in the same context (required for embedded SEQTA settings). +- `saveToStorage()` omits `undefined` values so optional keys are not accidentally stripped. diff --git a/src/background.ts b/src/background.ts index ad7ab92a..c1248182 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,6 +1,8 @@ import browser from "webextension-polyfill"; import semver from "semver"; import type { SettingsState } from "@/types/storage"; +import { getDefaultSettingsState } from "@/seqta/utils/defaultSettings"; +import { ensureSyncableStorageDefaults } from "@/seqta/utils/ensureSyncableStorageDefaults"; import { fetchNews } from "./background/news"; import { initCloudSettingsAutoSync, @@ -404,6 +406,15 @@ const MESSAGE_HANDLERS: Record = { void browser.tabs.create({ url: "github.com/BetterSEQTA/BetterSEQTA-Plus" }); }, setDefaultStorage: () => SetStorageValue(getDefaultValues()), + ensureStorageDefaults: (_req, sendResponse) => { + void ensureSyncableStorageDefaults() + .then(() => sendResponse({ ok: true })) + .catch((e) => { + console.warn("[BetterSEQTA+] ensureStorageDefaults failed:", e); + sendResponse({ ok: false }); + }); + return true; + }, sendNews: (req, sendResponse) => { fetchNews(req.source ?? "australia", sendResponse); return true; @@ -477,76 +488,8 @@ browser.runtime.onMessage.addListener( }, ); -function detectLowEndDevice(): boolean { - // Check for low-end hardware indicators - const lowCoreCount = navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4; - const lowMemory = (navigator as any).deviceMemory && (navigator as any).deviceMemory <= 2; - - return lowCoreCount || lowMemory; -} - function getDefaultValues(): SettingsState { - const isLowEndDevice = detectLowEndDevice(); - - return { - onoff: true, - animatedbk: true, - bksliderinput: "50", - transparencyEffects: false, - lessonalert: true, - defaultmenuorder: [], - menuitems: { - assessments: { toggle: true }, - courses: { toggle: true }, - dashboard: { toggle: true }, - documents: { toggle: true }, - forums: { toggle: true }, - goals: { toggle: true }, - home: { toggle: true }, - messages: { toggle: true }, - myed: { toggle: true }, - news: { toggle: true }, - notices: { toggle: true }, - portals: { toggle: true }, - reports: { toggle: true }, - settings: { toggle: true }, - timetable: { toggle: true }, - welcome: { toggle: true }, - }, - menuorder: [], - subjectfilters: {}, - selectedTheme: "", - selectedColor: - "linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)", - originalSelectedColor: "", - DarkMode: true, - animations: !isLowEndDevice, - assessmentsAverage: false, - defaultPage: "home", - shortcuts: [ - { - name: "Outlook", - enabled: true, - }, - { - name: "Office", - enabled: true, - }, - { - name: "Google", - enabled: true, - }, - ], - customshortcuts: [], - lettergrade: false, - newsSource: "australia", - iconOnlySidebar: false, - adaptiveThemeColour: false, - adaptiveThemeGradient: false, - adaptiveThemeColourTransition: true, - themeOfTheMonthDisabled: false, - autoCloudSettingsSync: true, - }; + return getDefaultSettingsState(); } function SetStorageValue(object: any) { @@ -673,6 +616,8 @@ browser.runtime.onInstalled.addListener(function (event) { browser.storage.local.remove(["justupdated"]); browser.storage.local.remove(["data"]); + void ensureSyncableStorageDefaults(); + if (event.reason == "install" || event.reason == "update") { browser.storage.local.set({ justupdated: true }); } @@ -684,4 +629,9 @@ browser.runtime.onInstalled.addListener(function (event) { } }); +browser.runtime.onStartup.addListener(() => { + void ensureSyncableStorageDefaults(); +}); + initCloudSettingsAutoSync({ reloadSeqtaPages }); +void ensureSyncableStorageDefaults(); diff --git a/src/css/injected/popup.scss b/src/css/injected/popup.scss index 6f64171d..5ecf0385 100644 --- a/src/css/injected/popup.scss +++ b/src/css/injected/popup.scss @@ -18,7 +18,7 @@ right: 10px; top: 80px; height: 590px; - z-index: 20; + z-index: 100; transition-duration: 100ms; } diff --git a/src/interface/pages/settings/shortcuts.svelte b/src/interface/pages/settings/shortcuts.svelte index b042b1a0..aaef38c8 100644 --- a/src/interface/pages/settings/shortcuts.svelte +++ b/src/interface/pages/settings/shortcuts.svelte @@ -24,18 +24,18 @@ }); const switchChange = (shortcut: any) => { - const idx = $settingsState.shortcuts.findIndex(s => s.name === shortcut); + const current = settingsState.shortcuts ?? []; + const idx = current.findIndex(s => s.name === shortcut); if (idx !== -1) { - // Create a new array with the toggled value to ensure reactivity - const updated = settingsState.shortcuts.map(s => + const updated = current.map(s => s.name === shortcut ? { ...s, enabled: !s.enabled } : s ); - settingsState.shortcuts = updated; + settingsState.setKey("shortcuts", updated); } else { - settingsState.shortcuts = [ - ...settingsState.shortcuts, - { name: shortcut, enabled: true } - ]; + settingsState.setKey("shortcuts", [ + ...current, + { name: shortcut, enabled: true }, + ]); } } @@ -82,7 +82,10 @@ if (isValidTitle(newTitle) && isValidURL(newURL)) { const icon = newIcon || newTitle[0]; const newShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon }; - settingsState.customshortcuts = [...settingsState.customshortcuts, newShortcut]; + settingsState.setKey("customshortcuts", [ + ...(settingsState.customshortcuts ?? []), + newShortcut, + ]); newTitle = ""; newURL = ""; @@ -94,7 +97,10 @@ }; const deleteCustomShortcut = (index: number) => { - settingsState.customshortcuts = settingsState.customshortcuts.filter((_, i) => i !== index); + settingsState.setKey( + "customshortcuts", + (settingsState.customshortcuts ?? []).filter((_, i) => i !== index), + ); }; @@ -203,7 +209,7 @@ - {#each $settingsState.customshortcuts as shortcut, index} + {#each ($settingsState.customshortcuts ?? []) as shortcut, index}
{shortcut.name}