@vibehooks/react 0.0.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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +55 -0
- package/dist/useAsyncState.d.ts +52 -0
- package/dist/useAsyncState.js +173 -0
- package/dist/useAudio.d.ts +26 -0
- package/dist/useAudio.js +64 -0
- package/dist/useAutoScroll.d.ts +47 -0
- package/dist/useAutoScroll.js +122 -0
- package/dist/useBarcode.d.ts +77 -0
- package/dist/useBarcode.js +140 -0
- package/dist/useBatteryStatus.d.ts +53 -0
- package/dist/useBatteryStatus.js +67 -0
- package/dist/useBodyScrollFreeze.d.ts +36 -0
- package/dist/useBodyScrollFreeze.js +74 -0
- package/dist/useCameraCapture.d.ts +76 -0
- package/dist/useCameraCapture.js +116 -0
- package/dist/useCookies.d.ts +42 -0
- package/dist/useCookies.js +61 -0
- package/dist/useCopyToClipboard.d.ts +22 -0
- package/dist/useCopyToClipboard.js +31 -0
- package/dist/useCountDown.d.ts +80 -0
- package/dist/useCountDown.js +106 -0
- package/dist/useDebouncedState.d.ts +47 -0
- package/dist/useDebouncedState.js +47 -0
- package/dist/useExternalNotifications.d.ts +36 -0
- package/dist/useExternalNotifications.js +100 -0
- package/dist/useFile.d.ts +74 -0
- package/dist/useFile.js +74 -0
- package/dist/useFullScreen.d.ts +20 -0
- package/dist/useFullScreen.js +43 -0
- package/dist/useGeolocation.d.ts +47 -0
- package/dist/useGeolocation.js +68 -0
- package/dist/useHoverIntent.d.ts +45 -0
- package/dist/useHoverIntent.js +81 -0
- package/dist/useIdle.d.ts +47 -0
- package/dist/useIdle.js +59 -0
- package/dist/useIndexedDB.d.ts +60 -0
- package/dist/useIndexedDB.js +75 -0
- package/dist/useIntersectionObserver.d.ts +45 -0
- package/dist/useIntersectionObserver.js +70 -0
- package/dist/useIntervalSafe.d.ts +72 -0
- package/dist/useIntervalSafe.js +85 -0
- package/dist/useIsClient.d.ts +12 -0
- package/dist/useIsClient.js +21 -0
- package/dist/useIsDesktop.d.ts +12 -0
- package/dist/useIsDesktop.js +23 -0
- package/dist/useIsFirstRender.d.ts +12 -0
- package/dist/useIsFirstRender.js +21 -0
- package/dist/useList.d.ts +19 -0
- package/dist/useList.js +44 -0
- package/dist/useLocalNotifications.d.ts +23 -0
- package/dist/useLocalNotifications.js +50 -0
- package/dist/useLocalStorage.d.ts +45 -0
- package/dist/useLocalStorage.js +71 -0
- package/dist/useNetworkInformation.d.ts +138 -0
- package/dist/useNetworkInformation.js +76 -0
- package/dist/useOnline.d.ts +17 -0
- package/dist/useOnline.js +29 -0
- package/dist/usePageVisibility.d.ts +32 -0
- package/dist/usePageVisibility.js +65 -0
- package/dist/usePermissions.d.ts +28 -0
- package/dist/usePermissions.js +70 -0
- package/dist/usePictureInPicture.d.ts +47 -0
- package/dist/usePictureInPicture.js +60 -0
- package/dist/usePopover.d.ts +54 -0
- package/dist/usePopover.js +67 -0
- package/dist/usePreferredLanguage.d.ts +55 -0
- package/dist/usePreferredLanguage.js +127 -0
- package/dist/usePreferredTheme.d.ts +67 -0
- package/dist/usePreferredTheme.js +133 -0
- package/dist/usePreviousDistinct.d.ts +12 -0
- package/dist/usePreviousDistinct.js +23 -0
- package/dist/useResettableState.d.ts +15 -0
- package/dist/useResettableState.js +25 -0
- package/dist/useScreenOrientation.d.ts +48 -0
- package/dist/useScreenOrientation.js +51 -0
- package/dist/useScreenSize.d.ts +16 -0
- package/dist/useScreenSize.js +34 -0
- package/dist/useScreenWakeLock.d.ts +37 -0
- package/dist/useScreenWakeLock.js +48 -0
- package/dist/useServerSentEvent.d.ts +57 -0
- package/dist/useServerSentEvent.js +78 -0
- package/dist/useShoppingCart.d.ts +54 -0
- package/dist/useShoppingCart.js +122 -0
- package/dist/useSmartVideo.d.ts +35 -0
- package/dist/useSmartVideo.js +76 -0
- package/dist/useSpeech.d.ts +74 -0
- package/dist/useSpeech.js +156 -0
- package/dist/useSummarizer.d.ts +92 -0
- package/dist/useSummarizer.js +83 -0
- package/dist/useTaskQueue.d.ts +25 -0
- package/dist/useTaskQueue.js +51 -0
- package/dist/useThrottledCallback.d.ts +32 -0
- package/dist/useThrottledCallback.js +42 -0
- package/dist/useTimeout.d.ts +58 -0
- package/dist/useTimeout.js +70 -0
- package/dist/useToggle.d.ts +30 -0
- package/dist/useToggle.js +23 -0
- package/dist/useTraceUpdates.d.ts +22 -0
- package/dist/useTraceUpdates.js +38 -0
- package/dist/useTranslator.d.ts +110 -0
- package/dist/useTranslator.js +119 -0
- package/dist/useUserActivation.d.ts +40 -0
- package/dist/useUserActivation.js +63 -0
- package/dist/useVibration.d.ts +55 -0
- package/dist/useVibration.js +50 -0
- package/dist/useWebsocket.d.ts +80 -0
- package/dist/useWebsocket.js +125 -0
- package/package.json +70 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/useServerSentEvent.d.ts
|
|
2
|
+
interface UseServerSentEventOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the connection should be established.
|
|
5
|
+
* Usefull to lazily connect.
|
|
6
|
+
*/
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Called when an error occurs.
|
|
10
|
+
*/
|
|
11
|
+
onError?: (event: Event) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Called on every incoming message event.
|
|
14
|
+
*/
|
|
15
|
+
onMessage?: (event: MessageEvent<string>) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Called then the connection is opened.
|
|
18
|
+
*/
|
|
19
|
+
onOpen?: (event: Event) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Whether credentials (cookies) should be sent.
|
|
22
|
+
*/
|
|
23
|
+
withCredentials?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface UseServerSentEventReturn {
|
|
26
|
+
/**
|
|
27
|
+
* Manually closes the connection.
|
|
28
|
+
*/
|
|
29
|
+
close: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Current connection state.
|
|
32
|
+
*/
|
|
33
|
+
readyState: SSEReadyState | null;
|
|
34
|
+
}
|
|
35
|
+
type SSEReadyState = 0 | 1 | 2;
|
|
36
|
+
/**
|
|
37
|
+
* 0 = CONNECTING
|
|
38
|
+
* 1 = OPEN
|
|
39
|
+
* 2 = CLOSED
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* `useServerSideEvent` provides unopinionated access to Server-Sent Event (SSE) via the EventSource Web API.
|
|
43
|
+
* It manages the EventSource lifecycle but delegates data handling to consumer callbacks.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* useServerSideEvent('/api/events', {
|
|
48
|
+
* onMessage: (e) => {
|
|
49
|
+
* const data = JSON.parse(e.data);
|
|
50
|
+
* console.log(data);
|
|
51
|
+
* }
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function useServerSentEvent(url: string, options?: UseServerSentEventOptions): UseServerSentEventReturn;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { useServerSentEvent };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useServerSentEvent.ts
|
|
4
|
+
/**
|
|
5
|
+
* 0 = CONNECTING
|
|
6
|
+
* 1 = OPEN
|
|
7
|
+
* 2 = CLOSED
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* `useServerSideEvent` provides unopinionated access to Server-Sent Event (SSE) via the EventSource Web API.
|
|
11
|
+
* It manages the EventSource lifecycle but delegates data handling to consumer callbacks.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* useServerSideEvent('/api/events', {
|
|
16
|
+
* onMessage: (e) => {
|
|
17
|
+
* const data = JSON.parse(e.data);
|
|
18
|
+
* console.log(data);
|
|
19
|
+
* }
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function useServerSentEvent(url, options = {}) {
|
|
24
|
+
const { enabled = true, onError, onMessage, onOpen, withCredentials = false } = options;
|
|
25
|
+
const sourceRef = React.useRef(null);
|
|
26
|
+
const [readyState, setReadyState] = React.useState(null);
|
|
27
|
+
const onMessageRef = React.useRef(onMessage);
|
|
28
|
+
const onOpenRef = React.useRef(onOpen);
|
|
29
|
+
const onErrorRef = React.useRef(onError);
|
|
30
|
+
const isSupported = typeof window !== "undefined" && typeof EventSource !== "undefined";
|
|
31
|
+
const close = React.useCallback(() => {
|
|
32
|
+
sourceRef.current?.close();
|
|
33
|
+
sourceRef.current = null;
|
|
34
|
+
setReadyState(null);
|
|
35
|
+
}, []);
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
onMessageRef.current = onMessage;
|
|
38
|
+
onOpenRef.current = onOpen;
|
|
39
|
+
onErrorRef.current = onError;
|
|
40
|
+
}, [
|
|
41
|
+
onMessageRef,
|
|
42
|
+
onOpenRef,
|
|
43
|
+
onErrorRef
|
|
44
|
+
]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (!isSupported || !enabled) return;
|
|
47
|
+
const source = new EventSource(url, { withCredentials });
|
|
48
|
+
sourceRef.current = source;
|
|
49
|
+
source.onopen = (event) => {
|
|
50
|
+
setReadyState(source.readyState);
|
|
51
|
+
onOpenRef.current?.(event);
|
|
52
|
+
};
|
|
53
|
+
source.onmessage = (event) => {
|
|
54
|
+
setReadyState(source.readyState);
|
|
55
|
+
onMessageRef.current?.(event);
|
|
56
|
+
};
|
|
57
|
+
source.onerror = (event) => {
|
|
58
|
+
setReadyState(source.readyState);
|
|
59
|
+
onErrorRef.current?.(event);
|
|
60
|
+
};
|
|
61
|
+
return () => {
|
|
62
|
+
source.close();
|
|
63
|
+
sourceRef.current = null;
|
|
64
|
+
};
|
|
65
|
+
}, [
|
|
66
|
+
url,
|
|
67
|
+
enabled,
|
|
68
|
+
withCredentials,
|
|
69
|
+
isSupported
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
close,
|
|
73
|
+
readyState
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { useServerSentEvent };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/useShoppingCart.d.ts
|
|
2
|
+
interface UseShoppingCartOptions<T extends object> {
|
|
3
|
+
getItemDiscount?: (item: T) => number;
|
|
4
|
+
getItemKey: (item: T) => string | number;
|
|
5
|
+
getItemPrice: (item: T) => number;
|
|
6
|
+
getItemQuantity: (item: T) => number;
|
|
7
|
+
getItemTax?: (item: T) => number;
|
|
8
|
+
}
|
|
9
|
+
interface ShoppingCartItemDetail {
|
|
10
|
+
discount: number;
|
|
11
|
+
key: string | number;
|
|
12
|
+
quantity: number;
|
|
13
|
+
subtotal: number;
|
|
14
|
+
tax: number;
|
|
15
|
+
total: number;
|
|
16
|
+
unitPrice: number;
|
|
17
|
+
}
|
|
18
|
+
interface UseShoppingCartReturn<T extends object> {
|
|
19
|
+
addItem: (item: T) => void;
|
|
20
|
+
clear: () => void;
|
|
21
|
+
getDetails: () => ShoppingCartItemDetail[];
|
|
22
|
+
getItemCount: () => number;
|
|
23
|
+
getSubtotal: () => number;
|
|
24
|
+
getTotal: () => number;
|
|
25
|
+
getTotalDiscount: () => number;
|
|
26
|
+
getTotalQuantity: () => number;
|
|
27
|
+
getTotalTax: () => number;
|
|
28
|
+
items: T[];
|
|
29
|
+
removeItem: (id: number | string) => void;
|
|
30
|
+
updateItem: (id: number | string, patch: Partial<T>) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `useShoppingCart` is a fully unopinionated React hook for managing shopping cart state.
|
|
34
|
+
* The hook does not assume any data structure; instead, it relies on user-provided extractor functions to derive semantic meaning (identity, price, quantity, taxes, discounts).
|
|
35
|
+
* This design allows the hook to adapt to any domain model while still providing a complete and ergonomic shopping cart API.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const cart = useShoppingCart<Product>({
|
|
40
|
+
* getItemKey: p => p.id,
|
|
41
|
+
* getItemPrice: p => p.price,
|
|
42
|
+
* getItemQuantity: p => p.quantity,
|
|
43
|
+
* getItemTax: p => p.tax ?? 0,
|
|
44
|
+
* getItemDiscount: p => p.discount ?? 0,
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* cart.addItem(product);
|
|
48
|
+
* cart.getTotal();
|
|
49
|
+
* cart.getDetails();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function useShoppingCart<T extends object>(options: UseShoppingCartOptions<T>): UseShoppingCartReturn<T>;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { useShoppingCart };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useShoppingCart.ts
|
|
4
|
+
/**
|
|
5
|
+
* `useShoppingCart` is a fully unopinionated React hook for managing shopping cart state.
|
|
6
|
+
* The hook does not assume any data structure; instead, it relies on user-provided extractor functions to derive semantic meaning (identity, price, quantity, taxes, discounts).
|
|
7
|
+
* This design allows the hook to adapt to any domain model while still providing a complete and ergonomic shopping cart API.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const cart = useShoppingCart<Product>({
|
|
12
|
+
* getItemKey: p => p.id,
|
|
13
|
+
* getItemPrice: p => p.price,
|
|
14
|
+
* getItemQuantity: p => p.quantity,
|
|
15
|
+
* getItemTax: p => p.tax ?? 0,
|
|
16
|
+
* getItemDiscount: p => p.discount ?? 0,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* cart.addItem(product);
|
|
20
|
+
* cart.getTotal();
|
|
21
|
+
* cart.getDetails();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function useShoppingCart(options) {
|
|
25
|
+
const { getItemDiscount, getItemKey, getItemPrice, getItemQuantity, getItemTax } = options;
|
|
26
|
+
const [items, setItems] = React.useState([]);
|
|
27
|
+
const addItem = React.useCallback((item) => {
|
|
28
|
+
setItems((prevItems) => [...prevItems, item]);
|
|
29
|
+
}, []);
|
|
30
|
+
const removeItem = React.useCallback((key) => {
|
|
31
|
+
setItems((prevItems) => prevItems.filter((item) => getItemKey(item) !== key));
|
|
32
|
+
}, [getItemKey]);
|
|
33
|
+
const updateItem = React.useCallback((key, patch) => {
|
|
34
|
+
setItems((prev) => prev.map((item) => getItemKey(item) === key ? {
|
|
35
|
+
...item,
|
|
36
|
+
...patch
|
|
37
|
+
} : item));
|
|
38
|
+
}, [getItemKey]);
|
|
39
|
+
const clear = React.useCallback(() => {
|
|
40
|
+
setItems([]);
|
|
41
|
+
}, []);
|
|
42
|
+
const getItemCount = React.useCallback(() => items.length, [items]);
|
|
43
|
+
const getTotalQuantity = React.useCallback(() => items.reduce((acc, item) => acc + getItemQuantity(item), 0), [items, getItemQuantity]);
|
|
44
|
+
const getSubtotal = React.useCallback(() => items.reduce((acc, item) => acc + getItemPrice(item) * getItemQuantity(item), 0), [
|
|
45
|
+
items,
|
|
46
|
+
getItemPrice,
|
|
47
|
+
getItemQuantity
|
|
48
|
+
]);
|
|
49
|
+
const getTotalTax = React.useCallback(() => getItemTax ? items.reduce((acc, item) => acc + getItemTax(item) * getItemQuantity(item), 0) : 0, [
|
|
50
|
+
items,
|
|
51
|
+
getItemTax,
|
|
52
|
+
getItemQuantity
|
|
53
|
+
]);
|
|
54
|
+
const getTotalDiscount = React.useCallback(() => getItemDiscount ? items.reduce((acc, item) => acc + getItemDiscount(item) * getItemQuantity(item), 0) : 0, [
|
|
55
|
+
items,
|
|
56
|
+
getItemDiscount,
|
|
57
|
+
getItemQuantity
|
|
58
|
+
]);
|
|
59
|
+
const getTotal = React.useCallback(() => getSubtotal() + getTotalTax() + getTotalDiscount(), [
|
|
60
|
+
getSubtotal,
|
|
61
|
+
getTotalTax,
|
|
62
|
+
getTotalDiscount
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
addItem,
|
|
66
|
+
clear,
|
|
67
|
+
getDetails: React.useCallback(() => {
|
|
68
|
+
const map = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const key = getItemKey(item);
|
|
71
|
+
const quantity = getItemQuantity(item);
|
|
72
|
+
const unitPrice = getItemPrice(item);
|
|
73
|
+
const tax = getItemTax ? getItemTax(item) : 0;
|
|
74
|
+
const discount = getItemDiscount ? getItemDiscount(item) : 0;
|
|
75
|
+
const existing = map.get(key);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
const subtotal = quantity * unitPrice;
|
|
78
|
+
const taxTotal = tax * quantity;
|
|
79
|
+
const discountTotal = discount * quantity;
|
|
80
|
+
map.set(key, {
|
|
81
|
+
discount: discountTotal,
|
|
82
|
+
key,
|
|
83
|
+
quantity,
|
|
84
|
+
subtotal,
|
|
85
|
+
tax: taxTotal,
|
|
86
|
+
total: subtotal + taxTotal - discountTotal,
|
|
87
|
+
unitPrice
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
existing.quantity += quantity;
|
|
91
|
+
const subtTotalIncrement = quantity * unitPrice;
|
|
92
|
+
const taxIncrement = tax * quantity;
|
|
93
|
+
const discountIncrement = discount * quantity;
|
|
94
|
+
existing.subtotal += subtTotalIncrement;
|
|
95
|
+
existing.tax += taxIncrement;
|
|
96
|
+
existing.discount += discountIncrement;
|
|
97
|
+
existing.total += subtTotalIncrement + taxIncrement - discountIncrement;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return Array.from(map.values());
|
|
101
|
+
}, [
|
|
102
|
+
items,
|
|
103
|
+
getItemKey,
|
|
104
|
+
getItemPrice,
|
|
105
|
+
getItemQuantity,
|
|
106
|
+
getItemTax,
|
|
107
|
+
getItemDiscount
|
|
108
|
+
]),
|
|
109
|
+
getItemCount,
|
|
110
|
+
getSubtotal,
|
|
111
|
+
getTotal,
|
|
112
|
+
getTotalDiscount,
|
|
113
|
+
getTotalQuantity,
|
|
114
|
+
getTotalTax,
|
|
115
|
+
items,
|
|
116
|
+
removeItem,
|
|
117
|
+
updateItem
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { useShoppingCart };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useSmartVideo.d.ts
|
|
4
|
+
interface SmartVideoOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Auto-play when video becomes visible.
|
|
7
|
+
*/
|
|
8
|
+
autoPlay?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Pause video when it leaves the viewport.
|
|
11
|
+
*/
|
|
12
|
+
pauseOnExit?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Reset video to time 0 when it leaves the viewport.
|
|
15
|
+
*/
|
|
16
|
+
resetOnExit?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Percentage of visibility required to start playing.
|
|
19
|
+
* (0.0 - 1.0)
|
|
20
|
+
* Default: 0.5
|
|
21
|
+
*/
|
|
22
|
+
threshold?: number;
|
|
23
|
+
}
|
|
24
|
+
interface SmartVideoReturn {
|
|
25
|
+
isPlaying: boolean;
|
|
26
|
+
isVisible: boolean;
|
|
27
|
+
pause: () => void;
|
|
28
|
+
play: () => Promise<void>;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
stop: () => void;
|
|
31
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
32
|
+
}
|
|
33
|
+
declare function useSmartVideo(options?: SmartVideoOptions): SmartVideoReturn;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { useSmartVideo };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useIntersectionObserver } from "./useIntersectionObserver.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/useSmartVideo.ts
|
|
5
|
+
function useSmartVideo(options = {}) {
|
|
6
|
+
const { autoPlay, pauseOnExit, resetOnExit, threshold } = options;
|
|
7
|
+
const videoRef = React.useRef(null);
|
|
8
|
+
const { isVisible } = useIntersectionObserver({ threshold: threshold ?? .5 }, videoRef);
|
|
9
|
+
const [isPlaying, setIsPlaying] = React.useState(false);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const video = videoRef.current;
|
|
12
|
+
if (!video) return;
|
|
13
|
+
const canPlay = canAutoPlay(video);
|
|
14
|
+
if (isVisible) {
|
|
15
|
+
if (autoPlay && canPlay && video.paused) video.play().catch(() => {});
|
|
16
|
+
} else {
|
|
17
|
+
if (pauseOnExit && !video.paused) video.pause();
|
|
18
|
+
if (resetOnExit) video.currentTime = 0;
|
|
19
|
+
}
|
|
20
|
+
}, [
|
|
21
|
+
isVisible,
|
|
22
|
+
autoPlay,
|
|
23
|
+
pauseOnExit,
|
|
24
|
+
resetOnExit
|
|
25
|
+
]);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
const video = videoRef.current;
|
|
28
|
+
if (!video) return;
|
|
29
|
+
const onPlay = () => setIsPlaying(true);
|
|
30
|
+
const onPause = () => setIsPlaying(false);
|
|
31
|
+
video.addEventListener("play", onPlay);
|
|
32
|
+
video.addEventListener("pause", onPause);
|
|
33
|
+
return () => {
|
|
34
|
+
video.removeEventListener("play", onPlay);
|
|
35
|
+
video.removeEventListener("pause", onPause);
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
const play = React.useCallback(async () => {
|
|
39
|
+
if (!videoRef.current) return;
|
|
40
|
+
await safePlay(videoRef.current);
|
|
41
|
+
}, []);
|
|
42
|
+
const pause = React.useCallback(() => {
|
|
43
|
+
videoRef.current?.pause();
|
|
44
|
+
}, []);
|
|
45
|
+
const stop = React.useCallback(() => {
|
|
46
|
+
const video = videoRef.current;
|
|
47
|
+
if (!video) return;
|
|
48
|
+
video.pause();
|
|
49
|
+
video.currentTime = 0;
|
|
50
|
+
}, []);
|
|
51
|
+
return {
|
|
52
|
+
isPlaying,
|
|
53
|
+
isVisible,
|
|
54
|
+
pause,
|
|
55
|
+
play,
|
|
56
|
+
reset: React.useCallback(() => {
|
|
57
|
+
if (videoRef.current) videoRef.current.currentTime = 0;
|
|
58
|
+
}, []),
|
|
59
|
+
stop,
|
|
60
|
+
videoRef
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function safePlay(video) {
|
|
64
|
+
try {
|
|
65
|
+
await video.play();
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function canAutoPlay(video) {
|
|
72
|
+
return video.muted && video.playsInline;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { useSmartVideo };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/useSpeech.d.ts
|
|
2
|
+
interface SpeechRecognition extends EventTarget {
|
|
3
|
+
abort(): void;
|
|
4
|
+
continuous: boolean;
|
|
5
|
+
interimResults: boolean;
|
|
6
|
+
lang: string;
|
|
7
|
+
onend: ((this: SpeechRecognition, ev: Event) => unknown) | null;
|
|
8
|
+
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => unknown) | null;
|
|
9
|
+
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => unknown) | null;
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
13
|
+
interface SpeechRecognitionEvent extends Event {
|
|
14
|
+
readonly resultIndex: number;
|
|
15
|
+
readonly results: SpeechRecognitionResultList;
|
|
16
|
+
}
|
|
17
|
+
interface SpeechRecognitionResultList {
|
|
18
|
+
[index: number]: SpeechRecognitionResult;
|
|
19
|
+
item(index: number): SpeechRecognitionResult;
|
|
20
|
+
readonly length: number;
|
|
21
|
+
}
|
|
22
|
+
interface SpeechRecognitionResult {
|
|
23
|
+
[index: number]: SpeechRecognitionAlternative;
|
|
24
|
+
readonly isFinal: boolean;
|
|
25
|
+
item(index: number): SpeechRecognitionAlternative;
|
|
26
|
+
readonly length: number;
|
|
27
|
+
}
|
|
28
|
+
interface SpeechRecognitionAlternative {
|
|
29
|
+
readonly confidence: number;
|
|
30
|
+
readonly transcript: string;
|
|
31
|
+
}
|
|
32
|
+
interface SpeechRecognitionErrorEvent extends Event {
|
|
33
|
+
readonly error: string;
|
|
34
|
+
readonly message?: string;
|
|
35
|
+
}
|
|
36
|
+
interface SpeechRecognitionConstructor {
|
|
37
|
+
new (): SpeechRecognition;
|
|
38
|
+
}
|
|
39
|
+
declare global {
|
|
40
|
+
interface Window {
|
|
41
|
+
SpeechRecognition?: SpeechRecognitionConstructor;
|
|
42
|
+
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
type SpeechStatus = 'idle' | 'listening' | 'stopped' | 'unsupported' | 'error';
|
|
46
|
+
interface UseSpeechOptions {
|
|
47
|
+
continuous?: boolean;
|
|
48
|
+
interimResults?: boolean;
|
|
49
|
+
lang?: string;
|
|
50
|
+
}
|
|
51
|
+
interface UseSpeechReturn {
|
|
52
|
+
error: Error | null;
|
|
53
|
+
finalTranscript: string;
|
|
54
|
+
interimTranscript: string;
|
|
55
|
+
reset: () => void;
|
|
56
|
+
start: () => void;
|
|
57
|
+
status: SpeechStatus;
|
|
58
|
+
stop: () => void;
|
|
59
|
+
transcript: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `useSpeech` is a React hook that provides unopinionated access to the Speech API.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const speech = useSpeech({ lang: 'en-US' });
|
|
67
|
+
* speech.start();
|
|
68
|
+
* ```
|
|
69
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API
|
|
70
|
+
*
|
|
71
|
+
*/
|
|
72
|
+
declare function useSpeech(options?: UseSpeechOptions): UseSpeechReturn;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { useSpeech };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useSpeech.ts
|
|
4
|
+
function isChrome() {
|
|
5
|
+
if (typeof navigator === "undefined") return false;
|
|
6
|
+
const ua = navigator.userAgent;
|
|
7
|
+
const isChromium = ua.includes("Chrome");
|
|
8
|
+
const isEdge = ua.includes("Edg");
|
|
9
|
+
const isBrave = typeof navigator.brave === "object";
|
|
10
|
+
return isChromium && !isEdge && !isBrave;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `useSpeech` is a React hook that provides unopinionated access to the Speech API.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const speech = useSpeech({ lang: 'en-US' });
|
|
18
|
+
* speech.start();
|
|
19
|
+
* ```
|
|
20
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
function useSpeech(options = {}) {
|
|
24
|
+
const isClient = typeof window !== "undefined";
|
|
25
|
+
const isChromeBrowser = React.useMemo(() => isChrome(), []);
|
|
26
|
+
const recognitionRef = React.useRef(null);
|
|
27
|
+
const isManuallyStoppedRef = React.useRef(false);
|
|
28
|
+
const shouldRestartRef = React.useRef(false);
|
|
29
|
+
const nextStatusRef = React.useRef(null);
|
|
30
|
+
const [status, setStatus] = React.useState("idle");
|
|
31
|
+
const [error, setError] = React.useState(null);
|
|
32
|
+
const [finalTranscript, setFinalTranscript] = React.useState("");
|
|
33
|
+
const [interimTranscript, setInterimTranscript] = React.useState("");
|
|
34
|
+
const initializeRecognition = React.useCallback(() => {
|
|
35
|
+
if (!isClient) return;
|
|
36
|
+
if (!isChromeBrowser) {
|
|
37
|
+
setStatus("unsupported");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const SpeechRecognitionCtor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
|
|
41
|
+
if (!SpeechRecognitionCtor) {
|
|
42
|
+
setStatus("unsupported");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const recognition = new SpeechRecognitionCtor();
|
|
46
|
+
recognition.lang = options.lang ?? "en-US";
|
|
47
|
+
recognition.continuous = options.continuous ?? true;
|
|
48
|
+
recognition.interimResults = options.interimResults ?? true;
|
|
49
|
+
recognition.onresult = (event) => {
|
|
50
|
+
let interim = "";
|
|
51
|
+
let final = "";
|
|
52
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
53
|
+
const result = event.results[i];
|
|
54
|
+
const text = result?.[0]?.transcript ?? "";
|
|
55
|
+
if (result?.isFinal) final += text + " ";
|
|
56
|
+
else interim += text;
|
|
57
|
+
}
|
|
58
|
+
if (interim) setInterimTranscript(interim);
|
|
59
|
+
if (final) {
|
|
60
|
+
setFinalTranscript((prev) => prev + final);
|
|
61
|
+
setInterimTranscript("");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
recognition.onerror = (event) => {
|
|
65
|
+
if (isManuallyStoppedRef.current) return;
|
|
66
|
+
if (event.error === "no-speech") {
|
|
67
|
+
console.warn("No se detectó voz.");
|
|
68
|
+
shouldRestartRef.current = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (event.error === "aborted") return;
|
|
72
|
+
setError(new Error(event.error));
|
|
73
|
+
setStatus("error");
|
|
74
|
+
};
|
|
75
|
+
recognition.onend = () => {
|
|
76
|
+
if (isManuallyStoppedRef.current) {
|
|
77
|
+
isManuallyStoppedRef.current = false;
|
|
78
|
+
setStatus(nextStatusRef.current ?? "stopped");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (status === "error") return;
|
|
82
|
+
if (shouldRestartRef.current) {
|
|
83
|
+
try {
|
|
84
|
+
recognition.start();
|
|
85
|
+
setStatus("listening");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("Error al reiniciar reconocimiento:", err);
|
|
88
|
+
setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to restart"));
|
|
89
|
+
setStatus("error");
|
|
90
|
+
shouldRestartRef.current = false;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setStatus("stopped");
|
|
95
|
+
};
|
|
96
|
+
recognitionRef.current = recognition;
|
|
97
|
+
}, [
|
|
98
|
+
isClient,
|
|
99
|
+
options.lang,
|
|
100
|
+
options.continuous,
|
|
101
|
+
options.interimResults
|
|
102
|
+
]);
|
|
103
|
+
const start = React.useCallback(() => {
|
|
104
|
+
if (!recognitionRef.current) initializeRecognition();
|
|
105
|
+
const recognition = recognitionRef.current;
|
|
106
|
+
if (!recognition) return;
|
|
107
|
+
try {
|
|
108
|
+
isManuallyStoppedRef.current = false;
|
|
109
|
+
shouldRestartRef.current = true;
|
|
110
|
+
recognition.start();
|
|
111
|
+
setStatus("listening");
|
|
112
|
+
setError(null);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("Error al iniciar reconocimiento:", err);
|
|
115
|
+
setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to start"));
|
|
116
|
+
setStatus("error");
|
|
117
|
+
}
|
|
118
|
+
}, [initializeRecognition]);
|
|
119
|
+
const stop = React.useCallback(() => {
|
|
120
|
+
isManuallyStoppedRef.current = true;
|
|
121
|
+
shouldRestartRef.current = false;
|
|
122
|
+
if (recognitionRef.current) recognitionRef.current.stop();
|
|
123
|
+
}, []);
|
|
124
|
+
const reset = React.useCallback(() => {
|
|
125
|
+
nextStatusRef.current = "idle";
|
|
126
|
+
stop();
|
|
127
|
+
setFinalTranscript("");
|
|
128
|
+
setInterimTranscript("");
|
|
129
|
+
setError(null);
|
|
130
|
+
}, [stop]);
|
|
131
|
+
const transcript = React.useMemo(() => `${finalTranscript}${interimTranscript}`.trim(), [finalTranscript, interimTranscript]);
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
initializeRecognition();
|
|
134
|
+
return () => {
|
|
135
|
+
isManuallyStoppedRef.current = true;
|
|
136
|
+
shouldRestartRef.current = false;
|
|
137
|
+
if (recognitionRef.current) {
|
|
138
|
+
recognitionRef.current.stop();
|
|
139
|
+
recognitionRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}, [initializeRecognition]);
|
|
143
|
+
return {
|
|
144
|
+
error,
|
|
145
|
+
finalTranscript: finalTranscript.trim(),
|
|
146
|
+
interimTranscript,
|
|
147
|
+
reset,
|
|
148
|
+
start,
|
|
149
|
+
status,
|
|
150
|
+
stop,
|
|
151
|
+
transcript
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { useSpeech };
|