From e609eca00ec96e5a079225830588f8b327163bd2 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 4 Mar 2026 23:33:36 +0100 Subject: [PATCH 1/5] replace old dialog manager --- .../confirm-dialog/dialog-confirm.svelte | 55 ------ .../confirm-dialog/dialog-manager.svelte | 25 --- .../dialog-alert-content.svelte | 21 ++ .../dialog-confirm-content.svelte | 45 +++++ .../dialog-custom-content.svelte | 16 ++ .../dialog-manager/dialog-manager.svelte | 35 ++++ .../dialog-manager/dialog-store.svelte.ts | 96 +++++++++ src/lib/components/dialog-manager/types.ts | 59 ++++++ src/lib/stores/ConfirmDialogStore.ts | 18 -- src/lib/stores/HubsStore.ts | 20 +- src/routes/(authenticated)/hubs/+page.svelte | 33 +++- .../hubs/data-table-actions.svelte | 183 ++++++++++++++++-- .../hubs/dialog-hub-create.svelte | 28 --- .../hubs/dialog-hub-delete.svelte | 40 ---- .../hubs/dialog-hub-edit.svelte | 34 ---- .../hubs/dialog-hub-pair.svelte | 70 ------- .../hubs/dialog-hub-regenerate-token.svelte | 66 ------- .../user/incoming/incoming-share-item.svelte | 8 +- .../shares/user/incoming/manage-share.svelte | 8 +- .../user/invites/incoming-invite-item.svelte | 8 +- .../user/invites/outgoing-invite-item.svelte | 8 +- .../shares/user/outgoing/edit-share.svelte | 8 +- src/routes/+layout.svelte | 2 +- 23 files changed, 498 insertions(+), 388 deletions(-) delete mode 100644 src/lib/components/confirm-dialog/dialog-confirm.svelte delete mode 100644 src/lib/components/confirm-dialog/dialog-manager.svelte create mode 100644 src/lib/components/dialog-manager/dialog-alert-content.svelte create mode 100644 src/lib/components/dialog-manager/dialog-confirm-content.svelte create mode 100644 src/lib/components/dialog-manager/dialog-custom-content.svelte create mode 100644 src/lib/components/dialog-manager/dialog-manager.svelte create mode 100644 src/lib/components/dialog-manager/dialog-store.svelte.ts create mode 100644 src/lib/components/dialog-manager/types.ts delete mode 100644 src/lib/stores/ConfirmDialogStore.ts delete mode 100644 src/routes/(authenticated)/hubs/dialog-hub-create.svelte delete mode 100644 src/routes/(authenticated)/hubs/dialog-hub-delete.svelte delete mode 100644 src/routes/(authenticated)/hubs/dialog-hub-edit.svelte delete mode 100644 src/routes/(authenticated)/hubs/dialog-hub-pair.svelte delete mode 100644 src/routes/(authenticated)/hubs/dialog-hub-regenerate-token.svelte diff --git a/src/lib/components/confirm-dialog/dialog-confirm.svelte b/src/lib/components/confirm-dialog/dialog-confirm.svelte deleted file mode 100644 index 595a5516..00000000 --- a/src/lib/components/confirm-dialog/dialog-confirm.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - open, (o) => (open = o)}> - - - {title} - - {desc} - {#if descSnippet} - {@render descSnippet(data)} - {/if} - - - - - diff --git a/src/lib/components/confirm-dialog/dialog-manager.svelte b/src/lib/components/confirm-dialog/dialog-manager.svelte deleted file mode 100644 index 5423abce..00000000 --- a/src/lib/components/confirm-dialog/dialog-manager.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -{#key dialogCounter} - {#if dialogData} - - {/if} -{/key} diff --git a/src/lib/components/dialog-manager/dialog-alert-content.svelte b/src/lib/components/dialog-manager/dialog-alert-content.svelte new file mode 100644 index 00000000..e8b4ea1f --- /dev/null +++ b/src/lib/components/dialog-manager/dialog-alert-content.svelte @@ -0,0 +1,21 @@ + + + + {title} + {#if desc} + {desc} + {/if} + + + + diff --git a/src/lib/components/dialog-manager/dialog-confirm-content.svelte b/src/lib/components/dialog-manager/dialog-confirm-content.svelte new file mode 100644 index 00000000..48b7ca44 --- /dev/null +++ b/src/lib/components/dialog-manager/dialog-confirm-content.svelte @@ -0,0 +1,45 @@ + + + + {title} + + {desc} + {#if descSnippet} + {@render descSnippet(data as T)} + {/if} + + + + + + diff --git a/src/lib/components/dialog-manager/dialog-custom-content.svelte b/src/lib/components/dialog-manager/dialog-custom-content.svelte new file mode 100644 index 00000000..38f91730 --- /dev/null +++ b/src/lib/components/dialog-manager/dialog-custom-content.svelte @@ -0,0 +1,16 @@ + + +{@render contentSnippet(renderProps)} diff --git a/src/lib/components/dialog-manager/dialog-manager.svelte b/src/lib/components/dialog-manager/dialog-manager.svelte new file mode 100644 index 00000000..5d4749c0 --- /dev/null +++ b/src/lib/components/dialog-manager/dialog-manager.svelte @@ -0,0 +1,35 @@ + + +{#if dialogId && ctx} + {#key dialogId} + open, handleOpenChange}> + + + + + {/key} +{/if} diff --git a/src/lib/components/dialog-manager/dialog-store.svelte.ts b/src/lib/components/dialog-manager/dialog-store.svelte.ts new file mode 100644 index 00000000..b9e32436 --- /dev/null +++ b/src/lib/components/dialog-manager/dialog-store.svelte.ts @@ -0,0 +1,96 @@ +import { SvelteMap } from 'svelte/reactivity'; +import DialogAlertContent from './dialog-alert-content.svelte'; +import DialogConfirmContent from './dialog-confirm-content.svelte'; +import DialogCustomContent from './dialog-custom-content.svelte'; +import type { + AlertDialogOptions, + ConfirmDialogOptions, + ConfirmResult, + CustomDialogOptions, + DialogContext, +} from './types'; + +// State +let dialogCount = $state(0); +const dialogs = new SvelteMap(); + +export function getOldestDialog(): [number, DialogContext] | null { + const firstEntry = dialogs.entries().next(); + return firstEntry.done ? null : firstEntry.value; +} + +export function removeDialog(id: number): void { + dialogs.delete(id); +} + +// Helper to create dialog with common setup +export function createDialog( + contextFactory: (resolve: (result: R) => void) => DialogContext +): Promise { + return new Promise((resolve) => { + const id = ++dialogCount; + let resolved = false; + + const wrappedResolve = (result: R) => { + if (resolved) return; + resolved = true; + setTimeout(() => removeDialog(id), 150); + resolve(result); + }; + + dialogs.set(id, contextFactory(wrappedResolve) as DialogContext); + }); +} + +/** + * Opens a fully custom dialog with your own content snippet. + */ +export function open(options: CustomDialogOptions): Promise { + return createDialog((resolve) => ({ + content: DialogCustomContent, + props: { + data: options.data, + contentSnippet: options.contentSnippet, + resolve, + close: () => resolve(undefined as R), + }, + resolve, + })); +} + +/** + * Opens a confirm dialog with built-in confirm/cancel buttons. + */ +export function confirm(options: ConfirmDialogOptions): Promise> { + return createDialog>((resolve) => ({ + content: DialogConfirmContent, + props: { + ...options, + resolve, + close: () => resolve({ confirmed: false }), + }, + resolve, + })); +} + +/** + * Opens an alert dialog that the user acknowledges. + */ +export function alert(options: AlertDialogOptions): Promise { + return createDialog((resolve) => ({ + content: DialogAlertContent, + props: { + ...options, + resolve, + close: () => resolve(), + }, + resolve, + })); +} + +export const dialog = { + open, + confirm, + alert, + createDialog, +}; diff --git a/src/lib/components/dialog-manager/types.ts b/src/lib/components/dialog-manager/types.ts new file mode 100644 index 00000000..46b121ef --- /dev/null +++ b/src/lib/components/dialog-manager/types.ts @@ -0,0 +1,59 @@ +import type { Component, Snippet } from 'svelte'; + +// Props passed to dialog content components +export interface DialogContentProps { + resolve: (result: R) => void; + close: () => void; +} + +// Props for custom dialog snippets +export interface DialogRenderProps extends DialogContentProps { + data: T; +} + +// Generic dialog context - stores a component to render +export interface DialogContext { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content: Component; + props: Record; + resolve: (result: R) => void; +} + +// Result types +export type ConfirmResult = { confirmed: true; data: T } | { confirmed: false }; + +// Options for each dialog type +export interface CustomDialogOptions { + data?: T; + contentSnippet: Snippet<[DialogRenderProps]>; +} + +export interface ConfirmDialogOptions { + title: string; + desc?: string; + data?: T; + confirmButtonText?: string; + cancelButtonText?: string; + descSnippet?: Snippet<[T]>; +} + +export interface AlertDialogOptions { + title: string; + desc?: string; + buttonText?: string; +} + +export interface AlertProps extends AlertDialogOptions { + resolve: () => void; + close: () => void; +} + +export interface ConfirmProps extends ConfirmDialogOptions { + resolve: (result: ConfirmResult) => void; + close: () => void; +} + +export interface CustomProps extends CustomDialogOptions { + resolve: (result: R) => void; + close: () => void; +} diff --git a/src/lib/stores/ConfirmDialogStore.ts b/src/lib/stores/ConfirmDialogStore.ts deleted file mode 100644 index 0b8addb8..00000000 --- a/src/lib/stores/ConfirmDialogStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Snippet } from 'svelte'; -import { writable } from 'svelte/store'; - -export interface ConfirmDialogContext { - data: T; - onConfirm: (value: T) => void; - title: string; - desc?: string; - confirmButtonText?: string; - descSnippet?: Snippet<[T]>; -} - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- store holds generic dialog context, concrete type varies per caller */ -export const ConfirmDialogStore = writable | null>(null); - -export function openConfirmDialog(context: ConfirmDialogContext) { - ConfirmDialogStore.set(context); -} diff --git a/src/lib/stores/HubsStore.ts b/src/lib/stores/HubsStore.ts index c4cfa75e..1eeaabc3 100644 --- a/src/lib/stores/HubsStore.ts +++ b/src/lib/stores/HubsStore.ts @@ -20,15 +20,15 @@ export type HubOnlineState = { export const OwnHubsStore = writable>(new Map()); export const OnlineHubsStore = writable>(new Map()); -export function refreshOwnHubs() { - shockersV1Api - .shockerListShockers() - .then((response) => { - if (!response.data) { - throw new Error(`Failed to fetch devices: ${response.message}`); - } +export async function refreshOwnHubs() { + try { + const response = await shockersV1Api.shockerListShockers(); + if (!response.data) { + throw new Error(`Failed to fetch devices: ${response.message}`); + } - OwnHubsStore.set(new Map(response.data.map((d) => [d.id, d]))); - }) - .catch(handleApiError); + OwnHubsStore.set(new Map(response.data.map((d) => [d.id, d]))); + } catch (error) { + handleApiError(error); + } } diff --git a/src/routes/(authenticated)/hubs/+page.svelte b/src/routes/(authenticated)/hubs/+page.svelte index 52e4f12f..f5d794f0 100644 --- a/src/routes/(authenticated)/hubs/+page.svelte +++ b/src/routes/(authenticated)/hubs/+page.svelte @@ -13,7 +13,12 @@ import { onMount } from 'svelte'; import { type Hub, columns } from './columns'; import DataTableActions from './data-table-actions.svelte'; - import HubCreateDialog from './dialog-hub-create.svelte'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; + import type { DialogRenderProps } from '$lib/components/dialog-manager/types'; + import { hubManagementV2Api } from '$lib/api'; + import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; + import * as Dialog from '$lib/components/ui/dialog'; + import TextInput from '$lib/components/input/TextInput.svelte'; const isMobile = new IsMobile(); @@ -45,20 +50,40 @@ }); let sorting = $state([]); - let showAddHubModal = $state(false); + async function openCreateHubDialog() { + const result = await dialog.open<{ name: string }, { name: string } | undefined>({ + data: { name: '' }, + contentSnippet: createHubSnippet, + }); + if (!result) return; + try { + await hubManagementV2Api.devicesCreateDeviceV2({ name: result.name }); + await refreshOwnHubs(); + } catch (error) { + handleApiError(error); + } + } breadcrumbs.push('Hubs', '/hubs'); onMount(refreshOwnHubs); - +{#snippet createHubSnippet(props: DialogRenderProps<{ name: string }, { name: string } | undefined>)} + + Create Hub + + + +{/snippet} Hubs
- diff --git a/src/routes/(authenticated)/hubs/data-table-actions.svelte b/src/routes/(authenticated)/hubs/data-table-actions.svelte index 6c0ef520..e1e2d3bf 100644 --- a/src/routes/(authenticated)/hubs/data-table-actions.svelte +++ b/src/routes/(authenticated)/hubs/data-table-actions.svelte @@ -1,18 +1,23 @@ - - - - +{#snippet editSnippet(props: DialogRenderProps<{ name: string }>)} + + Edit hub + + + +{/snippet} + +{#snippet deleteDescSnippet(h: Hub)} + You are about to delete hub "{h.name}"
+ This will also delete the following shockers: +
+ {#each h.shockers as shocker (shocker.id)} + {shocker.name} + {/each} +
+{/snippet} + +{#snippet pairSnippet(props: DialogRenderProps<{ loading: boolean; code: string | null }>)} + + + {props.data.loading ? 'Generating...' : props.data.code ? 'Pair code generated' : 'Generate pair code?'} + + {#if !props.data.loading} + + {#if props.data.code} + Pair code generated for {hub.name}
+ The code below will not be accessible later, please copy it now and update clients with it + {:else} + You are about to generate a pair code for {hub.name}
+ It will be valid for 15 minutes after its creation.
+ There is only one active pair code per hub, newly generated ones will override the older active ones. + {/if} +
+ {/if} +
+ {#if !props.data.loading} + {#if props.data.code} +
+ Pair code: + +
+ + {:else} + + {/if} + {/if} +{/snippet} + +{#snippet regenerateTokenSnippet(props: DialogRenderProps<{ loading: boolean; token: string | null }>)} + + + {props.data.loading ? 'Generating...' : props.data.token ? 'Token generated' : 'Are you sure?'} + + {#if !props.data.loading} + + {#if props.data.token} + New token generated for {hub.name}
+ The code below will not be accessible later, please copy it now and update clients with it + {:else} + You are about to regenerate the token for {hub.name}
+ This action will invalidate the current token and disconnect clients using it
+ Are you sure you want to do this? + {/if} +
+ {/if} +
+ {#if !props.data.loading} + {#if props.data.token} +
+ New token: + +
+ + {:else} + + {/if} + {/if} +{/snippet} @@ -70,11 +219,11 @@ > Disable Wi-Fi hotspot - (pairDialogOpen = true)}>Pair - (regenerateTokenDialogOpen = true)}> + Pair + Regenerate Token - (editDialogOpen = true)}>Edit - (deleteDialogOpen = true)}>Delete + Edit + Delete diff --git a/src/routes/(authenticated)/hubs/dialog-hub-create.svelte b/src/routes/(authenticated)/hubs/dialog-hub-create.svelte deleted file mode 100644 index bddf5815..00000000 --- a/src/routes/(authenticated)/hubs/dialog-hub-create.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - open, (o) => (open = o)}> - - - Create hub - - - - diff --git a/src/routes/(authenticated)/hubs/dialog-hub-delete.svelte b/src/routes/(authenticated)/hubs/dialog-hub-delete.svelte deleted file mode 100644 index ff09fe23..00000000 --- a/src/routes/(authenticated)/hubs/dialog-hub-delete.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - open, (o) => (open = o)}> - - - Are you sure? - - You are about to delete hub "{hub.name}"
- This will also delete the following shockers: -
- {#each hub.shockers as shocker (shocker.id)} - {shocker.name} - {/each} -
-
-
- -
-
diff --git a/src/routes/(authenticated)/hubs/dialog-hub-edit.svelte b/src/routes/(authenticated)/hubs/dialog-hub-edit.svelte deleted file mode 100644 index 43a79550..00000000 --- a/src/routes/(authenticated)/hubs/dialog-hub-edit.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - open, (o) => (open = o)}> - - - Edit hub - - - - - diff --git a/src/routes/(authenticated)/hubs/dialog-hub-pair.svelte b/src/routes/(authenticated)/hubs/dialog-hub-pair.svelte deleted file mode 100644 index f4930ad5..00000000 --- a/src/routes/(authenticated)/hubs/dialog-hub-pair.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - - open, setOpen}> - -
- {loading ? 'Generating...' : code ? 'Pair code generated' : 'Generate pair code?'} - {#if !loading} - - {#if code} - Pair code generated for {hub.name}
- The code below will not be accessible later, please copy it now and update clients with it - {:else} - You are about to generate a pair code for {hub.name}
- It will be vaild for 15 minutes after its creation.
- There is only one active pair code per hub, newly generated ones will override the older active - ones.
- {/if} -
- {/if} -
- {#if !loading} - {#if code} -
- New token: - -
- - {:else} - - {/if} - {/if} -
-
diff --git a/src/routes/(authenticated)/hubs/dialog-hub-regenerate-token.svelte b/src/routes/(authenticated)/hubs/dialog-hub-regenerate-token.svelte deleted file mode 100644 index 06a6fe3e..00000000 --- a/src/routes/(authenticated)/hubs/dialog-hub-regenerate-token.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - open, setOpen}> - -
- {loading ? 'Generating...' : newToken ? 'Token generated' : 'Are you sure?'} - {#if !loading} - - {#if newToken} - New token generated for {hub.name}
- The code below will not be accessible later, please copy it now and update clients with it - {:else} - You are about to regenerate the token for {hub.name}
- This action will invalidate the current token and disconnect clients using it
- Are you sure you want to do this?
- {/if} -
- {/if} -
- {#if !loading} - {#if newToken} -
- New token: - -
- - {:else} - - {/if} - {/if} -
-
diff --git a/src/routes/(authenticated)/shares/user/incoming/incoming-share-item.svelte b/src/routes/(authenticated)/shares/user/incoming/incoming-share-item.svelte index 3cb492eb..59f6bb09 100644 --- a/src/routes/(authenticated)/shares/user/incoming/incoming-share-item.svelte +++ b/src/routes/(authenticated)/shares/user/incoming/incoming-share-item.svelte @@ -8,7 +8,7 @@ import * as Table from '$lib/components/ui/table'; import * as Tooltip from '$lib/components/ui/tooltip'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { openConfirmDialog } from '$lib/stores/ConfirmDialogStore'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { refreshOutgoingInvites } from '$lib/stores/UserSharesStore'; import { toast } from 'svelte-sonner'; @@ -29,14 +29,14 @@ } } - function removeShare() { - openConfirmDialog({ + async function removeShare() { + const result = await dialog.confirm({ title: 'Remove Share', confirmButtonText: 'Remove', data: share, - onConfirm: removeShareCall, descSnippet: confirmDesc, }); + if (result.confirmed) await removeShareCall(result.data); } diff --git a/src/routes/(authenticated)/shares/user/incoming/manage-share.svelte b/src/routes/(authenticated)/shares/user/incoming/manage-share.svelte index 13366eff..faa08375 100644 --- a/src/routes/(authenticated)/shares/user/incoming/manage-share.svelte +++ b/src/routes/(authenticated)/shares/user/incoming/manage-share.svelte @@ -7,7 +7,7 @@ import * as Drawer from '$lib/components/ui/drawer'; import * as Table from '$lib/components/ui/table/index.js'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { openConfirmDialog } from '$lib/stores/ConfirmDialogStore'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { UserShares, refreshUserShares } from '$lib/stores/UserSharesStore'; import { UserStore } from '$lib/stores/UserStore'; import { toast } from 'svelte-sonner'; @@ -36,14 +36,14 @@ } } - function handleDeleteClick(shocker: UserShareInfo) { - openConfirmDialog({ + async function handleDeleteClick(shocker: UserShareInfo) { + const result = await dialog.confirm({ title: 'Confirm removal of Incoming Shocker Share', descSnippet: deleteConfirmDesc, data: shocker, - onConfirm: deleteShockerShare, confirmButtonText: 'Remove Share', }); + if (result.confirmed) await deleteShockerShare(result.data); } diff --git a/src/routes/(authenticated)/shares/user/invites/incoming-invite-item.svelte b/src/routes/(authenticated)/shares/user/invites/incoming-invite-item.svelte index e72f1140..6dbda63d 100644 --- a/src/routes/(authenticated)/shares/user/invites/incoming-invite-item.svelte +++ b/src/routes/(authenticated)/shares/user/invites/incoming-invite-item.svelte @@ -9,7 +9,7 @@ import * as Table from '$lib/components/ui/table'; import * as Tooltip from '$lib/components/ui/tooltip'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { openConfirmDialog } from '$lib/stores/ConfirmDialogStore'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { refreshIncomingInvites } from '$lib/stores/UserSharesStore'; import { cn } from '$lib/utils'; import { toast } from 'svelte-sonner'; @@ -42,14 +42,14 @@ } } - function denyInvite() { - openConfirmDialog({ + async function denyInvite() { + const result = await dialog.confirm({ title: 'Deny Invite', confirmButtonText: 'Deny', data: shareInvite, - onConfirm: denyInviteCall, descSnippet: confirmDesc, }); + if (result.confirmed) await denyInviteCall(result.data); } diff --git a/src/routes/(authenticated)/shares/user/invites/outgoing-invite-item.svelte b/src/routes/(authenticated)/shares/user/invites/outgoing-invite-item.svelte index 0467d907..9ae266c5 100644 --- a/src/routes/(authenticated)/shares/user/invites/outgoing-invite-item.svelte +++ b/src/routes/(authenticated)/shares/user/invites/outgoing-invite-item.svelte @@ -9,7 +9,7 @@ import * as Table from '$lib/components/ui/table'; import * as Tooltip from '$lib/components/ui/tooltip'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { openConfirmDialog } from '$lib/stores/ConfirmDialogStore'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { refreshOutgoingInvites } from '$lib/stores/UserSharesStore'; import { cn } from '$lib/utils'; import { toast } from 'svelte-sonner'; @@ -40,14 +40,14 @@ } } - function removeInvite() { - openConfirmDialog({ + async function removeInvite() { + const result = await dialog.confirm({ title: 'Cancel Invite', confirmButtonText: 'Cancel', data: shareInvite, - onConfirm: removeInviteCall, descSnippet: confirmDesc, }); + if (result.confirmed) await removeInviteCall(result.data); } diff --git a/src/routes/(authenticated)/shares/user/outgoing/edit-share.svelte b/src/routes/(authenticated)/shares/user/outgoing/edit-share.svelte index f10e5fbd..e10c233b 100644 --- a/src/routes/(authenticated)/shares/user/outgoing/edit-share.svelte +++ b/src/routes/(authenticated)/shares/user/outgoing/edit-share.svelte @@ -12,7 +12,7 @@ import MultiPauseToggle from '$lib/components/utils/MultiPauseToggle.svelte'; import PauseToggle from '$lib/components/utils/PauseToggle.svelte'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { openConfirmDialog } from '$lib/stores/ConfirmDialogStore'; + import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { UserShares, refreshUserShares } from '$lib/stores/UserSharesStore'; import { onMount } from 'svelte'; import { toast } from 'svelte-sonner'; @@ -166,14 +166,14 @@ } } - function handleDeleteClick(shocker: EditableShare) { - openConfirmDialog({ + async function handleDeleteClick(shocker: EditableShare) { + const result = await dialog.confirm({ title: 'Confirm Deletion', descSnippet: deleteConfirmDesc, data: shocker, - onConfirm: deleteShockerShare, confirmButtonText: 'Remove', }); + if (result.confirmed) await deleteShockerShare(result.data); } function onTabChanged(value: string) { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index abec3386..ce9a78b1 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,8 +11,8 @@ import Sidebar from './Sidebar.svelte'; import '../app.css'; import { browser } from '$app/environment'; - import DialogManager from '$lib/components/confirm-dialog/dialog-manager.svelte'; import { isMobile } from '$lib/utils/compatibility'; + import DialogManager from '$lib/components/dialog-manager/dialog-manager.svelte'; interface Props { children?: Snippet; From 1803fd1964a323af4504b28e3c2fff0e165865f6 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Thu, 5 Mar 2026 00:47:41 +0100 Subject: [PATCH 2/5] switch to svelte states instead of store --- src/hooks.client.ts | 2 +- .../ClassicControlModule.svelte | 7 +- .../ControlModules/MapControlModule.svelte | 7 +- .../ControlModules/RichControlModule.svelte | 7 +- .../SharedShockerControlModule.svelte | 7 +- .../ControlModules/SimpleControlModule.svelte | 7 +- src/lib/signalr/handlers/DeviceStatus.ts | 20 ++-- src/lib/signalr/handlers/DeviceUpdate.ts | 2 +- src/lib/signalr/handlers/OtaInstallFailed.ts | 15 ++- .../signalr/handlers/OtaInstallProgress.ts | 15 ++- src/lib/signalr/handlers/OtaInstallStarted.ts | 23 ++--- .../signalr/handlers/OtaInstallSucceeded.ts | 13 +-- src/lib/signalr/handlers/OtaRollback.ts | 15 ++- src/lib/signalr/{index.ts => index.svelte.ts} | 47 ++++----- .../{HubsStore.ts => HubsStore.svelte.ts} | 11 ++- src/routes/(anonymous)/login/+page.svelte | 2 +- src/routes/(anonymous)/logout/+page.ts | 2 +- src/routes/(authenticated)/hubs/+page.svelte | 95 +++++++++++++------ .../hubs/[hubId=guid]/update/+page.svelte | 13 +-- src/routes/(authenticated)/hubs/columns.ts | 32 +------ .../hubs/data-table-actions.svelte | 12 +-- .../public/[shareId=guid]/edit/+page.svelte | 2 +- .../edit/dialog-add-shocker.svelte | 4 +- .../shares/user/+layout.svelte | 2 +- .../user/dialog-share-code-create.svelte | 4 +- .../(authenticated)/shockers/own/+page.svelte | 6 +- .../shockers/shared/+page.svelte | 4 +- src/routes/Footer.svelte | 10 +- 28 files changed, 187 insertions(+), 199 deletions(-) rename src/lib/signalr/{index.ts => index.svelte.ts} (66%) rename src/lib/stores/{HubsStore.ts => HubsStore.svelte.ts} (75%) diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 7cba2f5e..db20e461 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,5 +1,5 @@ import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; -import { initializeSignalR } from '$lib/signalr'; +import { initializeSignalR } from '$lib/signalr/index.svelte'; import { backendMetadata } from '$lib/state/BackendMetadata.svelte'; import { initializeDarkModeStore } from '$lib/stores/ColorSchemeStore.svelte'; import { initializeSerialPortsStore } from '$lib/stores/SerialPortsStore'; diff --git a/src/lib/components/ControlModules/ClassicControlModule.svelte b/src/lib/components/ControlModules/ClassicControlModule.svelte index f78a414d..e9c6ffb7 100644 --- a/src/lib/components/ControlModules/ClassicControlModule.svelte +++ b/src/lib/components/ControlModules/ClassicControlModule.svelte @@ -6,7 +6,7 @@ ControlIntensityDefault, ControlIntensityProps, } from '$lib/constants/ControlConstants'; - import { SignalR_Connection } from '$lib/signalr'; + import { getConnection } from '$lib/signalr/index.svelte'; import { ControlType } from '$lib/signalr/models/ControlType'; import { serializeControlMessages } from '$lib/signalr/serializers/Control'; import ControlListener from './ControlListener.svelte'; @@ -26,8 +26,9 @@ let active = $state(null); function ctrl(type: ControlType) { - if (!$SignalR_Connection) return; - serializeControlMessages($SignalR_Connection, [{ id: shocker.id, type, intensity, duration }]); + const conn = getConnection(); + if (!conn) return; + serializeControlMessages(conn, [{ id: shocker.id, type, intensity, duration }]); } diff --git a/src/lib/components/ControlModules/MapControlModule.svelte b/src/lib/components/ControlModules/MapControlModule.svelte index f91ebcd9..39f7034b 100644 --- a/src/lib/components/ControlModules/MapControlModule.svelte +++ b/src/lib/components/ControlModules/MapControlModule.svelte @@ -1,7 +1,7 @@ diff --git a/src/lib/components/ControlModules/SharedShockerControlModule.svelte b/src/lib/components/ControlModules/SharedShockerControlModule.svelte index 3916bf38..6cca0672 100644 --- a/src/lib/components/ControlModules/SharedShockerControlModule.svelte +++ b/src/lib/components/ControlModules/SharedShockerControlModule.svelte @@ -7,7 +7,7 @@ ControlIntensityDefault, ControlIntensityProps, } from '$lib/constants/ControlConstants'; - import { SignalR_Connection } from '$lib/signalr'; + import { getConnection } from '$lib/signalr/index.svelte'; import { ControlType } from '$lib/signalr/models/ControlType'; import { serializeControlMessages } from '$lib/signalr/serializers/Control'; import ControlListener from './ControlListener.svelte'; @@ -42,8 +42,9 @@ const clampedDuration = $derived(Math.min(duration, maxDurationSeconds)); function ctrl(type: ControlType) { - if (!$SignalR_Connection) return; - serializeControlMessages($SignalR_Connection, [ + const conn = getConnection(); + if (!conn) return; + serializeControlMessages(conn, [ { id: shocker.id, type, diff --git a/src/lib/components/ControlModules/SimpleControlModule.svelte b/src/lib/components/ControlModules/SimpleControlModule.svelte index c4902f74..39e7d98a 100644 --- a/src/lib/components/ControlModules/SimpleControlModule.svelte +++ b/src/lib/components/ControlModules/SimpleControlModule.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/signalr/handlers/DeviceStatus.ts b/src/lib/signalr/handlers/DeviceStatus.ts index 64270d3b..38c3510a 100644 --- a/src/lib/signalr/handlers/DeviceStatus.ts +++ b/src/lib/signalr/handlers/DeviceStatus.ts @@ -1,5 +1,5 @@ import { isDeviceOnlineState } from '$lib/signalr/models/DeviceOnlineState'; -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { toast } from 'svelte-sonner'; export function handleSignalrDeviceStatus(array: unknown) { @@ -9,16 +9,12 @@ export function handleSignalrDeviceStatus(array: unknown) { return; } - OnlineHubsStore.update((state) => { - array.forEach((entry) => { - state.set(entry.device, { - hubId: entry.device, - isOnline: entry.online, - firmwareVersion: entry.firmwareVersion, - otaInstall: null, - }); + for (const entry of array) { + onlineHubs.set(entry.device, { + hubId: entry.device, + isOnline: entry.online, + firmwareVersion: entry.firmwareVersion, + otaInstall: null, }); - - return state; - }); + } } diff --git a/src/lib/signalr/handlers/DeviceUpdate.ts b/src/lib/signalr/handlers/DeviceUpdate.ts index 535bcfd9..806ba8f2 100644 --- a/src/lib/signalr/handlers/DeviceUpdate.ts +++ b/src/lib/signalr/handlers/DeviceUpdate.ts @@ -1,5 +1,5 @@ import { HubUpdateType, isHubUpdateType } from '$lib/signalr/models/HubUpdateType'; -import { refreshOwnHubs } from '$lib/stores/HubsStore'; +import { refreshOwnHubs } from '$lib/stores/HubsStore.svelte'; import { isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; diff --git a/src/lib/signalr/handlers/OtaInstallFailed.ts b/src/lib/signalr/handlers/OtaInstallFailed.ts index aafd9692..b87658b5 100644 --- a/src/lib/signalr/handlers/OtaInstallFailed.ts +++ b/src/lib/signalr/handlers/OtaInstallFailed.ts @@ -1,4 +1,4 @@ -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { isBoolean, isNumber, isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; @@ -17,12 +17,9 @@ export function handleSignalrOtaInstallFailed( return; } - OnlineHubsStore.update((hubs) => { - const hub = hubs.get(hubId); - if (hub && hub.otaInstall?.id === updateId) { - hub.otaInstall = null; - //hub.otaError = { fatal, message }; - } - return hubs; - }); + const hub = onlineHubs.get(hubId); + if (hub && hub.otaInstall?.id === updateId) { + hub.otaInstall = null; + //hub.otaError = { fatal, message }; + } } diff --git a/src/lib/signalr/handlers/OtaInstallProgress.ts b/src/lib/signalr/handlers/OtaInstallProgress.ts index 97dab815..5b2c44b3 100644 --- a/src/lib/signalr/handlers/OtaInstallProgress.ts +++ b/src/lib/signalr/handlers/OtaInstallProgress.ts @@ -1,5 +1,5 @@ import { isOtaUpdateProgressTask } from '$lib/signalr/models/OtaUpdateProgressTask'; -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { isNumber, isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; @@ -23,12 +23,9 @@ export function handleSignalrOtaInstallProgress( return; } - OnlineHubsStore.update((hubs) => { - const hub = hubs.get(hubId); - if (hub && hub.otaInstall?.id === updateId) { - hub.otaInstall.task = task; - hub.otaInstall.progress = progress; - } - return hubs; - }); + const hub = onlineHubs.get(hubId); + if (hub && hub.otaInstall?.id === updateId) { + hub.otaInstall.task = task; + hub.otaInstall.progress = progress; + } } diff --git a/src/lib/signalr/handlers/OtaInstallStarted.ts b/src/lib/signalr/handlers/OtaInstallStarted.ts index e073c695..25ba5308 100644 --- a/src/lib/signalr/handlers/OtaInstallStarted.ts +++ b/src/lib/signalr/handlers/OtaInstallStarted.ts @@ -1,4 +1,4 @@ -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { isNumber, isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; @@ -16,16 +16,13 @@ export function handleSignalrOtaInstallStarted( return; } - OnlineHubsStore.update((hubs) => { - const hub = hubs.get(hubId); - if (hub) { - hub.otaInstall = { - id: updateId, - version: targetVersion, - task: 0, - progress: 0, - }; - } - return hubs; - }); + const hub = onlineHubs.get(hubId); + if (hub) { + hub.otaInstall = { + id: updateId, + version: targetVersion, + task: 0, + progress: 0, + }; + } } diff --git a/src/lib/signalr/handlers/OtaInstallSucceeded.ts b/src/lib/signalr/handlers/OtaInstallSucceeded.ts index e58d8cc6..8ac647b7 100644 --- a/src/lib/signalr/handlers/OtaInstallSucceeded.ts +++ b/src/lib/signalr/handlers/OtaInstallSucceeded.ts @@ -1,4 +1,4 @@ -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { isNumber, isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; @@ -12,11 +12,8 @@ export function handleSignalrOtaInstallSucceeded(hubId: unknown, updateId: unkno return; } - OnlineHubsStore.update((hubs) => { - const hub = hubs.get(hubId); - if (hub && hub.otaInstall?.id === updateId) { - hub.otaInstall = null; - } - return hubs; - }); + const hub = onlineHubs.get(hubId); + if (hub && hub.otaInstall?.id === updateId) { + hub.otaInstall = null; + } } diff --git a/src/lib/signalr/handlers/OtaRollback.ts b/src/lib/signalr/handlers/OtaRollback.ts index 64d9fa2d..d51f6ee7 100644 --- a/src/lib/signalr/handlers/OtaRollback.ts +++ b/src/lib/signalr/handlers/OtaRollback.ts @@ -1,4 +1,4 @@ -import { OnlineHubsStore } from '$lib/stores/HubsStore'; +import { onlineHubs } from '$lib/stores/HubsStore.svelte'; import { isNumber, isString } from '$lib/typeguards'; import { toast } from 'svelte-sonner'; @@ -12,12 +12,9 @@ export function handleSignalrOtaRollback(hubId: unknown, updateId: unknown): voi return; } - OnlineHubsStore.update((hubs) => { - const hub = hubs.get(hubId); - if (hub && hub.otaInstall?.id === updateId) { - hub.otaInstall = null; - //hub.otaError = { fatal: false, message: 'Rollback performed' }; - } - return hubs; - }); + const hub = onlineHubs.get(hubId); + if (hub && hub.otaInstall?.id === updateId) { + hub.otaInstall = null; + //hub.otaError = { fatal: false, message: 'Rollback performed' }; + } } diff --git a/src/lib/signalr/index.ts b/src/lib/signalr/index.svelte.ts similarity index 66% rename from src/lib/signalr/index.ts rename to src/lib/signalr/index.svelte.ts index 23e71210..c05ad70c 100644 --- a/src/lib/signalr/index.ts +++ b/src/lib/signalr/index.svelte.ts @@ -8,7 +8,6 @@ import { LogLevel, } from '@microsoft/signalr'; import { toast } from 'svelte-sonner'; -import { type Readable, get, writable } from 'svelte/store'; import { handleSignalrDeviceStatus, handleSignalrDeviceUpdate, @@ -22,11 +21,18 @@ import { const BackendHubUserUrl = getBackendURL('1/hubs/user').href; -const signalr_connection = writable(null); -const signalr_state = writable(HubConnectionState.Disconnected); +let connection = $state(null); +let connectionState = $state(HubConnectionState.Disconnected); + +export function getConnection(): HubConnection | null { + return connection; +} + +export function getConnectionState(): HubConnectionState { + return connectionState; +} export async function initializeSignalR() { - let connection = get(signalr_connection); if (connection) { return; } @@ -41,21 +47,21 @@ export async function initializeSignalR() { .build(); connection.onclose(() => { - signalr_state.set(HubConnectionState.Disconnected); + connectionState = HubConnectionState.Disconnected; }); connection.onreconnecting(() => { - signalr_state.set(HubConnectionState.Reconnecting); + connectionState = HubConnectionState.Reconnecting; }); connection.onreconnected(() => { - signalr_state.set(HubConnectionState.Connected); + connectionState = HubConnectionState.Connected; }); // Look up in OpenShock API repository: Common/Hubs/IUserHub.cs connection.on('Welcome', () => { // Arg is the SignalR connectionId - signalr_state.set(HubConnectionState.Connected); + connectionState = HubConnectionState.Connected; }); connection.on('Log', handleSignalrLog); @@ -69,39 +75,26 @@ export async function initializeSignalR() { connection.on('OtaInstallFailed', handleSignalrOtaInstallFailed); connection.on('OtaRollback', handleSignalrOtaRollback); - signalr_connection.set(connection); - try { await connection.start(); - signalr_state.set(HubConnectionState.Connected); + connectionState = HubConnectionState.Connected; } catch (error) { console.error(error); toast.error('Failed to connect to server!'); - signalr_state.set(HubConnectionState.Disconnected); + connectionState = HubConnectionState.Disconnected; } } export async function destroySignalR() { - if (!signalr_connection) return; + if (!connection) return; try { - const connection = get(signalr_connection); - if (connection) { - await connection.stop(); - } + await connection.stop(); } catch (error) { console.error(error); toast.error('Encountered error while disconnecting from server!'); } finally { - signalr_connection.set(null); - signalr_state.set(HubConnectionState.Disconnected); + connection = null; + connectionState = HubConnectionState.Disconnected; } } - -export const SignalR_State = { - subscribe: signalr_state.subscribe, -} as Readable; - -export const SignalR_Connection = { - subscribe: signalr_connection.subscribe, -} as Readable; diff --git a/src/lib/stores/HubsStore.ts b/src/lib/stores/HubsStore.svelte.ts similarity index 75% rename from src/lib/stores/HubsStore.ts rename to src/lib/stores/HubsStore.svelte.ts index 1eeaabc3..442b11bc 100644 --- a/src/lib/stores/HubsStore.ts +++ b/src/lib/stores/HubsStore.svelte.ts @@ -1,8 +1,8 @@ +import { SvelteMap } from 'svelte/reactivity'; import { shockersV1Api } from '$lib/api'; import type { DeviceWithShockersResponse } from '$lib/api/internal/v1'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import type { OtaUpdateProgressTask } from '$lib/signalr/models/OtaUpdateProgressTask'; -import { writable } from 'svelte/store'; export type OwnHub = DeviceWithShockersResponse; export type HubOnlineState = { @@ -17,8 +17,8 @@ export type HubOnlineState = { } | null; }; -export const OwnHubsStore = writable>(new Map()); -export const OnlineHubsStore = writable>(new Map()); +export const ownHubs = new SvelteMap(); +export const onlineHubs = new SvelteMap(); export async function refreshOwnHubs() { try { @@ -27,7 +27,10 @@ export async function refreshOwnHubs() { throw new Error(`Failed to fetch devices: ${response.message}`); } - OwnHubsStore.set(new Map(response.data.map((d) => [d.id, d]))); + ownHubs.clear(); + for (const d of response.data) { + ownHubs.set(d.id, d); + } } catch (error) { handleApiError(error); } diff --git a/src/routes/(anonymous)/login/+page.svelte b/src/routes/(anonymous)/login/+page.svelte index 615e8eea..6c1b137c 100644 --- a/src/routes/(anonymous)/login/+page.svelte +++ b/src/routes/(anonymous)/login/+page.svelte @@ -15,7 +15,7 @@ import Turnstile from '$lib/components/Turnstile.svelte'; import { accountV2Api } from '$lib/api'; import { UserStore } from '$lib/stores/UserStore'; - import { initializeSignalR } from '$lib/signalr'; + import { initializeSignalR } from '$lib/signalr/index.svelte'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { isValidationError, mapToValRes } from '$lib/errorhandling/ValidationProblemDetails'; import OauthButtons from '$lib/components/auth/oauth-buttons.svelte'; diff --git a/src/routes/(anonymous)/logout/+page.ts b/src/routes/(anonymous)/logout/+page.ts index 4bf89455..8b36d09f 100644 --- a/src/routes/(anonymous)/logout/+page.ts +++ b/src/routes/(anonymous)/logout/+page.ts @@ -2,7 +2,7 @@ import { browser } from '$app/environment'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { accountV1Api } from '$lib/api'; -import { destroySignalR } from '$lib/signalr'; +import { destroySignalR } from '$lib/signalr/index.svelte'; import { UserStore } from '$lib/stores/UserStore'; export const prerender = false; diff --git a/src/routes/(authenticated)/hubs/+page.svelte b/src/routes/(authenticated)/hubs/+page.svelte index f5d794f0..30590885 100644 --- a/src/routes/(authenticated)/hubs/+page.svelte +++ b/src/routes/(authenticated)/hubs/+page.svelte @@ -1,17 +1,15 @@ -{#snippet createHubSnippet(props: DialogRenderProps<{ name: string }, { name: string } | undefined>)} +{#snippet createHubSnippet( + props: DialogRenderProps<{ name: string }, { name: string } | undefined> +)} Create Hub - {/snippet} @@ -95,26 +93,61 @@ This is a list of all hubs you own. -
+
{#if isMobile.current} - {#each data as hub (hub.id)} -
-
- -
- {hub.name} - {#if hub.firmware_version} - {hub.firmware_version} - {:else} - Offline - {/if} +
+ {#each data as hub (hub.id)} +
+
+ +
+ {hub.name} + {#if hub.is_online && hub.firmware_version} + {hub.firmware_version} + {:else} + Offline + {/if} +
+
- -
- {/each} + {/each} +
{:else} - + + + + Name + Status + Version + Created + + + + + {#each data as hub (hub.id)} + + {hub.name} + + {#if hub.is_online} + Online + {:else} + Offline + {/if} + + {hub.firmware_version ?? '—'} + {hub.created_at.toLocaleDateString()} + + + + + {:else} + + No hubs found. + + {/each} + + {/if}
diff --git a/src/routes/(authenticated)/hubs/[hubId=guid]/update/+page.svelte b/src/routes/(authenticated)/hubs/[hubId=guid]/update/+page.svelte index 13c7b3f7..a9429ada 100644 --- a/src/routes/(authenticated)/hubs/[hubId=guid]/update/+page.svelte +++ b/src/routes/(authenticated)/hubs/[hubId=guid]/update/+page.svelte @@ -10,9 +10,9 @@ import { Progress } from '$lib/components/ui/progress'; import * as Table from '$lib/components/ui/table'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; - import { SignalR_Connection } from '$lib/signalr'; + import { getConnection } from '$lib/signalr/index.svelte'; import { serializeOtaInstallMessage } from '$lib/signalr/serializers/OtaInstall'; - import { type HubOnlineState, OnlineHubsStore } from '$lib/stores/HubsStore'; + import { type HubOnlineState, onlineHubs } from '$lib/stores/HubsStore.svelte'; import { cn } from '$lib/utils'; import { NumberToHexPadded } from '$lib/utils/convert'; @@ -21,8 +21,9 @@ let version = $state(null); function startUpdate() { - if ($SignalR_Connection === null || hub === null || version === null) return; - serializeOtaInstallMessage($SignalR_Connection, hub.hubId, version); + const conn = getConnection(); + if (conn === null || hub === null || version === null) return; + serializeOtaInstallMessage(conn, hub.hubId, version); } let isLoading = $state(false); @@ -41,7 +42,7 @@ return; } - hub = $OnlineHubsStore.get(hubId) ?? { + hub = onlineHubs.get(hubId) ?? { hubId, isOnline: false, firmwareVersion: null, @@ -76,7 +77,7 @@