diff --git a/frontend/src/lib/api/device.remote.ts b/frontend/src/lib/api/device.remote.ts new file mode 100644 index 0000000..52db49d --- /dev/null +++ b/frontend/src/lib/api/device.remote.ts @@ -0,0 +1,43 @@ +import { command, form, getRequestEvent, query } from '$app/server'; +import { createDeviceSchema, type Device } from '$lib/schema/device'; +import { error } from '@sveltejs/kit'; +import z from 'zod'; + +export const listDevices = query(async (): Promise => { + const { locals } = getRequestEvent(); + + const response = await locals.api.get(`/devices`); + + if (!response.ok) error(500, 'An unkown error occurred'); + + const deviceResult = await response.json(); + return deviceResult.items +}); + +export const createDevice = form(createDeviceSchema, async (data): Promise => { + const { locals } = getRequestEvent(); + + const response = await locals.api.post(`/devices`, data); + + if (!response.ok) error(500, 'An unknown error occurred'); + + return await response.json(); +}); + +export const regenerateDeviceApiKey = command(z.string(), async (deviceId): Promise => { + const { locals } = getRequestEvent(); + + const response = await locals.api.get(`/devices/${deviceId}/regenerate`); + + if (!response.ok) error(500, 'An unknown error occurred'); + + return await response.json(); +}) + +export const deleteDevice = command(z.string(), async (deviceId): Promise => { + const { locals } = getRequestEvent(); + + const response = await locals.api.delete(`/devices/${deviceId}`); + + if (!response.ok) error(500, 'An unknown error occurred'); +}); diff --git a/frontend/src/lib/components/forms/device-create-form.svelte b/frontend/src/lib/components/forms/device-create-form.svelte new file mode 100644 index 0000000..708149f --- /dev/null +++ b/frontend/src/lib/components/forms/device-create-form.svelte @@ -0,0 +1,50 @@ + + +
{ + try { + await submit(); + + const issues = createDevice.fields.allIssues(); + if (issues && issues.length > 0) { + return; + } + + const deviceName = createDevice.fields.name.value(); + + form.reset(); + toast.success(`Device '${deviceName}' created`); + onSuccess?.(); + } catch (error) { + console.error('Failed to create device: ', error); + toast.error('Failed to create device'); + } + })} + class="flex flex-col gap-3" +> + + + + Device name + + {#each createDevice.fields.name.issues() ?? [] as issue} + {issue.message} + {/each} + + + + + +
diff --git a/frontend/src/lib/schema/device.ts b/frontend/src/lib/schema/device.ts new file mode 100644 index 0000000..1a16cca --- /dev/null +++ b/frontend/src/lib/schema/device.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +import type { components } from './openapi/schema'; +export type Device = components['schemas']['KosyncDeviceRead'] + +export const createDeviceSchema = z.object({ + name: z.string().min(1, 'Name cannot be empty') +}) \ No newline at end of file diff --git a/frontend/src/routes/(root)/settings/devices/+page.server.ts b/frontend/src/routes/(root)/settings/devices/+page.server.ts new file mode 100644 index 0000000..55628d4 --- /dev/null +++ b/frontend/src/routes/(root)/settings/devices/+page.server.ts @@ -0,0 +1,9 @@ +import { listDevices } from "$lib/api/device.remote"; + +export async function load() { + const devices = await listDevices(); + + return { + devices + }; +} diff --git a/frontend/src/routes/(root)/settings/devices/+page.svelte b/frontend/src/routes/(root)/settings/devices/+page.svelte new file mode 100644 index 0000000..7fa3d83 --- /dev/null +++ b/frontend/src/routes/(root)/settings/devices/+page.svelte @@ -0,0 +1,240 @@ + + +
+
+

Devices

+

Manage your KOReader devices

+
+ + + + Add Device + + + + + Add a new device + + Create a device to sync your KOReader reading progress. + + + + + +
+ +{#if data.devices.length === 0} + + + + + + No devices + + Add a device to sync your KOReader reading progress. + + + + + + +{:else} + + + + + + Name + API Key + + + + + {#each data.devices as device} + {@const isVisible = visibleApiKeys.has(String(device.id))} + + + {device.name} + + +
+ + {isVisible ? device.api_key : maskApiKey(device.api_key)} + + + +
+
+ + + + + + + (regenerateConfirmDevice = device)}> + + Regenerate API Key + + + (deleteConfirmDevice = device)} + > + + Delete + + + + +
+ {/each} +
+
+
+
+{/if} + + + + + + Delete device? + + Are you sure you want to delete "{deleteConfirmDevice?.name}"? This action cannot be undone. + The device will no longer be able to sync reading progress. + + + + (deleteConfirmDevice = null)}>Cancel + deleteConfirmDevice && handleDelete(deleteConfirmDevice)} + > + Delete + + + + + + + + + + Regenerate API key? + + This will invalidate the current API key for "{regenerateConfirmDevice?.name}". + You will need to update the key in your KOReader device settings. + + + + (regenerateConfirmDevice = null)}>Cancel + regenerateConfirmDevice && handleRegenerate(regenerateConfirmDevice)} + > + Regenerate + + + +