diff --git a/package.json b/package.json index 8870fea..d04ffdc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build-only": "vite build", "type-check": "vue-tsc --build", "format": "prettier --write --experimental-cli src/", - "fetch-wasm": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/download/v0.1.1/core.wasm", + "fetch-wasm": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/download/v0.1.2/core.wasm", "fetch-wasm-latest": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/latest/download/core.wasm" }, "dependencies": { diff --git a/src/components/SideBar.vue b/src/components/SideBar.vue index cdaae3c..60ecb33 100644 --- a/src/components/SideBar.vue +++ b/src/components/SideBar.vue @@ -4,6 +4,8 @@ import { useSlots } from 'vue'; import SidebarCloseBtn from '@/components/SidebarCloseBtn.vue'; import { useSidebar } from '@/composables/useSidebar'; import NewEventBtn from '@/components/NewEventBtn.vue'; +import { exportZip } from '@/utils'; +import { LuFolderArchive } from 'vue-icons-plus/lu'; const slots = useSlots(); const sidebar = useSidebar(); @@ -29,6 +31,9 @@ const sidebar = useSidebar(); + @@ -63,6 +68,7 @@ aside { padding: 0.3rem; width: 2.1rem; aspect-ratio: 1/1; + background-color: transparent; border-radius: var(--small-border-radius); diff --git a/src/types/core.ts b/src/types/core.ts index f7de11b..4ca787f 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -32,6 +32,7 @@ export interface CalendarApi { pullAll(): void; pushAll(): void; + exportZip(calendar: string): ArrayBuffer; createEvent(event: CalendarEvent): CalendarEvent; updateEvent(event: CalendarEvent): CalendarEvent; diff --git a/src/utils.ts b/src/utils.ts index 81c31db..796f5e5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import { DateTime } from 'luxon'; import { type RouteParamsGeneric, type Router } from 'vue-router'; import { useSettings } from '@/composables/useSettings'; import type { CalendarEvent } from './types/core'; +import { CalendarCore } from './wasm/core-wrapper'; const { settings } = useSettings(); @@ -122,3 +123,42 @@ export function timeInPercentOnTimeline(datetime: DateTime): number { export function isWholeDay(event: CalendarEvent): boolean { return event.from.toFormat('HH:mm') == '00:00' && event.to.toFormat('HH:mm') == '23:59'; } + +/** + * Downloads a zipped calendar or all calendars if ''. + */ +export async function exportZip(calendar: string = '') { + const zipBytes = await CalendarCore.exportZip(calendar); + const fileName = calendar ? `${calendar}.zip` : `git-calendar-data.zip`; + + if ('showSaveFilePicker' in window && typeof window.showSaveFilePicker == 'function') { + const handle = await window.showSaveFilePicker({ + suggestedName: fileName, + types: [ + { + description: 'ZIP archive', + accept: { 'application/zip': ['.zip'] }, + }, + ], + }); + + const writable = await handle.createWritable(); + await writable.write(zipBytes); + await writable.close(); + } else { + // firefox doesnt have the showSaveFilePicker method + downloadBlob(zipBytes, fileName); + } +} + +function downloadBlob(data: BlobPart, filename: string) { + const blob = new Blob([data], { type: 'application/zip' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + + URL.revokeObjectURL(url); +} diff --git a/src/wasm/mapper.ts b/src/wasm/mapper.ts index 4a5cb0e..a530761 100644 --- a/src/wasm/mapper.ts +++ b/src/wasm/mapper.ts @@ -21,9 +21,25 @@ function toSnakeCase(str: string): string { // // So this hydrateDates tries to parse it beforehand. export function hydrateDates(data: unknown): T { - // non-object, primitive, null + // primitives / null if (data === null || typeof data !== 'object') return data as T; + // preserve binary data + if ( + data instanceof Uint8Array || + data instanceof ArrayBuffer || + ArrayBuffer.isView(data) || + data instanceof Blob || + data instanceof File + ) { + return data as T; + } + + // preserve Luxon values if they ever come through here + if (DateTime.isDateTime(data)) { + return data as T; + } + if (Array.isArray(data)) { return data.map(hydrateDates) as unknown as T; // recursively for arrays } @@ -44,10 +60,8 @@ export function hydrateDates(data: unknown): T { } else { result[key] = dt; } - } else if (value && typeof value === 'object' && !Array.isArray(value)) { - result[key] = hydrateDates(value); // recursively for objects } else { - result[key] = value; // just assign + result[key] = hydrateDates(value); // recursively } }