@nikala-ui/hooks 0.8.0 → 0.9.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.
- package/README.md +15 -1
- package/package.json +1 -1
- package/src/create-audio-video.ts +444 -0
- package/src/create-battery.ts +78 -0
- package/src/create-document-title.ts +33 -0
- package/src/create-event-source.ts +126 -0
- package/src/create-favicon.ts +52 -0
- package/src/create-fetch.ts +93 -0
- package/src/create-fullscreen.ts +105 -0
- package/src/create-geolocation.ts +140 -0
- package/src/create-infinite-scroll.ts +90 -0
- package/src/create-orientation.ts +129 -0
- package/src/create-permission.ts +91 -0
- package/src/create-scroll-into-view.ts +57 -0
- package/src/create-undo-redo.ts +100 -0
- package/src/create-web-notification.ts +111 -0
- package/src/create-websocket.ts +179 -0
- package/src/index.ts +16 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Official Documentation & Interactive Demos: [nikala.magradze.dev](https://nikala
|
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
|
-
This package provides reusable, fine-grained reactive primitives designed natively for SolidJS applications. It simplifies managing complex component state such as controlled and uncontrolled inputs, state synchronization, and reactive event callbacks.
|
|
13
|
+
This package provides reusable, fine-grained reactive primitives designed natively for SolidJS applications. It simplifies managing complex component state such as controlled and uncontrolled inputs, state synchronization, browser APIs, and reactive event callbacks.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -41,6 +41,20 @@ This package provides reusable, fine-grained reactive primitives designed native
|
|
|
41
41
|
- **`createInputMask`** — SolidJS reactive primitive for input value masking (phone numbers, credit cards, dates).
|
|
42
42
|
- **`createIdle`** — SolidJS reactive primitive for detecting user inactivity with customizable timeout and activity events.
|
|
43
43
|
- **`createActiveElement`** — SolidJS reactive primitive for tracking the currently focused DOM element.
|
|
44
|
+
- **`createInfiniteScroll`** — SolidJS reactive primitive for infinite scrolling data fetching and threshold triggers.
|
|
45
|
+
- **`createFullscreen`** — SolidJS reactive primitive for toggling and observing Fullscreen API state.
|
|
46
|
+
- **`createAudio` / `createVideo`** — SolidJS reactive primitives for media playback, volume, duration, and control tracking.
|
|
47
|
+
- **`createOrientation`** — SolidJS reactive primitive for observing device screen orientation and angle.
|
|
48
|
+
- **`createUndoRedo`** — SolidJS reactive primitive for managing state history, undo/redo stacks, and reset capabilities.
|
|
49
|
+
- **`createFetch`** — SolidJS reactive primitive for reactive HTTP data fetching, loading states, and error handling.
|
|
50
|
+
- **`createGeolocation`** — SolidJS reactive primitive for tracking device GPS coordinates and location errors.
|
|
51
|
+
- **`createPermission`** — SolidJS reactive primitive for querying and observing browser permission status changes.
|
|
52
|
+
- **`createBattery`** — SolidJS reactive primitive for observing device battery charge level and charging status.
|
|
53
|
+
- **`createWebNotification`** — SolidJS reactive primitive for sending browser desktop notifications and managing permissions.
|
|
54
|
+
- **`createWebSocket`** — SolidJS reactive primitive for WebSocket client connections, auto-reconnections, and message passing.
|
|
55
|
+
- **`createDocumentTitle`** — SolidJS reactive primitive for managing document title dynamically with unmount restoration.
|
|
56
|
+
- **`createFavicon`** — SolidJS reactive primitive for dynamically updating browser favicon link element.
|
|
57
|
+
- **`createEventSource`** — SolidJS reactive primitive for subscribing to Server-Sent Events (SSE) streams.
|
|
44
58
|
- **Native SolidJS Reactivity** — Zero-dependency, fine-grained reactivity built directly on SolidJS signals.
|
|
45
59
|
- **TypeScript First** — Fully typed options, getters, and return tuple interfaces.
|
|
46
60
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
import { createControllableSignal } from "./create-controllable-signal";
|
|
3
|
+
|
|
4
|
+
export interface CreateAudioOptions {
|
|
5
|
+
/** Controlled playing state. */
|
|
6
|
+
playing?: boolean | Accessor<boolean | undefined>;
|
|
7
|
+
/** Uncontrolled default playing state. Defaults to false. */
|
|
8
|
+
defaultPlaying?: boolean;
|
|
9
|
+
/** Controlled muted state. */
|
|
10
|
+
muted?: boolean | Accessor<boolean | undefined>;
|
|
11
|
+
/** Uncontrolled default muted state. Defaults to false. */
|
|
12
|
+
defaultMuted?: boolean;
|
|
13
|
+
/** Initial volume level between 0.0 and 1.0. Defaults to 1.0. */
|
|
14
|
+
volume?: number;
|
|
15
|
+
/** Whether the audio should loop upon finishing. Defaults to false. */
|
|
16
|
+
loop?: boolean;
|
|
17
|
+
/** Whether the audio should start playing automatically. Defaults to false. */
|
|
18
|
+
autoplay?: boolean;
|
|
19
|
+
/** Playback speed rate. Defaults to 1.0. */
|
|
20
|
+
playbackRate?: number;
|
|
21
|
+
/** Callback fired when playing state changes. */
|
|
22
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
23
|
+
/** Callback fired when muted state changes. */
|
|
24
|
+
onMutedChange?: (muted: boolean) => void;
|
|
25
|
+
/** Callback fired when audio finishes playing. */
|
|
26
|
+
onEnded?: () => void;
|
|
27
|
+
/** Callback fired when audio playback encounters an error. */
|
|
28
|
+
onError?: (err: Event) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CreateAudioReturn {
|
|
32
|
+
/** Signal indicating if audio is currently playing. */
|
|
33
|
+
isPlaying: Accessor<boolean>;
|
|
34
|
+
/** Signal indicating current playback time in seconds. */
|
|
35
|
+
currentTime: Accessor<number>;
|
|
36
|
+
/** Signal indicating total duration in seconds. */
|
|
37
|
+
duration: Accessor<number>;
|
|
38
|
+
/** Signal indicating volume level (0.0 to 1.0). */
|
|
39
|
+
volume: Accessor<number>;
|
|
40
|
+
/** Signal indicating if audio is muted. */
|
|
41
|
+
isMuted: Accessor<boolean>;
|
|
42
|
+
/** Signal indicating if audio source is loaded and ready. */
|
|
43
|
+
isReady: Accessor<boolean>;
|
|
44
|
+
/** Play audio. */
|
|
45
|
+
play: () => Promise<void>;
|
|
46
|
+
/** Pause audio. */
|
|
47
|
+
pause: () => void;
|
|
48
|
+
/** Toggle play/pause state. */
|
|
49
|
+
toggle: () => void;
|
|
50
|
+
/** Seek to specified time in seconds. */
|
|
51
|
+
seek: (time: number) => void;
|
|
52
|
+
/** Set volume level between 0.0 and 1.0. */
|
|
53
|
+
setVolume: (vol: number) => void;
|
|
54
|
+
/** Toggle audio mute status. */
|
|
55
|
+
toggleMute: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* SolidJS reactive primitive for controlling audio playback state with controlled/uncontrolled signal support.
|
|
60
|
+
*/
|
|
61
|
+
export function createAudio(
|
|
62
|
+
src: string | Accessor<string>,
|
|
63
|
+
options: CreateAudioOptions = {}
|
|
64
|
+
): CreateAudioReturn {
|
|
65
|
+
const playingAccessor = (): boolean | undefined => {
|
|
66
|
+
if (typeof options.playing === "function") {
|
|
67
|
+
return options.playing();
|
|
68
|
+
}
|
|
69
|
+
return options.playing;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mutedAccessor = (): boolean | undefined => {
|
|
73
|
+
if (typeof options.muted === "function") {
|
|
74
|
+
return options.muted();
|
|
75
|
+
}
|
|
76
|
+
return options.muted;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const [isPlayingVal, setIsPlayingVal] = createControllableSignal<boolean>({
|
|
80
|
+
value: playingAccessor,
|
|
81
|
+
defaultValue: options.defaultPlaying ?? false,
|
|
82
|
+
onChange: options.onPlayingChange,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const [isMutedVal, setIsMutedVal] = createControllableSignal<boolean>({
|
|
86
|
+
value: mutedAccessor,
|
|
87
|
+
defaultValue: options.defaultMuted ?? false,
|
|
88
|
+
onChange: options.onMutedChange,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const [audioEl, setAudioEl] = createSignal<HTMLAudioElement | null>(null);
|
|
92
|
+
const [currentTime, setCurrentTime] = createSignal(0);
|
|
93
|
+
const [duration, setDuration] = createSignal(0);
|
|
94
|
+
const [volume, setVolumeSignal] = createSignal(options.volume ?? 1.0);
|
|
95
|
+
const [isReady, setIsReady] = createSignal(false);
|
|
96
|
+
|
|
97
|
+
const isPlaying = (): boolean => Boolean(isPlayingVal());
|
|
98
|
+
const isMuted = (): boolean => Boolean(isMutedVal());
|
|
99
|
+
|
|
100
|
+
const getSrc = (): string => {
|
|
101
|
+
return typeof src === "function" ? src() : src;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/* Initialize Audio instance */
|
|
105
|
+
createEffect(() => {
|
|
106
|
+
if (typeof window === "undefined") return;
|
|
107
|
+
|
|
108
|
+
const audio = new Audio(getSrc());
|
|
109
|
+
setAudioEl(audio);
|
|
110
|
+
|
|
111
|
+
const handleLoadedMetadata = (): void => {
|
|
112
|
+
setDuration(audio.duration || 0);
|
|
113
|
+
setIsReady(true);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleTimeUpdate = (): void => {
|
|
117
|
+
setCurrentTime(audio.currentTime || 0);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handlePlay = (): void => setIsPlayingVal(true);
|
|
121
|
+
const handlePause = (): void => setIsPlayingVal(false);
|
|
122
|
+
const handleEnded = (): void => {
|
|
123
|
+
setIsPlayingVal(false);
|
|
124
|
+
options.onEnded?.();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleError = (e: Event): void => {
|
|
128
|
+
setIsReady(false);
|
|
129
|
+
options.onError?.(e);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
133
|
+
audio.addEventListener("timeupdate", handleTimeUpdate);
|
|
134
|
+
audio.addEventListener("play", handlePlay);
|
|
135
|
+
audio.addEventListener("pause", handlePause);
|
|
136
|
+
audio.addEventListener("ended", handleEnded);
|
|
137
|
+
audio.addEventListener("error", handleError);
|
|
138
|
+
|
|
139
|
+
onCleanup(() => {
|
|
140
|
+
audio.pause();
|
|
141
|
+
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
142
|
+
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
|
143
|
+
audio.removeEventListener("play", handlePlay);
|
|
144
|
+
audio.removeEventListener("pause", handlePause);
|
|
145
|
+
audio.removeEventListener("ended", handleEnded);
|
|
146
|
+
audio.removeEventListener("error", handleError);
|
|
147
|
+
setAudioEl(null);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/* Sync properties with audio element */
|
|
152
|
+
createEffect(() => {
|
|
153
|
+
const audio = audioEl();
|
|
154
|
+
if (!audio) return;
|
|
155
|
+
|
|
156
|
+
audio.volume = volume();
|
|
157
|
+
audio.muted = isMuted();
|
|
158
|
+
audio.loop = options.loop ?? false;
|
|
159
|
+
audio.autoplay = options.autoplay ?? false;
|
|
160
|
+
if (options.playbackRate) {
|
|
161
|
+
audio.playbackRate = options.playbackRate;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/* Sync controlled playing state with HTMLAudioElement */
|
|
166
|
+
createEffect(() => {
|
|
167
|
+
const audio = audioEl();
|
|
168
|
+
const shouldPlay = isPlaying();
|
|
169
|
+
if (!audio) return;
|
|
170
|
+
if (shouldPlay && audio.paused) {
|
|
171
|
+
audio.play().catch(() => setIsPlayingVal(false));
|
|
172
|
+
} else if (!shouldPlay && !audio.paused) {
|
|
173
|
+
audio.pause();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const play = async (): Promise<void> => {
|
|
178
|
+
setIsPlayingVal(true);
|
|
179
|
+
const audio = audioEl();
|
|
180
|
+
if (audio) await audio.play();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const pause = (): void => {
|
|
184
|
+
setIsPlayingVal(false);
|
|
185
|
+
const audio = audioEl();
|
|
186
|
+
if (audio) audio.pause();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const toggle = (): void => {
|
|
190
|
+
if (isPlaying()) {
|
|
191
|
+
pause();
|
|
192
|
+
} else {
|
|
193
|
+
play();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const seek = (time: number): void => {
|
|
198
|
+
const audio = audioEl();
|
|
199
|
+
if (audio) {
|
|
200
|
+
audio.currentTime = Math.max(0, Math.min(time, duration()));
|
|
201
|
+
setCurrentTime(audio.currentTime);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const setVolume = (vol: number): void => {
|
|
206
|
+
const clamped = Math.max(0, Math.min(vol, 1.0));
|
|
207
|
+
setVolumeSignal(clamped);
|
|
208
|
+
const audio = audioEl();
|
|
209
|
+
if (audio) {
|
|
210
|
+
audio.volume = clamped;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const toggleMute = (): void => {
|
|
215
|
+
const nextMuted = !isMuted();
|
|
216
|
+
setIsMutedVal(nextMuted);
|
|
217
|
+
const audio = audioEl();
|
|
218
|
+
if (audio) {
|
|
219
|
+
audio.muted = nextMuted;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
isPlaying,
|
|
225
|
+
currentTime,
|
|
226
|
+
duration,
|
|
227
|
+
volume,
|
|
228
|
+
isMuted,
|
|
229
|
+
isReady,
|
|
230
|
+
play,
|
|
231
|
+
pause,
|
|
232
|
+
toggle,
|
|
233
|
+
seek,
|
|
234
|
+
setVolume,
|
|
235
|
+
toggleMute,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface CreateVideoOptions {
|
|
240
|
+
/** Controlled playing state. */
|
|
241
|
+
playing?: boolean | Accessor<boolean | undefined>;
|
|
242
|
+
/** Uncontrolled default playing state. Defaults to false. */
|
|
243
|
+
defaultPlaying?: boolean;
|
|
244
|
+
/** Controlled muted state. */
|
|
245
|
+
muted?: boolean | Accessor<boolean | undefined>;
|
|
246
|
+
/** Uncontrolled default muted state. Defaults to false. */
|
|
247
|
+
defaultMuted?: boolean;
|
|
248
|
+
/** Initial volume level between 0.0 and 1.0. Defaults to 1.0. */
|
|
249
|
+
volume?: number;
|
|
250
|
+
/** Whether video should loop upon finishing. Defaults to false. */
|
|
251
|
+
loop?: boolean;
|
|
252
|
+
/** Callback fired when playing state changes. */
|
|
253
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
254
|
+
/** Callback fired when muted state changes. */
|
|
255
|
+
onMutedChange?: (muted: boolean) => void;
|
|
256
|
+
/** Callback fired when video playback finishes. */
|
|
257
|
+
onEnded?: () => void;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface CreateVideoReturn {
|
|
261
|
+
/** Ref callback function to bind to HTMLVideoElement. */
|
|
262
|
+
setVideoRef: (el: HTMLVideoElement | null) => void;
|
|
263
|
+
/** Signal indicating if video is playing. */
|
|
264
|
+
isPlaying: Accessor<boolean>;
|
|
265
|
+
/** Signal indicating current playback time in seconds. */
|
|
266
|
+
currentTime: Accessor<number>;
|
|
267
|
+
/** Signal indicating total video duration in seconds. */
|
|
268
|
+
duration: Accessor<number>;
|
|
269
|
+
/** Signal indicating volume level (0.0 to 1.0). */
|
|
270
|
+
volume: Accessor<number>;
|
|
271
|
+
/** Signal indicating if video is muted. */
|
|
272
|
+
isMuted: Accessor<boolean>;
|
|
273
|
+
/** Play video. */
|
|
274
|
+
play: () => Promise<void>;
|
|
275
|
+
/** Pause video. */
|
|
276
|
+
pause: () => void;
|
|
277
|
+
/** Toggle play/pause. */
|
|
278
|
+
toggle: () => void;
|
|
279
|
+
/** Seek to time position. */
|
|
280
|
+
seek: (time: number) => void;
|
|
281
|
+
/** Set volume level. */
|
|
282
|
+
setVolume: (vol: number) => void;
|
|
283
|
+
/** Toggle mute status. */
|
|
284
|
+
toggleMute: () => void;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* SolidJS reactive primitive for controlling media element video playback with controlled/uncontrolled signal support.
|
|
289
|
+
*/
|
|
290
|
+
export function createVideo(
|
|
291
|
+
options: CreateVideoOptions = {}
|
|
292
|
+
): CreateVideoReturn {
|
|
293
|
+
const [videoEl, setVideoEl] = createSignal<HTMLVideoElement | null>(null);
|
|
294
|
+
|
|
295
|
+
const playingAccessor = (): boolean | undefined => {
|
|
296
|
+
if (typeof options.playing === "function") {
|
|
297
|
+
return options.playing();
|
|
298
|
+
}
|
|
299
|
+
return options.playing;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const mutedAccessor = (): boolean | undefined => {
|
|
303
|
+
if (typeof options.muted === "function") {
|
|
304
|
+
return options.muted();
|
|
305
|
+
}
|
|
306
|
+
return options.muted;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const [isPlayingVal, setIsPlayingVal] = createControllableSignal<boolean>({
|
|
310
|
+
value: playingAccessor,
|
|
311
|
+
defaultValue: options.defaultPlaying ?? false,
|
|
312
|
+
onChange: options.onPlayingChange,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const [isMutedVal, setIsMutedVal] = createControllableSignal<boolean>({
|
|
316
|
+
value: mutedAccessor,
|
|
317
|
+
defaultValue: options.defaultMuted ?? false,
|
|
318
|
+
onChange: options.onMutedChange,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const [currentTime, setCurrentTime] = createSignal(0);
|
|
322
|
+
const [duration, setDuration] = createSignal(0);
|
|
323
|
+
const [volume, setVolumeSignal] = createSignal(options.volume ?? 1.0);
|
|
324
|
+
|
|
325
|
+
const isPlaying = (): boolean => Boolean(isPlayingVal());
|
|
326
|
+
const isMuted = (): boolean => Boolean(isMutedVal());
|
|
327
|
+
|
|
328
|
+
createEffect(() => {
|
|
329
|
+
const el = videoEl();
|
|
330
|
+
if (!el) return;
|
|
331
|
+
|
|
332
|
+
el.volume = volume();
|
|
333
|
+
el.muted = isMuted();
|
|
334
|
+
el.loop = options.loop ?? false;
|
|
335
|
+
|
|
336
|
+
const handleLoadedMetadata = (): void => {
|
|
337
|
+
setDuration(el.duration || 0);
|
|
338
|
+
};
|
|
339
|
+
const handleTimeUpdate = (): void => {
|
|
340
|
+
setCurrentTime(el.currentTime || 0);
|
|
341
|
+
};
|
|
342
|
+
const handlePlay = (): void => setIsPlayingVal(true);
|
|
343
|
+
const handlePause = (): void => setIsPlayingVal(false);
|
|
344
|
+
const handleEnded = (): void => {
|
|
345
|
+
setIsPlayingVal(false);
|
|
346
|
+
options.onEnded?.();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
el.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
350
|
+
el.addEventListener("timeupdate", handleTimeUpdate);
|
|
351
|
+
el.addEventListener("play", handlePlay);
|
|
352
|
+
el.addEventListener("pause", handlePause);
|
|
353
|
+
el.addEventListener("ended", handleEnded);
|
|
354
|
+
|
|
355
|
+
onCleanup(() => {
|
|
356
|
+
el.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
357
|
+
el.removeEventListener("timeupdate", handleTimeUpdate);
|
|
358
|
+
el.removeEventListener("play", handlePlay);
|
|
359
|
+
el.removeEventListener("pause", handlePause);
|
|
360
|
+
el.removeEventListener("ended", handleEnded);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
/* Sync controlled playing state with video HTML element */
|
|
365
|
+
createEffect(() => {
|
|
366
|
+
const el = videoEl();
|
|
367
|
+
const shouldPlay = isPlaying();
|
|
368
|
+
if (!el) return;
|
|
369
|
+
if (shouldPlay && el.paused) {
|
|
370
|
+
el.play().catch(() => setIsPlayingVal(false));
|
|
371
|
+
} else if (!shouldPlay && !el.paused) {
|
|
372
|
+
el.pause();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/* Sync controlled muted state with video HTML element */
|
|
377
|
+
createEffect(() => {
|
|
378
|
+
const el = videoEl();
|
|
379
|
+
if (el) {
|
|
380
|
+
el.muted = isMuted();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const play = async (): Promise<void> => {
|
|
385
|
+
setIsPlayingVal(true);
|
|
386
|
+
const el = videoEl();
|
|
387
|
+
if (el) await el.play();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const pause = (): void => {
|
|
391
|
+
setIsPlayingVal(false);
|
|
392
|
+
const el = videoEl();
|
|
393
|
+
if (el) el.pause();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const toggle = (): void => {
|
|
397
|
+
if (isPlaying()) {
|
|
398
|
+
pause();
|
|
399
|
+
} else {
|
|
400
|
+
play();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const seek = (time: number): void => {
|
|
405
|
+
const el = videoEl();
|
|
406
|
+
if (el) {
|
|
407
|
+
el.currentTime = Math.max(0, Math.min(time, duration()));
|
|
408
|
+
setCurrentTime(el.currentTime);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const setVolume = (vol: number): void => {
|
|
413
|
+
const clamped = Math.max(0, Math.min(vol, 1.0));
|
|
414
|
+
setVolumeSignal(clamped);
|
|
415
|
+
const el = videoEl();
|
|
416
|
+
if (el) {
|
|
417
|
+
el.volume = clamped;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const toggleMute = (): void => {
|
|
422
|
+
const nextMuted = !isMuted();
|
|
423
|
+
setIsMutedVal(nextMuted);
|
|
424
|
+
const el = videoEl();
|
|
425
|
+
if (el) {
|
|
426
|
+
el.muted = nextMuted;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
setVideoRef: setVideoEl,
|
|
432
|
+
isPlaying,
|
|
433
|
+
currentTime,
|
|
434
|
+
duration,
|
|
435
|
+
volume,
|
|
436
|
+
isMuted,
|
|
437
|
+
play,
|
|
438
|
+
pause,
|
|
439
|
+
toggle,
|
|
440
|
+
seek,
|
|
441
|
+
setVolume,
|
|
442
|
+
toggleMute,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface BatteryState {
|
|
4
|
+
/** Battery charge level ratio from 0.0 (empty) to 1.0 (full). */
|
|
5
|
+
level: number;
|
|
6
|
+
/** Whether the device battery is currently charging. */
|
|
7
|
+
charging: boolean;
|
|
8
|
+
/** Seconds remaining until fully charged (0 if already full or unknown). */
|
|
9
|
+
chargingTime: number;
|
|
10
|
+
/** Seconds remaining until fully discharged. */
|
|
11
|
+
dischargingTime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateBatteryReturn {
|
|
15
|
+
/** Signal accessor containing battery state metrics. */
|
|
16
|
+
battery: Accessor<BatteryState>;
|
|
17
|
+
/** Signal accessor indicating whether Battery Status API is supported in browser environment. */
|
|
18
|
+
isSupported: Accessor<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const initialBatteryState: BatteryState = {
|
|
22
|
+
level: 1,
|
|
23
|
+
charging: true,
|
|
24
|
+
chargingTime: 0,
|
|
25
|
+
dischargingTime: Infinity,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* SolidJS reactive primitive for observing device battery status, charge level, and charging metrics.
|
|
30
|
+
*/
|
|
31
|
+
export function createBattery(): CreateBatteryReturn {
|
|
32
|
+
const [battery, setBattery] = createSignal<BatteryState>(initialBatteryState);
|
|
33
|
+
|
|
34
|
+
const isSupported = (): boolean =>
|
|
35
|
+
typeof window !== "undefined" &&
|
|
36
|
+
typeof navigator !== "undefined" &&
|
|
37
|
+
"getBattery" in navigator;
|
|
38
|
+
|
|
39
|
+
let batteryManager: any = null;
|
|
40
|
+
|
|
41
|
+
const updateBatteryStatus = (): void => {
|
|
42
|
+
if (!batteryManager) return;
|
|
43
|
+
setBattery({
|
|
44
|
+
level: batteryManager.level ?? 1,
|
|
45
|
+
charging: Boolean(batteryManager.charging),
|
|
46
|
+
chargingTime: batteryManager.chargingTime ?? 0,
|
|
47
|
+
dischargingTime: batteryManager.dischargingTime ?? Infinity,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
createEffect(() => {
|
|
52
|
+
if (!isSupported()) return;
|
|
53
|
+
|
|
54
|
+
(navigator as any).getBattery().then((manager: any) => {
|
|
55
|
+
batteryManager = manager;
|
|
56
|
+
updateBatteryStatus();
|
|
57
|
+
|
|
58
|
+
manager.addEventListener("levelchange", updateBatteryStatus);
|
|
59
|
+
manager.addEventListener("chargingchange", updateBatteryStatus);
|
|
60
|
+
manager.addEventListener("chargingtimechange", updateBatteryStatus);
|
|
61
|
+
manager.addEventListener("dischargingtimechange", updateBatteryStatus);
|
|
62
|
+
}).catch(() => {});
|
|
63
|
+
|
|
64
|
+
onCleanup(() => {
|
|
65
|
+
if (batteryManager) {
|
|
66
|
+
batteryManager.removeEventListener("levelchange", updateBatteryStatus);
|
|
67
|
+
batteryManager.removeEventListener("chargingchange", updateBatteryStatus);
|
|
68
|
+
batteryManager.removeEventListener("chargingtimechange", updateBatteryStatus);
|
|
69
|
+
batteryManager.removeEventListener("dischargingtimechange", updateBatteryStatus);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
battery,
|
|
76
|
+
isSupported,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateDocumentTitleOptions {
|
|
4
|
+
/** Whether to restore original title on component unmount. Defaults to true. */
|
|
5
|
+
restoreOnUnmount?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SolidJS reactive primitive for managing document title dynamically.
|
|
10
|
+
*/
|
|
11
|
+
export function createDocumentTitle(
|
|
12
|
+
title: string | Accessor<string>,
|
|
13
|
+
options: CreateDocumentTitleOptions = {}
|
|
14
|
+
): void {
|
|
15
|
+
const getTitle = (): string => (typeof title === "function" ? title() : title);
|
|
16
|
+
|
|
17
|
+
createEffect(() => {
|
|
18
|
+
if (typeof document === "undefined") return;
|
|
19
|
+
|
|
20
|
+
const originalTitle = document.title;
|
|
21
|
+
const newTitle = getTitle();
|
|
22
|
+
|
|
23
|
+
if (newTitle) {
|
|
24
|
+
document.title = newTitle;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onCleanup(() => {
|
|
28
|
+
if (typeof document !== "undefined" && (options.restoreOnUnmount ?? true)) {
|
|
29
|
+
document.title = originalTitle;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|