poe-svelte-ui-lib 1.9.23 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,14 +83,14 @@
83
83
  wrapperClass="{Header.value === 'SET' ? 'mt-1' : ''} "
84
84
  value={component.eventHandler.Argument}
85
85
  maxlength={32}
86
- disabled={Header.value === "SET" && (component.eventHandler.Argument == "Save" || component.eventHandler.Argument == "NoSave")}
86
+ disabled={Header.value === "SET" && component.eventHandler.Argument == "Save"}
87
87
  help={{ info: $T("constructor.props.argument.info"), autocomplete: "on", regExp: /^[a-zA-Z0-9\-_]{0,32}$/ }}
88
88
  onUpdate={(value) => onPropertyChange({ eventHandler: { Argument: value as string } })}
89
89
  />
90
90
  {/snippet}
91
91
 
92
92
  {#snippet ButtonVariables()}
93
- {#if (component.eventHandler.Argument !== "Save" && component.eventHandler.Argument !== "NoSave") || Header.value === "SET"}
93
+ {#if component.eventHandler.Argument !== "Save" || Header.value === "SET"}
94
94
  <UI.Input
95
95
  label={{ name: $T("constructor.props.value") }}
96
96
  value={component.eventHandler.Value}
@@ -71,7 +71,7 @@
71
71
  wrapperClass="{Header.value === 'SET' ? 'mt-1' : ''} "
72
72
  value={component.eventHandler.Argument}
73
73
  maxlength={32}
74
- disabled={component.eventHandler.Argument == "Save" || component.eventHandler.Argument == "NoSave"}
74
+ disabled={component.eventHandler.Argument == "Save"}
75
75
  help={{ info: $T("constructor.props.argument.info"), autocomplete: "on", regExp: /^[a-zA-Z0-9\-_]{0,32}$/ }}
76
76
  onUpdate={(value) => onPropertyChange({ eventHandler: { Argument: value as string } })}
77
77
  />
@@ -15,7 +15,7 @@
15
15
 
16
16
  <div
17
17
  id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
18
- class={twMerge(`relative flex w-full flex-col items-center ${background ? "rounded-2xl bg-max px-6 py-2" : ""}`, wrapperClass)}
18
+ class={twMerge(`relative flex w-full flex-col items-center ${background ? "rounded-2xl bg-(--back-color) px-6 py-2" : ""}`, wrapperClass)}
19
19
  >
20
20
  <p class={twMerge(`w-full text-center ${textSize[content.size ?? "base"]}`, content.class)}>
21
21
  {content.name}
@@ -2,42 +2,56 @@
2
2
  import Button from "../Button/Button.svelte"
3
3
  import Error from "../libIcons/Error.svelte"
4
4
  import { T } from "../locales/i18n"
5
- import type { IVideoViewerProps } from "../types"
5
+ import Select from "../Select/Select.svelte"
6
+ import type { IOption, IVideoViewerProps } from "../types"
6
7
  import { onMount } from "svelte"
8
+ import { twMerge } from "tailwind-merge"
7
9
 
8
- let { id = crypto.randomUUID(), stream = $bindable(), onstream, ondevicechange }: IVideoViewerProps = $props()
10
+ let { id = crypto.randomUUID(), wrapperClass, label = { name: "", class: "" }, showSelect = true }: IVideoViewerProps = $props()
9
11
 
10
12
  let videoElement = $state<HTMLVideoElement | null>(null)
11
- // let stream = $state<MediaStream | null>(null)
13
+ let stream = $state<MediaStream | null>(null)
12
14
  let error = $state<string | null>(null)
13
15
  let isStreaming = $state(false)
14
- // let source = $state<MediaDeviceInfo[]>([])
16
+ let sources = $state<MediaDeviceInfo[]>([])
15
17
  let loading = $state(false)
18
+ let devId: string = $state("")
16
19
 
17
- // const getDevices = async (): Promise<(MediaDeviceInfo)[]> => {
18
- // try {
19
- // const deviceList = await navigator.mediaDevices.enumerateDevices()
20
- // source = deviceList.filter((d) => d.kind === "videoinput")
21
- // ondevicechange?.(source)
22
- // return source
23
- // } catch (e) {
24
- // console.error("Failed to enumerate source:", e)
25
- // return []
26
- // }
27
- // }
28
-
29
- const startStream = async () => {
20
+ export const getDevices = async (): Promise<MediaDeviceInfo[]> => {
21
+ try {
22
+ const deviceList = await navigator.mediaDevices.enumerateDevices()
23
+ sources = deviceList.filter((d) => d.kind === "videoinput")
24
+ return sources
25
+ } catch (e) {
26
+ return []
27
+ }
28
+ }
29
+
30
+ export const startStream = async (devId?: string) => {
30
31
  loading = true
31
32
  error = null
32
33
 
33
34
  try {
34
35
  if (stream) stopStream()
35
36
 
37
+ const deviceId = devId ?? sources[0].deviceId
38
+
39
+ const constraints: MediaStreamConstraints = {
40
+ video: {
41
+ width: { ideal: 640 },
42
+ height: { ideal: 480 },
43
+ facingMode: "user",
44
+ ...(deviceId && { deviceId: { exact: deviceId } }),
45
+ },
46
+ audio: false,
47
+ }
48
+
49
+ stream = await navigator.mediaDevices.getUserMedia(constraints)
50
+
36
51
  if (videoElement) {
37
52
  videoElement.srcObject = stream as MediaStream
38
53
  await videoElement.play()
39
54
  isStreaming = true
40
- onstream?.(stream as MediaStream)
41
55
  }
42
56
  } catch (e: unknown) {
43
57
  const errorMessage = e instanceof Error ? e.message : $T("constructor.props.video.viewer.no.access")
@@ -48,7 +62,7 @@
48
62
  }
49
63
  }
50
64
 
51
- const stopStream = () => {
65
+ export const stopStream = () => {
52
66
  if (stream) {
53
67
  stream.getTracks().forEach((track) => track.stop())
54
68
  stream = null
@@ -59,89 +73,84 @@
59
73
  isStreaming = false
60
74
  }
61
75
 
62
- // const captureScreenshot = (): string | null => {
63
- // if (!videoElement || !isStreaming) return null
64
-
65
- // const canvas = document.createElement("canvas")
66
- // canvas.width = videoElement.videoWidth
67
- // canvas.height = videoElement.videoHeight
68
-
69
- // const context = canvas.getContext("2d")
70
- // if (!context) return null
71
-
72
- // context.drawImage(videoElement, 0, 0)
73
- // return canvas.toDataURL("image/png")
74
- // }
75
-
76
- // const downloadScreenshot = (filename: string = "screenshot.png") => {
77
- // const dataUrl = captureScreenshot()
78
- // if (!dataUrl) return
79
-
80
- // const link = document.createElement("a")
81
- // link.href = dataUrl
82
- // link.download = filename
83
- // link.click()
84
- // }
85
-
86
76
  const handlePermission = async () => {
87
77
  try {
88
78
  const permission = await navigator.permissions.query({ name: "camera" as PermissionName })
89
79
  if (permission.state === "granted") {
90
- // await getDevices()
80
+ await getDevices()
91
81
 
92
82
  await startStream()
93
83
  } else if (permission.state === "prompt") {
94
- // await getDevices()
84
+ await getDevices()
95
85
  await startStream()
96
86
  } else {
97
87
  error = $T("constructor.props.video.viewer.no.permission")
98
88
  }
99
89
  } catch {
100
- // await getDevices()
90
+ await getDevices()
101
91
  await startStream()
102
92
  }
103
93
  }
104
94
 
105
- $effect(() => {
95
+ setInterval(async () => {
96
+ sources = await getDevices()
97
+ }, 1000)
98
+
99
+ onMount(() => {
100
+ getDevices()
106
101
  if (typeof window !== "undefined" && "mediaDevices" in navigator) {
107
102
  handlePermission()
108
103
  }
109
-
110
104
  return () => {
111
105
  stopStream()
112
106
  }
113
107
  })
114
-
115
- onMount(() => {
116
- if (videoElement) {
117
- videoElement.addEventListener("loadedmetadata", () => {
118
- if (!videoElement!.paused) {
119
- videoElement!.play().catch(console.error)
120
- }
121
- })
122
- }
123
- })
124
108
  </script>
125
109
 
126
- <div
127
- id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
128
- class="relative h-full bg-(--border-color)/50 rounded-xl shadow-sm transition duration-200 hover:shadow-md"
129
- >
130
- {#if loading}
131
- <div class="h-full w-full flex items-center justify-center rounded-xl bg-(--border-color)/50 z-10">
132
- <h2>{$T("constructor.props.video.viewer.loading")}</h2>
133
- </div>
110
+ <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge("flex flex-col w-full h-full items-center", wrapperClass)}>
111
+ {#if label.name}
112
+ <h5 class={twMerge(`w-full px-4 text-center`, label.class)}>{label.name}</h5>
134
113
  {/if}
135
-
136
- {#if error}
137
- <div class="h-full w-full flex flex-col items-center justify-center rounded-xl bg-(--border-color)/50 gap-4 z-10">
138
- <div class="flex flex-col items-center justify-center gap-4 text-red-500">
139
- <Error />
140
- <h4>{error}</h4>
114
+ <div class="relative flex flex-1 w-full items-center justify-center">
115
+ {#if loading || error}
116
+ <div class="absolute h-full w-full rounded-2xl px-40 py-15 flex flex-col items-center justify-center bg-(--border-color)/50 gap-4 z-10">
117
+ {#if loading}
118
+ <h2>{$T("constructor.props.camera.loading")}</h2>
119
+ {:else if error}
120
+ <svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121
+ <path
122
+ stroke-linecap="round"
123
+ stroke-linejoin="round"
124
+ stroke-width="2"
125
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
126
+ />
127
+ </svg>
128
+ <h5 class="text-red-600">{error}</h5>
129
+ <Button wrapperClass="w-fit" content={{ name: $T("library.retry") }} componentClass="bg-blue px-5" onClick={handlePermission} />
130
+ {/if}
141
131
  </div>
142
- <Button wrapperClass="w-fit" content={{ name: $T("library.retry") }} componentClass="bg-blue px-5" onClick={handlePermission} />
132
+ {/if}
133
+ <div class="absolute h-full w-full rounded-2xl bg-(--border-color)/50">
134
+ <video bind:this={videoElement} class="h-full w-full rounded-2xl object-contain block {!isStreaming || loading ? 'invisible' : 'visible'}"></video>
143
135
  </div>
136
+ </div>
137
+
138
+ {#if showSelect}
139
+ {@const deviceOptions = sources.map((dev) => {
140
+ return { id: dev.deviceId, name: dev.label, value: dev.deviceId }
141
+ })}
142
+ <Select
143
+ wrapperClass="w-[50%]"
144
+ disabled={deviceOptions.length == 0}
145
+ type="buttons"
146
+ label={{ name: $T("constructor.props.source") }}
147
+ value={deviceOptions.find((p) => p.value == devId) || deviceOptions[0]}
148
+ options={deviceOptions}
149
+ onUpdate={async (value) => {
150
+ stopStream()
151
+ devId = (value as IOption).value as string
152
+ await startStream(devId)
153
+ }}
154
+ />
144
155
  {/if}
145
-
146
- <video bind:this={videoElement} class="w-full h-full block {!isStreaming || loading ? 'hidden' : 'visible'}"></video>
147
156
  </div>
@@ -1,4 +1,8 @@
1
1
  import type { IVideoViewerProps } from "../types";
2
- declare const VideoViewer: import("svelte").Component<IVideoViewerProps, {}, "stream">;
2
+ declare const VideoViewer: import("svelte").Component<IVideoViewerProps, {
3
+ getDevices: () => Promise<MediaDeviceInfo[]>;
4
+ startStream: (devId?: string) => Promise<void>;
5
+ stopStream: () => void;
6
+ }, "">;
3
7
  type VideoViewer = ReturnType<typeof VideoViewer>;
4
8
  export default VideoViewer;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { T } from "../locales/i18n"
3
+ import { updateProperty, type IUIComponentHandler, type UIComponent } from "../types"
4
+ import * as UI from ".."
5
+ import CommonSnippets from "../CommonSnippets.svelte"
6
+
7
+ const {
8
+ component,
9
+ onPropertyChange,
10
+ forConstructor = true,
11
+ } = $props<{
12
+ component: UIComponent & { properties: Partial<UI.IVideoViewerProps> }
13
+ onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string; eventHandler?: IUIComponentHandler }>) => void
14
+ forConstructor?: boolean
15
+ }>()
16
+ </script>
17
+
18
+ {#snippet VideoViewershowSelect()}
19
+ <UI.Switch
20
+ label={{ name: $T("constructor.props.showSelect") }}
21
+ value={component.properties?.showSelect ? 1 : 0}
22
+ options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
23
+ onChange={(value) => updateProperty("showSelect", value, component, onPropertyChange)}
24
+ />
25
+ {/snippet}
26
+
27
+ {#if forConstructor}
28
+ <div class="flex mb-4 justify-center gap-8">
29
+ <div class="flex w-1/3 flex-col px-2">
30
+ <CommonSnippets snippet="Access" {component} {onPropertyChange} />
31
+ </div>
32
+ <div class="flex w-1/3 flex-col px-2">
33
+ <CommonSnippets snippet="Label" {component} {onPropertyChange} />
34
+ </div>
35
+ <div class="flex w-1/3 flex-col px-2">
36
+ {@render VideoViewershowSelect()}
37
+ </div>
38
+ </div>
39
+ {:else}
40
+ <div class="flex mb-4 justify-center gap-8">
41
+ <div class="flex w-1/3 flex-col px-2">
42
+ <CommonSnippets snippet="Identificator" {component} {onPropertyChange} />
43
+ <CommonSnippets snippet="Access" {component} {onPropertyChange} />
44
+ </div>
45
+ <div class="flex w-1/3 flex-col px-2">
46
+ <CommonSnippets snippet="WrapperClass" {component} {onPropertyChange} />
47
+ <CommonSnippets snippet="Label" {component} {onPropertyChange} />
48
+ </div>
49
+ <div class="flex w-1/3 flex-col px-2">
50
+ {@render VideoViewershowSelect()}
51
+ </div>
52
+ </div>
53
+ {/if}
@@ -0,0 +1,17 @@
1
+ import { type IUIComponentHandler, type UIComponent } from "../types";
2
+ import * as UI from "..";
3
+ type $$ComponentProps = {
4
+ component: UIComponent & {
5
+ properties: Partial<UI.IVideoViewerProps>;
6
+ };
7
+ onPropertyChange: (updates: Partial<{
8
+ properties?: string | object;
9
+ name?: string;
10
+ access?: string;
11
+ eventHandler?: IUIComponentHandler;
12
+ }>) => void;
13
+ forConstructor?: boolean;
14
+ };
15
+ declare const VideoViewerProps: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type VideoViewerProps = ReturnType<typeof VideoViewerProps>;
17
+ export default VideoViewerProps;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export { default as TabsProps } from "./Tabs/TabsProps.svelte";
32
32
  export { default as TextField } from "./TextField/TextField.svelte";
33
33
  export { default as TextFieldProps } from "./TextField/TextFieldProps.svelte";
34
34
  export { default as VideoViewer } from "./VideoViewer/VideoViewer.svelte";
35
+ export { default as VideoViewerProps } from "./VideoViewer/VideoViewerProps.svelte";
35
36
  export { default as Widget } from "./Widget/Widget.svelte";
36
37
  export { default as WidgetProps } from "./Widget/WidgetProps.svelte";
37
38
  export * from "./locales/i18n";
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ export { default as TabsProps } from "./Tabs/TabsProps.svelte";
33
33
  export { default as TextField } from "./TextField/TextField.svelte";
34
34
  export { default as TextFieldProps } from "./TextField/TextFieldProps.svelte";
35
35
  export { default as VideoViewer } from "./VideoViewer/VideoViewer.svelte";
36
- // export { default as VideoViewerProps } from "./VideoViewer/VideoViewerProps.svelte"
36
+ export { default as VideoViewerProps } from "./VideoViewer/VideoViewerProps.svelte";
37
37
  export { default as Widget } from "./Widget/Widget.svelte";
38
38
  export { default as WidgetProps } from "./Widget/WidgetProps.svelte";
39
39
  export * from "./locales/i18n";
@@ -176,9 +176,11 @@ const translations = {
176
176
  "constructor.props.file.notselected": "Файл не выбран",
177
177
  "constructor.props.map.loading": "Загрузка карты...",
178
178
  "constructor.props.map.noconnection": "Карта не загружена. Нет подключения к интернету",
179
- "constructor.props.video.viewer.loading": "Загрузка камеры...",
179
+ "constructor.props.camera.loading": "Загрузка камеры...",
180
180
  "constructor.props.video.viewer.no.permission": "Нет разрешения для доступа к камере",
181
181
  "constructor.props.video.viewer.no.access": "Камера недоступна",
182
+ "constructor.props.source": "Источник",
183
+ "constructor.props.showSelect": "Выбор источника",
182
184
  "constructor.props.widget.mode": "Зацикленный",
183
185
  "constructor.props.table.type": "Тип таблицы",
184
186
  "constructor.props.table.type.table": "Статическая таблица",
package/dist/options.js CHANGED
@@ -13,12 +13,10 @@ export const optionsStore = derived(T, ($T) => {
13
13
  { id: id(), name: "ER!", value: "ER!" },
14
14
  ],
15
15
  SHORT_ARGUMENT_OPTION: [
16
- { id: id(), value: "NoSave", name: $T("constructor.props.action.update") },
17
16
  { id: id(), value: "Save", name: $T("constructor.props.action.save") },
18
17
  { id: id(), value: "NoSend", name: $T("constructor.props.action.nosend") },
19
18
  ],
20
19
  FULL_ARGUMENT_OPTION: [
21
- { id: id(), value: "NoSave", name: $T("constructor.props.action.update") },
22
20
  { id: id(), value: "Save", name: $T("constructor.props.action.save") },
23
21
  { id: id(), value: "", name: $T("constructor.props.action.custom") },
24
22
  ],
package/dist/types.d.ts CHANGED
@@ -413,9 +413,12 @@ export interface ITextFieldProps {
413
413
  }
414
414
  export interface IVideoViewerProps {
415
415
  id?: string;
416
- stream?: MediaStream | null;
417
- onstream?: (stream: MediaStream) => void;
418
- ondevicechange?: (devices: MediaDeviceInfo[]) => void;
416
+ wrapperClass?: string;
417
+ label?: {
418
+ name?: string;
419
+ class?: string;
420
+ };
421
+ showSelect?: boolean;
419
422
  }
420
423
  export interface IWidgetProps {
421
424
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.9.23",
3
+ "version": "1.10.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,23 +37,23 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@sveltejs/adapter-static": "^3.0.10",
40
- "@sveltejs/kit": "^2.61.1",
41
- "@sveltejs/package": "^2.5.7",
40
+ "@sveltejs/kit": "^2.68.0",
41
+ "@sveltejs/package": "^2.5.8",
42
42
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
43
- "@tailwindcss/vite": "^4.3.0",
43
+ "@tailwindcss/vite": "^4.3.1",
44
44
  "@types/maplibre-gl": "^1.14.0",
45
- "@types/node": "^25.9.1",
46
- "dompurify": "^3.4.7",
47
- "marked": "^18.0.4",
48
- "prettier": "^3.8.3",
49
- "prettier-plugin-svelte": "^4.0.1",
45
+ "@types/node": "^26.0.1",
46
+ "dompurify": "^3.4.11",
47
+ "marked": "^18.0.5",
48
+ "prettier": "^3.9.3",
49
+ "prettier-plugin-svelte": "^4.1.1",
50
50
  "prettier-plugin-tailwindcss": "^0.8.0",
51
51
  "publint": "^0.3.21",
52
- "svelte": "^5.55.10",
53
- "tailwindcss": "^4.3.0",
54
- "tsx": "^4.22.3",
52
+ "svelte": "^5.56.4",
53
+ "tailwindcss": "^4.3.1",
54
+ "tsx": "^4.22.4",
55
55
  "typescript": "^6.0.3",
56
- "vite": "^8.0.14",
56
+ "vite": "^8.1.0",
57
57
  "vite-plugin-compression": "^0.5.1"
58
58
  }
59
59
  }