@lobehub/ui 6.0.0-beta.2 → 6.0.0-beta.3
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/es/Accordion/style.d.mts +1 -1
- package/es/Tabs/style.d.mts +1 -1
- package/es/Video/Video.mjs +46 -0
- package/es/Video/Video.mjs.map +1 -0
- package/es/Video/context.mjs +14 -0
- package/es/Video/context.mjs.map +1 -0
- package/es/Video/index.d.mts +23 -39
- package/es/Video/index.mjs +21 -111
- package/es/Video/index.mjs.map +1 -1
- package/es/Video/parts/Caption.d.mts +9 -0
- package/es/Video/parts/Caption.mjs +34 -0
- package/es/Video/parts/Caption.mjs.map +1 -0
- package/es/Video/parts/CenterPlay.d.mts +8 -0
- package/es/Video/parts/CenterPlay.mjs +34 -0
- package/es/Video/parts/CenterPlay.mjs.map +1 -0
- package/es/Video/parts/Controls.d.mts +9 -0
- package/es/Video/parts/Controls.mjs +123 -0
- package/es/Video/parts/Controls.mjs.map +1 -0
- package/es/Video/parts/Frame.d.mts +9 -0
- package/es/Video/parts/Frame.mjs +94 -0
- package/es/Video/parts/Frame.mjs.map +1 -0
- package/es/Video/parts/HoverPreview.mjs +93 -0
- package/es/Video/parts/HoverPreview.mjs.map +1 -0
- package/es/Video/parts/LoadError.d.mts +8 -0
- package/es/Video/parts/LoadError.mjs +47 -0
- package/es/Video/parts/LoadError.mjs.map +1 -0
- package/es/Video/parts/PlayPauseIcon.mjs +48 -0
- package/es/Video/parts/PlayPauseIcon.mjs.map +1 -0
- package/es/Video/parts/RateMenu.mjs +48 -0
- package/es/Video/parts/RateMenu.mjs.map +1 -0
- package/es/Video/parts/Root.d.mts +9 -0
- package/es/Video/parts/Root.mjs +110 -0
- package/es/Video/parts/Root.mjs.map +1 -0
- package/es/Video/parts/SectionList.d.mts +9 -0
- package/es/Video/parts/SectionList.mjs +32 -0
- package/es/Video/parts/SectionList.mjs.map +1 -0
- package/es/Video/parts/SectionMenu.mjs +63 -0
- package/es/Video/parts/SectionMenu.mjs.map +1 -0
- package/es/Video/parts/Timeline.d.mts +9 -0
- package/es/Video/parts/Timeline.mjs +169 -0
- package/es/Video/parts/Timeline.mjs.map +1 -0
- package/es/Video/store.mjs +241 -0
- package/es/Video/store.mjs.map +1 -0
- package/es/Video/style.mjs +393 -23
- package/es/Video/style.mjs.map +1 -1
- package/es/Video/time.mjs +60 -0
- package/es/Video/time.mjs.map +1 -0
- package/es/Video/type.d.mts +125 -0
- package/es/Video/useControlsIdle.mjs +39 -0
- package/es/Video/useControlsIdle.mjs.map +1 -0
- package/es/Video/useFullscreen.mjs +17 -0
- package/es/Video/useFullscreen.mjs.map +1 -0
- package/es/Video/useKeyboard.mjs +60 -0
- package/es/Video/useKeyboard.mjs.map +1 -0
- package/es/Video/useVideo.d.mts +9 -0
- package/es/Video/useVideo.mjs +17 -0
- package/es/Video/useVideo.mjs.map +1 -0
- package/es/i18n/resources/en/index.d.mts +3 -2
- package/es/i18n/resources/en/index.mjs +4 -2
- package/es/i18n/resources/en/index.mjs.map +1 -1
- package/es/i18n/resources/en/video.d.mts +23 -0
- package/es/i18n/resources/en/video.mjs +24 -0
- package/es/i18n/resources/en/video.mjs.map +1 -0
- package/es/i18n/resources/zhCn/index.d.mts +3 -2
- package/es/i18n/resources/zhCn/index.mjs +4 -2
- package/es/i18n/resources/zhCn/index.mjs.map +1 -1
- package/es/i18n/resources/zhCn/video.d.mts +23 -0
- package/es/i18n/resources/zhCn/video.mjs +24 -0
- package/es/i18n/resources/zhCn/video.mjs.map +1 -0
- package/es/i18n/types.d.mts +2 -1
- package/es/index.d.mts +4 -2
- package/es/index.mjs +2 -1
- package/es/mdx/mdxComponents/Video.d.mts +2 -1
- package/es/mdx/mdxComponents/Video.mjs +5 -3
- package/es/mdx/mdxComponents/Video.mjs.map +1 -1
- package/es/style.css +263 -23
- package/package.json +1 -1
- package/es/Video/marker.stylex.mjs +0 -9
- package/es/Video/marker.stylex.mjs.map +0 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { VideoTimelineProps } from "../type.mjs";
|
|
2
|
+
//#region src/Video/parts/Timeline.d.ts
|
|
3
|
+
declare const Timeline: {
|
|
4
|
+
({ children, className, style, thumbnails }: VideoTimelineProps): import("react").JSX.Element;
|
|
5
|
+
displayName: string;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { Timeline as default };
|
|
9
|
+
//# sourceMappingURL=Timeline.d.mts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { styleProps } from "../../styles/stylex/props.mjs";
|
|
3
|
+
import { useTranslation } from "../../i18n/useTranslation.mjs";
|
|
4
|
+
import { useVideoContext } from "../context.mjs";
|
|
5
|
+
import { markerColor, styles } from "../style.mjs";
|
|
6
|
+
import { formatTime, mergeSegments, sectionAt } from "../time.mjs";
|
|
7
|
+
import { useVideo } from "../useVideo.mjs";
|
|
8
|
+
import video_default from "../../i18n/resources/en/video.mjs";
|
|
9
|
+
import HoverPreview from "./HoverPreview.mjs";
|
|
10
|
+
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
//#region src/Video/parts/Timeline.tsx
|
|
13
|
+
const SNAP_RATIO = .012;
|
|
14
|
+
const GAP_PX = 3;
|
|
15
|
+
const PREVIEW_HALF = 88;
|
|
16
|
+
const canHover = () => typeof window === "undefined" || !window.matchMedia?.("(hover: none)").matches;
|
|
17
|
+
const markerName = (marker) => [
|
|
18
|
+
formatTime(marker.time),
|
|
19
|
+
marker.label,
|
|
20
|
+
marker.content
|
|
21
|
+
].filter((part) => typeof part === "string").join(" ");
|
|
22
|
+
const Timeline = ({ children, className, style, thumbnails = true }) => {
|
|
23
|
+
const { config, media, store } = useVideoContext();
|
|
24
|
+
const { t } = useTranslation(video_default);
|
|
25
|
+
const duration = useVideo((state) => Number.isFinite(state.duration) ? state.duration : 0);
|
|
26
|
+
const trackRef = useRef(null);
|
|
27
|
+
const [width, setWidth] = useState(0);
|
|
28
|
+
const [hover, setHover] = useState(null);
|
|
29
|
+
const [hovering, setHovering] = useState(false);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const track = trackRef.current;
|
|
32
|
+
if (!track) return;
|
|
33
|
+
setWidth(track.getBoundingClientRect().width);
|
|
34
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
35
|
+
const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
|
|
36
|
+
observer.observe(track);
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, []);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const track = trackRef.current;
|
|
41
|
+
if (!track) return;
|
|
42
|
+
const setOnChange = (name, value) => {
|
|
43
|
+
if (track.getAttribute(name) !== value) track.setAttribute(name, value);
|
|
44
|
+
};
|
|
45
|
+
const paint = (state) => {
|
|
46
|
+
track.style.setProperty("--video-time", String(state.time));
|
|
47
|
+
setOnChange("aria-valuenow", String(Math.round(state.time)));
|
|
48
|
+
setOnChange("aria-valuetext", [`${formatTime(state.time)} / ${formatTime(state.duration)}`, state.activeSection?.title].filter(Boolean).join(", "));
|
|
49
|
+
};
|
|
50
|
+
paint(store.getState());
|
|
51
|
+
return store.subscribe(paint);
|
|
52
|
+
}, [store]);
|
|
53
|
+
const { hairlines, segments } = useMemo(() => mergeSegments(config.sections, duration, width), [
|
|
54
|
+
config.sections,
|
|
55
|
+
duration,
|
|
56
|
+
width
|
|
57
|
+
]);
|
|
58
|
+
const visibleMarkers = useMemo(() => config.markers.filter((marker) => marker.time >= 0 && marker.time <= duration), [config.markers, duration]);
|
|
59
|
+
const pct = (seconds) => duration > 0 ? seconds / duration * 100 : 0;
|
|
60
|
+
const timeAt = (element, clientX) => {
|
|
61
|
+
const box = element.getBoundingClientRect();
|
|
62
|
+
if (!box.width) return 0;
|
|
63
|
+
return Math.min(Math.max((clientX - box.left) / box.width, 0), 1) * duration;
|
|
64
|
+
};
|
|
65
|
+
const onPointerDown = (event) => {
|
|
66
|
+
if (event.button !== 0 || !duration) return;
|
|
67
|
+
const element = event.currentTarget;
|
|
68
|
+
store.seek(timeAt(element, event.clientX));
|
|
69
|
+
element.setPointerCapture?.(event.pointerId);
|
|
70
|
+
const move = (next) => store.seek(timeAt(element, next.clientX));
|
|
71
|
+
const up = () => {
|
|
72
|
+
element.removeEventListener("pointermove", move);
|
|
73
|
+
element.removeEventListener("pointerup", up);
|
|
74
|
+
element.removeEventListener("pointercancel", up);
|
|
75
|
+
};
|
|
76
|
+
element.addEventListener("pointermove", move);
|
|
77
|
+
element.addEventListener("pointerup", up);
|
|
78
|
+
element.addEventListener("pointercancel", up);
|
|
79
|
+
};
|
|
80
|
+
const onPointerMove = (event) => {
|
|
81
|
+
if (event.pointerType === "touch" || !duration || !canHover()) return;
|
|
82
|
+
const element = event.currentTarget;
|
|
83
|
+
const box = element.getBoundingClientRect();
|
|
84
|
+
const seconds = timeAt(element, event.clientX);
|
|
85
|
+
const snap = duration * SNAP_RATIO;
|
|
86
|
+
const marker = visibleMarkers.find((item) => Math.abs(item.time - seconds) < snap);
|
|
87
|
+
setHovering(true);
|
|
88
|
+
setHover({
|
|
89
|
+
marker,
|
|
90
|
+
time: marker?.time ?? seconds,
|
|
91
|
+
x: Math.min(Math.max(event.clientX - box.left, PREVIEW_HALF), Math.max(box.width - PREVIEW_HALF, PREVIEW_HALF))
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
...styleProps(void 0, className, {
|
|
96
|
+
position: "relative",
|
|
97
|
+
...style
|
|
98
|
+
}),
|
|
99
|
+
children: [
|
|
100
|
+
hover && /* @__PURE__ */ jsx(HoverPreview, {
|
|
101
|
+
marker: hover.marker,
|
|
102
|
+
section: sectionAt(config.sections, hover.time),
|
|
103
|
+
src: media.src,
|
|
104
|
+
thumbnail: thumbnails,
|
|
105
|
+
time: hover.time,
|
|
106
|
+
visible: hovering,
|
|
107
|
+
x: hover.x
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsxs("div", {
|
|
110
|
+
"aria-label": t("video.seek"),
|
|
111
|
+
"aria-valuemax": Math.round(duration),
|
|
112
|
+
"aria-valuemin": 0,
|
|
113
|
+
ref: trackRef,
|
|
114
|
+
role: "slider",
|
|
115
|
+
...styleProps(styles.track),
|
|
116
|
+
onPointerDown,
|
|
117
|
+
onPointerLeave: () => setHovering(false),
|
|
118
|
+
onPointerMove,
|
|
119
|
+
children: [
|
|
120
|
+
segments.map((segment, index) => {
|
|
121
|
+
const span = segment.end - segment.start;
|
|
122
|
+
const hovered = hovering && hover !== null && hover.time >= segment.start && hover.time < segment.end;
|
|
123
|
+
const hoverFill = hovering && hover ? Math.min(Math.max((hover.time - segment.start) / span, 0), 1) : 0;
|
|
124
|
+
const leftInset = index === 0 ? 0 : GAP_PX / 2;
|
|
125
|
+
const rightInset = index === segments.length - 1 ? 0 : GAP_PX / 2;
|
|
126
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
127
|
+
"data-video-segment": "",
|
|
128
|
+
...styleProps([styles.segment, hovered && styles.segmentHovered], void 0, {
|
|
129
|
+
left: `calc(${pct(segment.start)}% + ${leftInset}px)`,
|
|
130
|
+
width: `calc(${pct(span)}% - ${leftInset + rightInset}px)`
|
|
131
|
+
}),
|
|
132
|
+
children: [hoverFill > 0 && /* @__PURE__ */ jsx("div", { ...styleProps(styles.segmentFill, void 0, {
|
|
133
|
+
background: "rgb(255 255 255 / 32%)",
|
|
134
|
+
width: `${hoverFill * 100}%`
|
|
135
|
+
}) }), /* @__PURE__ */ jsx("div", { ...styleProps(styles.segmentFill, void 0, {
|
|
136
|
+
background: "#fff",
|
|
137
|
+
width: `calc(clamp(0, (var(--video-time, 0) - ${segment.start}) / ${span}, 1) * 100%)`
|
|
138
|
+
}) })]
|
|
139
|
+
}, segment.start);
|
|
140
|
+
}),
|
|
141
|
+
hairlines.map((at) => /* @__PURE__ */ jsx("div", { ...styleProps(styles.hairline, void 0, { left: `${pct(at)}%` }) }, at)),
|
|
142
|
+
/* @__PURE__ */ jsx("div", { ...styleProps(styles.head, void 0, { left: duration > 0 ? `calc(var(--video-time, 0) / ${duration} * 100%)` : 0 }) }),
|
|
143
|
+
visibleMarkers.map((marker, index) => /* @__PURE__ */ jsx("button", {
|
|
144
|
+
"aria-label": markerName(marker),
|
|
145
|
+
type: "button",
|
|
146
|
+
...styleProps(styles.marker, void 0, {
|
|
147
|
+
background: markerColor(marker.color),
|
|
148
|
+
left: `${pct(marker.time)}%`
|
|
149
|
+
}),
|
|
150
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
151
|
+
onClick: (event) => {
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
store.pause();
|
|
154
|
+
store.seek(marker.time);
|
|
155
|
+
store.setCaptions(true);
|
|
156
|
+
config.onMarkerClick?.(marker);
|
|
157
|
+
}
|
|
158
|
+
}, marker.key ?? `${marker.time}-${index}`))
|
|
159
|
+
]
|
|
160
|
+
}),
|
|
161
|
+
children
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
Timeline.displayName = "VideoTimeline";
|
|
166
|
+
//#endregion
|
|
167
|
+
export { Timeline as default };
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=Timeline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Timeline.mjs","names":["videoMessages"],"sources":["../../../src/Video/parts/Timeline.tsx"],"sourcesContent":["'use client';\n\nimport {\n type PointerEvent as ReactPointerEvent,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport videoMessages from '@/i18n/resources/en/video';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { styleProps } from '@/styles/stylex/props';\n\nimport { useVideoContext } from '../context';\nimport { markerColor, styles } from '../style';\nimport { formatTime, mergeSegments, sectionAt } from '../time';\nimport type { VideoMarker, VideoState, VideoTimelineProps } from '../type';\nimport { useVideo } from '../useVideo';\nimport HoverPreview from './HoverPreview';\n\nconst SNAP_RATIO = 0.012;\nconst GAP_PX = 3;\nconst PREVIEW_HALF = 88;\n\ninterface Hover {\n marker?: VideoMarker;\n time: number;\n x: number;\n}\n\nconst canHover = () =>\n typeof window === 'undefined' || !window.matchMedia?.('(hover: none)').matches;\n\nconst markerName = (marker: VideoMarker) =>\n [formatTime(marker.time), marker.label, marker.content]\n .filter((part) => typeof part === 'string')\n .join(' ');\n\nconst Timeline = ({ children, className, style, thumbnails = true }: VideoTimelineProps) => {\n const { config, media, store } = useVideoContext();\n const { t } = useTranslation(videoMessages);\n const duration = useVideo((state) => (Number.isFinite(state.duration) ? state.duration : 0));\n const trackRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState(0);\n const [hover, setHover] = useState<Hover | null>(null);\n const [hovering, setHovering] = useState(false);\n\n useLayoutEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n setWidth(track.getBoundingClientRect().width);\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));\n observer.observe(track);\n return () => observer.disconnect();\n }, []);\n\n // Playback must not re-render the timeline: the playhead and the played\n // fills read --video-time, written straight to the track on every frame.\n useLayoutEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n const setOnChange = (name: string, value: string) => {\n if (track.getAttribute(name) !== value) track.setAttribute(name, value);\n };\n const paint = (state: VideoState) => {\n track.style.setProperty('--video-time', String(state.time));\n setOnChange('aria-valuenow', String(Math.round(state.time)));\n setOnChange(\n 'aria-valuetext',\n [`${formatTime(state.time)} / ${formatTime(state.duration)}`, state.activeSection?.title]\n .filter(Boolean)\n .join(', '),\n );\n };\n paint(store.getState());\n return store.subscribe(paint);\n }, [store]);\n\n const { hairlines, segments } = useMemo(\n () => mergeSegments(config.sections, duration, width),\n [config.sections, duration, width],\n );\n\n const visibleMarkers = useMemo(\n () => config.markers.filter((marker) => marker.time >= 0 && marker.time <= duration),\n [config.markers, duration],\n );\n\n const pct = (seconds: number) => (duration > 0 ? (seconds / duration) * 100 : 0);\n const timeAt = (element: HTMLElement, clientX: number) => {\n const box = element.getBoundingClientRect();\n if (!box.width) return 0;\n return Math.min(Math.max((clientX - box.left) / box.width, 0), 1) * duration;\n };\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.button !== 0 || !duration) return;\n const element = event.currentTarget;\n store.seek(timeAt(element, event.clientX));\n element.setPointerCapture?.(event.pointerId);\n const move = (next: PointerEvent) => store.seek(timeAt(element, next.clientX));\n const up = () => {\n element.removeEventListener('pointermove', move);\n element.removeEventListener('pointerup', up);\n element.removeEventListener('pointercancel', up);\n };\n element.addEventListener('pointermove', move);\n element.addEventListener('pointerup', up);\n element.addEventListener('pointercancel', up);\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.pointerType === 'touch' || !duration || !canHover()) return;\n const element = event.currentTarget;\n const box = element.getBoundingClientRect();\n const seconds = timeAt(element, event.clientX);\n const snap = duration * SNAP_RATIO;\n const marker = visibleMarkers.find((item) => Math.abs(item.time - seconds) < snap);\n setHovering(true);\n setHover({\n marker,\n time: marker?.time ?? seconds,\n x: Math.min(\n Math.max(event.clientX - box.left, PREVIEW_HALF),\n Math.max(box.width - PREVIEW_HALF, PREVIEW_HALF),\n ),\n });\n };\n\n return (\n <div {...styleProps(undefined, className, { position: 'relative', ...style })}>\n {hover && (\n <HoverPreview\n marker={hover.marker}\n section={sectionAt(config.sections, hover.time)}\n src={media.src}\n thumbnail={thumbnails}\n time={hover.time}\n visible={hovering}\n x={hover.x}\n />\n )}\n <div\n aria-label={t('video.seek')}\n aria-valuemax={Math.round(duration)}\n aria-valuemin={0}\n ref={trackRef}\n role=\"slider\"\n {...styleProps(styles.track)}\n onPointerDown={onPointerDown}\n onPointerLeave={() => setHovering(false)}\n onPointerMove={onPointerMove}\n >\n {segments.map((segment, index) => {\n const span = segment.end - segment.start;\n const hovered =\n hovering && hover !== null && hover.time >= segment.start && hover.time < segment.end;\n const hoverFill =\n hovering && hover ? Math.min(Math.max((hover.time - segment.start) / span, 0), 1) : 0;\n const leftInset = index === 0 ? 0 : GAP_PX / 2;\n const rightInset = index === segments.length - 1 ? 0 : GAP_PX / 2;\n return (\n <div\n data-video-segment=\"\"\n key={segment.start}\n {...styleProps([styles.segment, hovered && styles.segmentHovered], undefined, {\n left: `calc(${pct(segment.start)}% + ${leftInset}px)`,\n width: `calc(${pct(span)}% - ${leftInset + rightInset}px)`,\n })}\n >\n {hoverFill > 0 && (\n <div\n {...styleProps(styles.segmentFill, undefined, {\n background: 'rgb(255 255 255 / 32%)',\n width: `${hoverFill * 100}%`,\n })}\n />\n )}\n <div\n {...styleProps(styles.segmentFill, undefined, {\n background: '#fff',\n width: `calc(clamp(0, (var(--video-time, 0) - ${segment.start}) / ${span}, 1) * 100%)`,\n })}\n />\n </div>\n );\n })}\n {hairlines.map((at) => (\n <div key={at} {...styleProps(styles.hairline, undefined, { left: `${pct(at)}%` })} />\n ))}\n <div\n {...styleProps(styles.head, undefined, {\n left: duration > 0 ? `calc(var(--video-time, 0) / ${duration} * 100%)` : 0,\n })}\n />\n {visibleMarkers.map((marker, index) => (\n <button\n aria-label={markerName(marker)}\n key={marker.key ?? `${marker.time}-${index}`}\n type=\"button\"\n {...styleProps(styles.marker, undefined, {\n background: markerColor(marker.color),\n left: `${pct(marker.time)}%`,\n })}\n onPointerDown={(event) => event.stopPropagation()}\n onClick={(event) => {\n event.stopPropagation();\n store.pause();\n store.seek(marker.time);\n store.setCaptions(true);\n config.onMarkerClick?.(marker);\n }}\n />\n ))}\n </div>\n {children}\n </div>\n );\n};\n\nTimeline.displayName = 'VideoTimeline';\n\nexport default Timeline;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { markersAt, sectionAt, sortSections, steppedTime } from "./time.mjs";
|
|
2
|
+
//#region src/Video/store.ts
|
|
3
|
+
const EMPTY_MARKERS = [];
|
|
4
|
+
const sameMarkers = (a, b) => a.length === b.length && a.every((marker, index) => marker === b[index]);
|
|
5
|
+
const createVideoStore = () => {
|
|
6
|
+
let data = {
|
|
7
|
+
fps: 30,
|
|
8
|
+
markers: [],
|
|
9
|
+
sections: []
|
|
10
|
+
};
|
|
11
|
+
let options = {};
|
|
12
|
+
let video = null;
|
|
13
|
+
let root = null;
|
|
14
|
+
let painted;
|
|
15
|
+
let state = {
|
|
16
|
+
activeMarkers: EMPTY_MARKERS,
|
|
17
|
+
activeSection: void 0,
|
|
18
|
+
captions: true,
|
|
19
|
+
duration: 0,
|
|
20
|
+
muted: false,
|
|
21
|
+
paused: true,
|
|
22
|
+
rate: 1,
|
|
23
|
+
size: void 0,
|
|
24
|
+
status: "loading",
|
|
25
|
+
time: 0
|
|
26
|
+
};
|
|
27
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
28
|
+
const derive = (next) => {
|
|
29
|
+
const activeSection = sectionAt(data.sections, next.time);
|
|
30
|
+
const found = markersAt(data.markers, next.time, data.fps);
|
|
31
|
+
const activeMarkers = found.length === 0 ? EMPTY_MARKERS : sameMarkers(found, state.activeMarkers) ? state.activeMarkers : found;
|
|
32
|
+
return {
|
|
33
|
+
...next,
|
|
34
|
+
activeMarkers,
|
|
35
|
+
activeSection
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const commit = (patch) => {
|
|
39
|
+
const next = derive({
|
|
40
|
+
...state,
|
|
41
|
+
...patch
|
|
42
|
+
});
|
|
43
|
+
if (!Object.keys(next).some((key) => next[key] !== state[key])) return;
|
|
44
|
+
state = next;
|
|
45
|
+
for (const listener of listeners) listener(state);
|
|
46
|
+
};
|
|
47
|
+
const frameTime = () => {
|
|
48
|
+
if (!video) return 0;
|
|
49
|
+
return video.paused ? video.currentTime : painted ?? video.currentTime;
|
|
50
|
+
};
|
|
51
|
+
const clampToMedia = (seconds) => {
|
|
52
|
+
const lower = Math.max(seconds, 0);
|
|
53
|
+
return video && Number.isFinite(video.duration) ? Math.min(lower, video.duration) : lower;
|
|
54
|
+
};
|
|
55
|
+
const atEnd = () => Boolean(video) && (video.ended || Number.isFinite(video.duration) && video.currentTime >= video.duration - .5 / data.fps);
|
|
56
|
+
const seek = (seconds) => {
|
|
57
|
+
if (!video || !Number.isFinite(seconds)) return;
|
|
58
|
+
painted = void 0;
|
|
59
|
+
video.currentTime = clampToMedia(seconds);
|
|
60
|
+
commit({ time: video.currentTime });
|
|
61
|
+
};
|
|
62
|
+
let attachedOnce = false;
|
|
63
|
+
return {
|
|
64
|
+
attach: (element) => {
|
|
65
|
+
const target = element;
|
|
66
|
+
video = target;
|
|
67
|
+
let handle = 0;
|
|
68
|
+
let running = false;
|
|
69
|
+
const cancel = () => {
|
|
70
|
+
if (!running) return;
|
|
71
|
+
running = false;
|
|
72
|
+
if (target.cancelVideoFrameCallback) target.cancelVideoFrameCallback(handle);
|
|
73
|
+
else cancelAnimationFrame(handle);
|
|
74
|
+
};
|
|
75
|
+
const schedule = () => {
|
|
76
|
+
handle = target.requestVideoFrameCallback ? target.requestVideoFrameCallback(tick) : requestAnimationFrame(() => tick());
|
|
77
|
+
};
|
|
78
|
+
function tick(_now, frame) {
|
|
79
|
+
if (!running) return;
|
|
80
|
+
if (!target.paused) painted = frame?.mediaTime;
|
|
81
|
+
commit({ time: target.paused ? target.currentTime : painted ?? target.currentTime });
|
|
82
|
+
schedule();
|
|
83
|
+
}
|
|
84
|
+
const start = () => {
|
|
85
|
+
if (running) return;
|
|
86
|
+
running = true;
|
|
87
|
+
schedule();
|
|
88
|
+
};
|
|
89
|
+
const onMeta = () => commit({
|
|
90
|
+
duration: target.duration,
|
|
91
|
+
time: target.currentTime,
|
|
92
|
+
size: {
|
|
93
|
+
height: target.videoHeight,
|
|
94
|
+
width: target.videoWidth
|
|
95
|
+
},
|
|
96
|
+
status: "ready"
|
|
97
|
+
});
|
|
98
|
+
const onPlay = () => {
|
|
99
|
+
commit({ paused: false });
|
|
100
|
+
start();
|
|
101
|
+
};
|
|
102
|
+
const onPause = () => {
|
|
103
|
+
cancel();
|
|
104
|
+
const held = painted;
|
|
105
|
+
painted = void 0;
|
|
106
|
+
if (!atEnd() && held !== void 0 && Math.abs(target.currentTime - held) > .001) target.currentTime = held;
|
|
107
|
+
commit({
|
|
108
|
+
paused: true,
|
|
109
|
+
time: target.currentTime
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const onSeeked = () => {
|
|
113
|
+
painted = void 0;
|
|
114
|
+
commit({ time: target.currentTime });
|
|
115
|
+
};
|
|
116
|
+
const onRate = () => {
|
|
117
|
+
if (target.playbackRate === state.rate) return;
|
|
118
|
+
commit({ rate: target.playbackRate });
|
|
119
|
+
options.onRateChange?.(target.playbackRate);
|
|
120
|
+
};
|
|
121
|
+
const onVolume = () => {
|
|
122
|
+
if (target.muted === state.muted) return;
|
|
123
|
+
commit({ muted: target.muted });
|
|
124
|
+
options.onMutedChange?.(target.muted);
|
|
125
|
+
};
|
|
126
|
+
const onError = () => commit({ status: "error" });
|
|
127
|
+
const muted = attachedOnce ? state.muted : options.muted ?? false;
|
|
128
|
+
const rate = attachedOnce ? state.rate : options.defaultPlaybackRate ?? 1;
|
|
129
|
+
attachedOnce = true;
|
|
130
|
+
commit({
|
|
131
|
+
duration: Number.isFinite(target.duration) ? target.duration : 0,
|
|
132
|
+
muted,
|
|
133
|
+
paused: target.paused,
|
|
134
|
+
rate,
|
|
135
|
+
size: void 0,
|
|
136
|
+
status: "loading",
|
|
137
|
+
time: target.currentTime
|
|
138
|
+
});
|
|
139
|
+
if (target.muted !== muted) target.muted = muted;
|
|
140
|
+
if (target.playbackRate !== rate) target.playbackRate = rate;
|
|
141
|
+
const events = [
|
|
142
|
+
["loadedmetadata", onMeta],
|
|
143
|
+
["play", onPlay],
|
|
144
|
+
["pause", onPause],
|
|
145
|
+
["ended", onPause],
|
|
146
|
+
["seeked", onSeeked],
|
|
147
|
+
["ratechange", onRate],
|
|
148
|
+
["volumechange", onVolume],
|
|
149
|
+
["error", onError]
|
|
150
|
+
];
|
|
151
|
+
for (const [type, handler] of events) target.addEventListener(type, handler);
|
|
152
|
+
if (target.readyState >= 1) onMeta();
|
|
153
|
+
if (!target.paused) start();
|
|
154
|
+
return () => {
|
|
155
|
+
cancel();
|
|
156
|
+
for (const [type, handler] of events) target.removeEventListener(type, handler);
|
|
157
|
+
if (video === target) video = null;
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
attachRoot: (element) => {
|
|
161
|
+
root = element;
|
|
162
|
+
},
|
|
163
|
+
getData: () => data,
|
|
164
|
+
getState: () => state,
|
|
165
|
+
pause: () => video?.pause(),
|
|
166
|
+
play: async () => {
|
|
167
|
+
await video?.play();
|
|
168
|
+
},
|
|
169
|
+
reload: () => {
|
|
170
|
+
if (!video) return;
|
|
171
|
+
commit({ status: "loading" });
|
|
172
|
+
video.load();
|
|
173
|
+
},
|
|
174
|
+
requestFullscreen: async () => {
|
|
175
|
+
await root?.requestFullscreen?.();
|
|
176
|
+
},
|
|
177
|
+
get root() {
|
|
178
|
+
return root;
|
|
179
|
+
},
|
|
180
|
+
seek,
|
|
181
|
+
setCaptions: (captions) => commit({ captions }),
|
|
182
|
+
setData: (next) => {
|
|
183
|
+
data = {
|
|
184
|
+
...data,
|
|
185
|
+
...next,
|
|
186
|
+
sections: next.sections ? sortSections(next.sections) : data.sections
|
|
187
|
+
};
|
|
188
|
+
commit({});
|
|
189
|
+
},
|
|
190
|
+
setMuted: (muted) => {
|
|
191
|
+
if (video) video.muted = muted;
|
|
192
|
+
},
|
|
193
|
+
setOptions: (next) => {
|
|
194
|
+
options = {
|
|
195
|
+
...options,
|
|
196
|
+
...next
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
setRate: (rate) => {
|
|
200
|
+
if (video) video.playbackRate = rate;
|
|
201
|
+
},
|
|
202
|
+
step: (frames) => {
|
|
203
|
+
if (!video) return;
|
|
204
|
+
const from = frameTime();
|
|
205
|
+
painted = void 0;
|
|
206
|
+
video.pause();
|
|
207
|
+
const limit = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;
|
|
208
|
+
video.currentTime = steppedTime(from, frames, limit, data.fps);
|
|
209
|
+
commit({ time: video.currentTime });
|
|
210
|
+
},
|
|
211
|
+
subscribe: (listener) => {
|
|
212
|
+
listeners.add(listener);
|
|
213
|
+
return () => {
|
|
214
|
+
listeners.delete(listener);
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
toggle: () => {
|
|
218
|
+
if (!video) return;
|
|
219
|
+
if (!video.paused) return video.pause();
|
|
220
|
+
if (atEnd()) video.currentTime = 0;
|
|
221
|
+
video.play();
|
|
222
|
+
},
|
|
223
|
+
toggleFullscreen: () => {
|
|
224
|
+
if (typeof document !== "undefined" && document.fullscreenElement) {
|
|
225
|
+
document.exitFullscreen?.();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
root?.requestFullscreen?.();
|
|
229
|
+
},
|
|
230
|
+
toggleMute: () => {
|
|
231
|
+
if (video) video.muted = !video.muted;
|
|
232
|
+
},
|
|
233
|
+
get video() {
|
|
234
|
+
return video;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
export { createVideoStore };
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=store.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.mjs","names":[],"sources":["../../src/Video/store.ts"],"sourcesContent":["import { markersAt, sectionAt, sortSections, steppedTime } from './time';\nimport type { VideoMarker, VideoPlayer, VideoSection, VideoState } from './type';\n\nexport interface VideoStoreOptions {\n defaultPlaybackRate?: number;\n muted?: boolean;\n onMutedChange?: (muted: boolean) => void;\n onRateChange?: (rate: number) => void;\n}\n\nexport interface VideoStoreData {\n fps: number;\n markers: VideoMarker[];\n sections: VideoSection[];\n}\n\nexport interface VideoStore extends VideoPlayer {\n attach: (video: HTMLVideoElement) => () => void;\n attachRoot: (root: HTMLDivElement | null) => void;\n getData: () => VideoStoreData;\n reload: () => void;\n setData: (data: Partial<VideoStoreData>) => void;\n setOptions: (options: VideoStoreOptions) => void;\n toggleFullscreen: () => void;\n}\n\nconst EMPTY_MARKERS: VideoMarker[] = [];\n\nconst sameMarkers = (a: VideoMarker[], b: VideoMarker[]) =>\n a.length === b.length && a.every((marker, index) => marker === b[index]);\n\ntype FrameVideo = HTMLVideoElement & {\n cancelVideoFrameCallback?: (handle: number) => void;\n requestVideoFrameCallback?: (\n callback: (now: number, frame: { mediaTime: number }) => void,\n ) => number;\n};\n\nexport const createVideoStore = (): VideoStore => {\n let data: VideoStoreData = { fps: 30, markers: [], sections: [] };\n let options: VideoStoreOptions = {};\n let video: FrameVideo | null = null;\n let root: HTMLDivElement | null = null;\n // The frame actually on screen while playing. `currentTime` runs ahead of\n // the painted frame, and pausing would otherwise land a frame past it.\n let painted: number | undefined;\n let state: VideoState = {\n activeMarkers: EMPTY_MARKERS,\n activeSection: undefined,\n captions: true,\n duration: 0,\n muted: false,\n paused: true,\n rate: 1,\n size: undefined,\n status: 'loading',\n time: 0,\n };\n const listeners = new Set<(state: VideoState) => void>();\n\n const derive = (next: VideoState): VideoState => {\n const activeSection = sectionAt(data.sections, next.time);\n const found = markersAt(data.markers, next.time, data.fps);\n const activeMarkers =\n found.length === 0\n ? EMPTY_MARKERS\n : sameMarkers(found, state.activeMarkers)\n ? state.activeMarkers\n : found;\n return { ...next, activeMarkers, activeSection };\n };\n\n const commit = (patch: Partial<VideoState>) => {\n const next = derive({ ...state, ...patch });\n const changed = (Object.keys(next) as (keyof VideoState)[]).some(\n (key) => next[key] !== state[key],\n );\n if (!changed) return;\n state = next;\n for (const listener of listeners) listener(state);\n };\n\n const frameTime = () => {\n if (!video) return 0;\n return video.paused ? video.currentTime : (painted ?? video.currentTime);\n };\n\n // Before metadata the duration is NaN: only the lower bound applies, and the\n // element keeps the value as its start position.\n const clampToMedia = (seconds: number) => {\n const lower = Math.max(seconds, 0);\n return video && Number.isFinite(video.duration) ? Math.min(lower, video.duration) : lower;\n };\n\n const atEnd = () =>\n Boolean(video) &&\n (video!.ended ||\n (Number.isFinite(video!.duration) && video!.currentTime >= video!.duration - 0.5 / data.fps));\n\n const seek = (seconds: number) => {\n if (!video || !Number.isFinite(seconds)) return;\n painted = undefined;\n video.currentTime = clampToMedia(seconds);\n commit({ time: video.currentTime });\n };\n\n let attachedOnce = false;\n\n const store: VideoStore = {\n attach: (element) => {\n const target = element as FrameVideo;\n video = target;\n let handle = 0;\n let running = false;\n\n const cancel = () => {\n if (!running) return;\n running = false;\n if (target.cancelVideoFrameCallback) target.cancelVideoFrameCallback(handle);\n else cancelAnimationFrame(handle);\n };\n const schedule = () => {\n handle = target.requestVideoFrameCallback\n ? target.requestVideoFrameCallback(tick)\n : requestAnimationFrame(() => tick());\n };\n function tick(_now?: number, frame?: { mediaTime: number }) {\n if (!running) return;\n if (!target.paused) painted = frame?.mediaTime;\n commit({ time: target.paused ? target.currentTime : (painted ?? target.currentTime) });\n schedule();\n }\n const start = () => {\n if (running) return;\n running = true;\n schedule();\n };\n\n const onMeta = () =>\n commit({\n duration: target.duration,\n time: target.currentTime,\n size: { height: target.videoHeight, width: target.videoWidth },\n status: 'ready',\n });\n const onPlay = () => {\n commit({ paused: false });\n start();\n };\n const onPause = () => {\n cancel();\n const held = painted;\n painted = undefined;\n // At the end the element must stay ended, or play() would resume on\n // the last frame instead of restarting.\n if (!atEnd() && held !== undefined && Math.abs(target.currentTime - held) > 1e-3)\n target.currentTime = held;\n commit({ paused: true, time: target.currentTime });\n };\n const onSeeked = () => {\n painted = undefined;\n commit({ time: target.currentTime });\n };\n const onRate = () => {\n if (target.playbackRate === state.rate) return;\n commit({ rate: target.playbackRate });\n options.onRateChange?.(target.playbackRate);\n };\n const onVolume = () => {\n if (target.muted === state.muted) return;\n commit({ muted: target.muted });\n options.onMutedChange?.(target.muted);\n };\n const onError = () => commit({ status: 'error' });\n\n // Initial props apply once; a later video (a new src) keeps the viewer's choices.\n const muted = attachedOnce ? state.muted : (options.muted ?? false);\n const rate = attachedOnce ? state.rate : (options.defaultPlaybackRate ?? 1);\n attachedOnce = true;\n commit({\n duration: Number.isFinite(target.duration) ? target.duration : 0,\n muted,\n paused: target.paused,\n rate,\n size: undefined,\n status: 'loading',\n time: target.currentTime,\n });\n if (target.muted !== muted) target.muted = muted;\n if (target.playbackRate !== rate) target.playbackRate = rate;\n\n const events: [string, () => void][] = [\n ['loadedmetadata', onMeta],\n ['play', onPlay],\n ['pause', onPause],\n ['ended', onPause],\n ['seeked', onSeeked],\n ['ratechange', onRate],\n ['volumechange', onVolume],\n ['error', onError],\n ];\n for (const [type, handler] of events) target.addEventListener(type, handler);\n if (target.readyState >= 1) onMeta();\n if (!target.paused) start();\n\n return () => {\n cancel();\n for (const [type, handler] of events) target.removeEventListener(type, handler);\n if (video === target) video = null;\n };\n },\n attachRoot: (element) => {\n root = element;\n },\n getData: () => data,\n getState: () => state,\n pause: () => video?.pause(),\n play: async () => {\n await video?.play();\n },\n reload: () => {\n if (!video) return;\n commit({ status: 'loading' });\n video.load();\n },\n requestFullscreen: async () => {\n await root?.requestFullscreen?.();\n },\n get root() {\n return root;\n },\n seek,\n setCaptions: (captions) => commit({ captions }),\n setData: (next) => {\n data = {\n ...data,\n ...next,\n sections: next.sections ? sortSections(next.sections) : data.sections,\n };\n commit({});\n },\n setMuted: (muted) => {\n if (video) video.muted = muted;\n },\n setOptions: (next) => {\n options = { ...options, ...next };\n },\n setRate: (rate) => {\n if (video) video.playbackRate = rate;\n },\n step: (frames) => {\n if (!video) return;\n const from = frameTime();\n painted = undefined;\n video.pause();\n const limit = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;\n video.currentTime = steppedTime(from, frames, limit, data.fps);\n commit({ time: video.currentTime });\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n toggle: () => {\n if (!video) return;\n if (!video.paused) return video.pause();\n if (atEnd()) video.currentTime = 0;\n void video.play();\n },\n toggleFullscreen: () => {\n if (typeof document !== 'undefined' && document.fullscreenElement) {\n void document.exitFullscreen?.();\n return;\n }\n void root?.requestFullscreen?.();\n },\n toggleMute: () => {\n if (video) video.muted = !video.muted;\n },\n get video() {\n return video;\n },\n };\n\n return store;\n};\n"],"mappings":";;AA0BA,MAAM,gBAA+B,CAAC;AAEtC,MAAM,eAAe,GAAkB,MACrC,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,QAAQ,UAAU,WAAW,EAAE,MAAM;AASzE,MAAa,yBAAqC;CAChD,IAAI,OAAuB;EAAE,KAAK;EAAI,SAAS,CAAC;EAAG,UAAU,CAAC;CAAE;CAChE,IAAI,UAA6B,CAAC;CAClC,IAAI,QAA2B;CAC/B,IAAI,OAA8B;CAGlC,IAAI;CACJ,IAAI,QAAoB;EACtB,eAAe;EACf,eAAe,KAAA;EACf,UAAU;EACV,UAAU;EACV,OAAO;EACP,QAAQ;EACR,MAAM;EACN,MAAM,KAAA;EACN,QAAQ;EACR,MAAM;CACR;CACA,MAAM,4BAAY,IAAI,IAAiC;CAEvD,MAAM,UAAU,SAAiC;EAC/C,MAAM,gBAAgB,UAAU,KAAK,UAAU,KAAK,IAAI;EACxD,MAAM,QAAQ,UAAU,KAAK,SAAS,KAAK,MAAM,KAAK,GAAG;EACzD,MAAM,gBACJ,MAAM,WAAW,IACb,gBACA,YAAY,OAAO,MAAM,aAAa,IACpC,MAAM,gBACN;EACR,OAAO;GAAE,GAAG;GAAM;GAAe;EAAc;CACjD;CAEA,MAAM,UAAU,UAA+B;EAC7C,MAAM,OAAO,OAAO;GAAE,GAAG;GAAO,GAAG;EAAM,CAAC;EAI1C,IAAI,CAHa,OAAO,KAAK,IAAI,CAAC,CAA0B,MACzD,QAAQ,KAAK,SAAS,MAAM,IAEpB,GAAG;EACd,QAAQ;EACR,KAAK,MAAM,YAAY,WAAW,SAAS,KAAK;CAClD;CAEA,MAAM,kBAAkB;EACtB,IAAI,CAAC,OAAO,OAAO;EACnB,OAAO,MAAM,SAAS,MAAM,cAAe,WAAW,MAAM;CAC9D;CAIA,MAAM,gBAAgB,YAAoB;EACxC,MAAM,QAAQ,KAAK,IAAI,SAAS,CAAC;EACjC,OAAO,SAAS,OAAO,SAAS,MAAM,QAAQ,IAAI,KAAK,IAAI,OAAO,MAAM,QAAQ,IAAI;CACtF;CAEA,MAAM,cACJ,QAAQ,KAAK,MACZ,MAAO,SACL,OAAO,SAAS,MAAO,QAAQ,KAAK,MAAO,eAAe,MAAO,WAAW,KAAM,KAAK;CAE5F,MAAM,QAAQ,YAAoB;EAChC,IAAI,CAAC,SAAS,CAAC,OAAO,SAAS,OAAO,GAAG;EACzC,UAAU,KAAA;EACV,MAAM,cAAc,aAAa,OAAO;EACxC,OAAO,EAAE,MAAM,MAAM,YAAY,CAAC;CACpC;CAEA,IAAI,eAAe;CAoLnB,OAAO;EAjLL,SAAS,YAAY;GACnB,MAAM,SAAS;GACf,QAAQ;GACR,IAAI,SAAS;GACb,IAAI,UAAU;GAEd,MAAM,eAAe;IACnB,IAAI,CAAC,SAAS;IACd,UAAU;IACV,IAAI,OAAO,0BAA0B,OAAO,yBAAyB,MAAM;SACtE,qBAAqB,MAAM;GAClC;GACA,MAAM,iBAAiB;IACrB,SAAS,OAAO,4BACZ,OAAO,0BAA0B,IAAI,IACrC,4BAA4B,KAAK,CAAC;GACxC;GACA,SAAS,KAAK,MAAe,OAA+B;IAC1D,IAAI,CAAC,SAAS;IACd,IAAI,CAAC,OAAO,QAAQ,UAAU,OAAO;IACrC,OAAO,EAAE,MAAM,OAAO,SAAS,OAAO,cAAe,WAAW,OAAO,YAAa,CAAC;IACrF,SAAS;GACX;GACA,MAAM,cAAc;IAClB,IAAI,SAAS;IACb,UAAU;IACV,SAAS;GACX;GAEA,MAAM,eACJ,OAAO;IACL,UAAU,OAAO;IACjB,MAAM,OAAO;IACb,MAAM;KAAE,QAAQ,OAAO;KAAa,OAAO,OAAO;IAAW;IAC7D,QAAQ;GACV,CAAC;GACH,MAAM,eAAe;IACnB,OAAO,EAAE,QAAQ,MAAM,CAAC;IACxB,MAAM;GACR;GACA,MAAM,gBAAgB;IACpB,OAAO;IACP,MAAM,OAAO;IACb,UAAU,KAAA;IAGV,IAAI,CAAC,MAAM,KAAK,SAAS,KAAA,KAAa,KAAK,IAAI,OAAO,cAAc,IAAI,IAAI,MAC1E,OAAO,cAAc;IACvB,OAAO;KAAE,QAAQ;KAAM,MAAM,OAAO;IAAY,CAAC;GACnD;GACA,MAAM,iBAAiB;IACrB,UAAU,KAAA;IACV,OAAO,EAAE,MAAM,OAAO,YAAY,CAAC;GACrC;GACA,MAAM,eAAe;IACnB,IAAI,OAAO,iBAAiB,MAAM,MAAM;IACxC,OAAO,EAAE,MAAM,OAAO,aAAa,CAAC;IACpC,QAAQ,eAAe,OAAO,YAAY;GAC5C;GACA,MAAM,iBAAiB;IACrB,IAAI,OAAO,UAAU,MAAM,OAAO;IAClC,OAAO,EAAE,OAAO,OAAO,MAAM,CAAC;IAC9B,QAAQ,gBAAgB,OAAO,KAAK;GACtC;GACA,MAAM,gBAAgB,OAAO,EAAE,QAAQ,QAAQ,CAAC;GAGhD,MAAM,QAAQ,eAAe,MAAM,QAAS,QAAQ,SAAS;GAC7D,MAAM,OAAO,eAAe,MAAM,OAAQ,QAAQ,uBAAuB;GACzE,eAAe;GACf,OAAO;IACL,UAAU,OAAO,SAAS,OAAO,QAAQ,IAAI,OAAO,WAAW;IAC/D;IACA,QAAQ,OAAO;IACf;IACA,MAAM,KAAA;IACN,QAAQ;IACR,MAAM,OAAO;GACf,CAAC;GACD,IAAI,OAAO,UAAU,OAAO,OAAO,QAAQ;GAC3C,IAAI,OAAO,iBAAiB,MAAM,OAAO,eAAe;GAExD,MAAM,SAAiC;IACrC,CAAC,kBAAkB,MAAM;IACzB,CAAC,QAAQ,MAAM;IACf,CAAC,SAAS,OAAO;IACjB,CAAC,SAAS,OAAO;IACjB,CAAC,UAAU,QAAQ;IACnB,CAAC,cAAc,MAAM;IACrB,CAAC,gBAAgB,QAAQ;IACzB,CAAC,SAAS,OAAO;GACnB;GACA,KAAK,MAAM,CAAC,MAAM,YAAY,QAAQ,OAAO,iBAAiB,MAAM,OAAO;GAC3E,IAAI,OAAO,cAAc,GAAG,OAAO;GACnC,IAAI,CAAC,OAAO,QAAQ,MAAM;GAE1B,aAAa;IACX,OAAO;IACP,KAAK,MAAM,CAAC,MAAM,YAAY,QAAQ,OAAO,oBAAoB,MAAM,OAAO;IAC9E,IAAI,UAAU,QAAQ,QAAQ;GAChC;EACF;EACA,aAAa,YAAY;GACvB,OAAO;EACT;EACA,eAAe;EACf,gBAAgB;EAChB,aAAa,OAAO,MAAM;EAC1B,MAAM,YAAY;GAChB,MAAM,OAAO,KAAK;EACpB;EACA,cAAc;GACZ,IAAI,CAAC,OAAO;GACZ,OAAO,EAAE,QAAQ,UAAU,CAAC;GAC5B,MAAM,KAAK;EACb;EACA,mBAAmB,YAAY;GAC7B,MAAM,MAAM,oBAAoB;EAClC;EACA,IAAI,OAAO;GACT,OAAO;EACT;EACA;EACA,cAAc,aAAa,OAAO,EAAE,SAAS,CAAC;EAC9C,UAAU,SAAS;GACjB,OAAO;IACL,GAAG;IACH,GAAG;IACH,UAAU,KAAK,WAAW,aAAa,KAAK,QAAQ,IAAI,KAAK;GAC/D;GACA,OAAO,CAAC,CAAC;EACX;EACA,WAAW,UAAU;GACnB,IAAI,OAAO,MAAM,QAAQ;EAC3B;EACA,aAAa,SAAS;GACpB,UAAU;IAAE,GAAG;IAAS,GAAG;GAAK;EAClC;EACA,UAAU,SAAS;GACjB,IAAI,OAAO,MAAM,eAAe;EAClC;EACA,OAAO,WAAW;GAChB,IAAI,CAAC,OAAO;GACZ,MAAM,OAAO,UAAU;GACvB,UAAU,KAAA;GACV,MAAM,MAAM;GACZ,MAAM,QAAQ,OAAO,SAAS,MAAM,QAAQ,IAAI,MAAM,WAAW,OAAO;GACxE,MAAM,cAAc,YAAY,MAAM,QAAQ,OAAO,KAAK,GAAG;GAC7D,OAAO,EAAE,MAAM,MAAM,YAAY,CAAC;EACpC;EACA,YAAY,aAAa;GACvB,UAAU,IAAI,QAAQ;GACtB,aAAa;IACX,UAAU,OAAO,QAAQ;GAC3B;EACF;EACA,cAAc;GACZ,IAAI,CAAC,OAAO;GACZ,IAAI,CAAC,MAAM,QAAQ,OAAO,MAAM,MAAM;GACtC,IAAI,MAAM,GAAG,MAAM,cAAc;GACjC,MAAW,KAAK;EAClB;EACA,wBAAwB;GACtB,IAAI,OAAO,aAAa,eAAe,SAAS,mBAAmB;IACjE,SAAc,iBAAiB;IAC/B;GACF;GACA,MAAW,oBAAoB;EACjC;EACA,kBAAkB;GAChB,IAAI,OAAO,MAAM,QAAQ,CAAC,MAAM;EAClC;EACA,IAAI,QAAQ;GACV,OAAO;EACT;CAGS;AACb"}
|