Add Google Calendar CalDAV sync via OAuth
Details
Wire the Calendar plugin into the existing shell-level Google OAuth
connection to support CalDAV sync with Google Calendar. Previously
the CalDavClient only supported HTTP Basic auth, which Google has
deprecated.
Changes:
- CalDavProviderType: add Google enum value
- CalDavCredentials: add BearerToken property (takes precedence
over Username/Password when set)
- CalDavClient: rename SetBasicAuth → SetAuth, support Bearer
authorization header for OAuth providers
- CalendarSubscription: add GoogleConnectionId field to persist
which Google OAuth account is linked to the subscription
- CalendarPlugin: implement IConnectionConsumer declaring Google
with calendar scope, pass IConnectionService through to sync
service and ViewModel
- AddSubscriptionViewModel: when provider is Google, load connected
Google accounts, skip username/password fields, use bearer token
for calendar discovery. Store GoogleConnectionId on subscription
- CalendarSyncService: add ResolveCalDavCredentialsAsync that
fetches OAuth bearer tokens from IConnectionService for Google
subscriptions, vault credentials for others
Google CalDAV endpoint: apidata.googleusercontent.com/caldav/v2
Setup: enable CalDAV API and Google Calendar API in Google Cloud
Console, then connect Google account in Settings → Connections.
Fix CalDAV bidirectional sync: delete propagation, metadata preservation, ICS fidelity
Details
Critical fixes:
- Delete propagation: CalDAV events now marked pending_delete instead of being
immediately removed from the store. The sync service handles the server DELETE
and local cleanup on the next cycle.
- Occurrence changes: Deleting or editing a single occurrence of a recurring
CalDAV event now marks the parent as pending_push so ExcludedDates propagate
to the server.
- Editor metadata: Editing a CalDAV event preserves SourceUid, CalDavEtag,
CalDavHref, and ExcludedDates from the original event, preventing silent
metadata wipe and broken ETag concurrency.
- Sync token initialization: After a full-fetch fallback (no token or expired
token), GetSyncTokenAsync PROPFIND acquires the current sync-token so
subsequent syncs use incremental sync-collection instead of full fetch.
- Multi-VEVENT resources: CalDAV resources containing multiple VEVENTs (parent
+ exception overrides) are now all processed, not just the first.
ICS parser/exporter improvements:
- EXDATE: Parse and export excluded dates for recurring events.
- VALARM: Parse TRIGGER durations from VALARM sub-components into
ReminderMinutes. Export reminders as VALARM blocks with proper duration
formatting (e.g., -P1D, -PT1H, -PT30M).
- Week duration: P#W format now correctly parsed as days (P1W = 7 days).
- UNTIL date-only: 8-char UNTIL values (e.g., UNTIL=20250301) now route
through the VALUE=DATE parser branch instead of floating-time.
- RECURRENCE-ID: Parsed into OriginalStart for CalDAV exception events.
Tests: 15 new test cases covering EXDATE, VALARM, week duration, UNTIL
date-only, RECURRENCE-ID, multi-VEVENT, and GetSyncTokenAsync.
Add "Publish to Gist" to Notes plugin
Details
One-click publishing of Notes pages as GitHub Gists. Converts the page to
markdown using the existing export infrastructure, creates a secret gist via
the GitHub API, and copies the gist URL to the clipboard.
Accessible from:
- Export dropdown in page toolbar (Export → GitHub Gist)
- Right-click context menu on page tree (Publish to Gist)
Uses the existing GitHub connection (Notes already implements IConnectionConsumer
with repo scope). No new dependencies or schema changes needed.
New files:
- GistPublishService.cs: lightweight HTTP client for POST /gists only
Fix gist push to handle title/filename renames
Details
PushSnippetAsync now detects when the snippet title has changed (producing a
different filename via BuildFilename) and sends a rename payload to the GitHub
API: deletes the old filename (null) and creates the new filename with content.
UpdateGistAsync gains a rawFiles parameter (Dictionary<string, object?>) to
support null values for file deletion, which is how the GitHub API expresses
file renames.
Auto-push to gist on save when bidirectional sync is enabled
Details
When a snippet is linked to a gist source with sync_direction="bidirectional",
saving the snippet now automatically pushes the changes to GitHub. If the push
fails, the local save still succeeds and the user is notified of the push failure.
Get notified about new releases