@therealtinhtute/baroiplayer 1.0.0
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 +384 -0
- package/dist/context/media.js +2 -0
- package/dist/context/media.js.map +1 -0
- package/dist/context/media.mjs +15 -0
- package/dist/context/media.mjs.map +1 -0
- package/dist/context/store.js +2 -0
- package/dist/context/store.js.map +1 -0
- package/dist/context/store.mjs +5 -0
- package/dist/context/store.mjs.map +1 -0
- package/dist/context-BMteAJDN.js +1140 -0
- package/dist/context-BMteAJDN.js.map +1 -0
- package/dist/context-DCJ_scmO.cjs +23 -0
- package/dist/context-DCJ_scmO.cjs.map +1 -0
- package/dist/core-events-B5W_j8yU.js +480 -0
- package/dist/core-events-B5W_j8yU.js.map +1 -0
- package/dist/core-events-CN0-Vhhq.cjs +2 -0
- package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
- package/dist/core-plugins-Bq9KLany.js +696 -0
- package/dist/core-plugins-Bq9KLany.js.map +1 -0
- package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
- package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
- package/dist/core.js +37 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +1217 -0
- package/dist/core.mjs.map +1 -0
- package/dist/enhanced-components-DHv0MFvG.js +1073 -0
- package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
- package/dist/enhanced-components-DTIObgB7.cjs +2 -0
- package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
- package/dist/hooks-C_r8wexn.cjs +4 -0
- package/dist/hooks-C_r8wexn.cjs.map +1 -0
- package/dist/hooks-Da2wVXlF.js +1630 -0
- package/dist/hooks-Da2wVXlF.js.map +1 -0
- package/dist/index.css +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6126 -0
- package/dist/index.mjs.map +1 -0
- package/dist/minimal-components-DSq5sQpy.js +2002 -0
- package/dist/minimal-components-DSq5sQpy.js.map +1 -0
- package/dist/minimal-components-DflWiihN.cjs +2 -0
- package/dist/minimal-components-DflWiihN.cjs.map +1 -0
- package/dist/minimal-components.css +1 -0
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
- package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
- package/dist/performance-monitor-rMW1RgV3.js +302 -0
- package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
- package/dist/utils-BnscpYYd.js +2798 -0
- package/dist/utils-BnscpYYd.js.map +1 -0
- package/dist/utils-D_czS_0K.cjs +9 -0
- package/dist/utils-D_czS_0K.cjs.map +1 -0
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +114 -0
- package/src/adapters/videojs-adapter.tsx +470 -0
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +115 -0
- package/src/components/LoadingState.tsx +248 -0
- package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
- package/src/components/audio-player.tsx +85 -0
- package/src/components/basic/index.ts +37 -0
- package/src/components/enhanced/advanced-features.tsx +316 -0
- package/src/components/enhanced/debug-panel.tsx +422 -0
- package/src/components/enhanced/enhanced-controls.tsx +600 -0
- package/src/components/enhanced/enhanced-media-player.tsx +509 -0
- package/src/components/enhanced/error-handling.tsx +345 -0
- package/src/components/enhanced/index.ts +39 -0
- package/src/components/enhanced/media-player-enhanced.tsx +349 -0
- package/src/components/examples/basic-video-player.tsx +55 -0
- package/src/components/examples/custom-controls-example.tsx +63 -0
- package/src/components/examples/enhanced-controls-example.tsx +114 -0
- package/src/components/examples/enhanced-media-player-example.tsx +293 -0
- package/src/components/examples/loading-states-demo.tsx +142 -0
- package/src/components/examples/react-19-demo.tsx +381 -0
- package/src/components/examples/volume-orientation-demo.tsx +114 -0
- package/src/components/icon-demo.tsx +209 -0
- package/src/components/icons/index.tsx +166 -0
- package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
- package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
- package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
- package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
- package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
- package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
- package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
- package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
- package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
- package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
- package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
- package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
- package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
- package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
- package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
- package/src/components/legacy.tsx +93 -0
- package/src/components/media-metadata-display.tsx +100 -0
- package/src/components/media-player-audio.tsx +39 -0
- package/src/components/media-player-controls.tsx +226 -0
- package/src/components/media-player-error.tsx +163 -0
- package/src/components/media-player-fullscreen.tsx +109 -0
- package/src/components/media-player-hls.tsx +111 -0
- package/src/components/media-player-loading.tsx +76 -0
- package/src/components/media-player-play.tsx +75 -0
- package/src/components/media-player-root.tsx +340 -0
- package/src/components/media-player-seek.tsx +168 -0
- package/src/components/media-player-time.tsx +99 -0
- package/src/components/media-player-tooltip.tsx +58 -0
- package/src/components/media-player-video.tsx +39 -0
- package/src/components/media-player-volume.tsx +155 -0
- package/src/components/media-player.tsx +257 -0
- package/src/components/media-suspense.tsx +362 -0
- package/src/components/player-settings-form.tsx +163 -0
- package/src/components/react-player-wrapper.tsx +651 -0
- package/src/components/simple-video-player.tsx +60 -0
- package/src/components/ui/enhanced-seek-bar.tsx +235 -0
- package/src/components/ui/fullscreen-button.tsx +60 -0
- package/src/components/ui/media-resource-preloader.tsx +395 -0
- package/src/components/ui/play-button.tsx +107 -0
- package/src/components/ui/seek-bar.tsx +161 -0
- package/src/components/ui/time-display.tsx +40 -0
- package/src/components/ui/volume-control.tsx +143 -0
- package/src/components/unified/base-media-player.tsx +246 -0
- package/src/components/unified/media-player.tsx +75 -0
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
- package/src/components/variants/minimal/MinimalControls.tsx +142 -0
- package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
- package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
- package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
- package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
- package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
- package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
- package/src/components/variants/minimal/components/RightControls.tsx +173 -0
- package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
- package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
- package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
- package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
- package/src/components/variants/minimal/index.ts +23 -0
- package/src/components/video-player.tsx +100 -0
- package/src/context/error-handling.ts +394 -0
- package/src/context/media-context.tsx +208 -0
- package/src/context/media-provider.tsx +265 -0
- package/src/context/media-store.ts +379 -0
- package/src/context/persistence.ts +453 -0
- package/src/core/events/event-system.ts +442 -0
- package/src/core/events/hooks.ts +302 -0
- package/src/core/events/index.ts +22 -0
- package/src/core/events/middleware.ts +269 -0
- package/src/core/index.ts +22 -0
- package/src/core/optimization/bundle-optimizer.tsx +308 -0
- package/src/core/performance/cicd.ts +96 -0
- package/src/core/performance/index.ts +38 -0
- package/src/core/performance/media-loading-optimizer.ts +538 -0
- package/src/core/performance/performance-budgets.ts +432 -0
- package/src/core/performance/performance-monitor.tsx +476 -0
- package/src/core/performance/performance-testing.ts +459 -0
- package/src/core/plugins/base-plugin.ts +224 -0
- package/src/core/plugins/enhanced-plugin-system.ts +411 -0
- package/src/core/plugins/examples/analytics-plugin.ts +242 -0
- package/src/core/plugins/index.ts +39 -0
- package/src/core/plugins/registry.ts +327 -0
- package/src/core/plugins/types.ts +159 -0
- package/src/core/plugins/utils.ts +316 -0
- package/src/core/progressive-loading.tsx +322 -0
- package/src/core/providers/enhanced-hls-provider.ts +339 -0
- package/src/core/providers/service-provider.tsx +168 -0
- package/src/core/utils.tsx +38 -0
- package/src/features/media-session/media-session-manager.ts +429 -0
- package/src/features/picture-in-picture/pip-manager.ts +429 -0
- package/src/features/playlist/playlist-manager.ts +606 -0
- package/src/features/subtitles/subtitle-manager.ts +441 -0
- package/src/features/subtitles/subtitle-parser.ts +280 -0
- package/src/features/thumbnails/thumbnail-generator.ts +519 -0
- package/src/hooks/accessibility/index.ts +27 -0
- package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
- package/src/hooks/accessibility/use-focus-management.ts +176 -0
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
- package/src/hooks/accessibility/use-screen-reader.ts +182 -0
- package/src/hooks/core/index.ts +47 -0
- package/src/hooks/legacy.ts +27 -0
- package/src/hooks/use-enhanced-media-player.ts +468 -0
- package/src/hooks/use-intelligent-preloading.ts +358 -0
- package/src/hooks/use-keyboard-controls.ts +115 -0
- package/src/hooks/use-media-actions.ts +175 -0
- package/src/hooks/use-media-metadata.ts +89 -0
- package/src/hooks/use-media-optimistic.ts +270 -0
- package/src/hooks/use-media-player.ts +312 -0
- package/src/hooks/use-media-state.ts +190 -0
- package/src/hooks/use-memory-optimization.ts +358 -0
- package/src/hooks/use-optimistic-controls.ts +92 -0
- package/src/hooks/use-optimized-media.ts +263 -0
- package/src/hooks/use-touch-gestures.ts +417 -0
- package/src/hooks/useEnhancedSubtitles.ts +258 -0
- package/src/hooks/useKeyboardShortcuts.ts +74 -0
- package/src/hooks/useMediaControls.ts +52 -0
- package/src/hooks/useScreenshot.ts +57 -0
- package/src/hooks/useSubtitles.ts +91 -0
- package/src/hooks/useVideoPlayer.ts +232 -0
- package/src/icons/components/FullscreenIcon.tsx +37 -0
- package/src/icons/components/SettingsIcon.tsx +38 -0
- package/src/icons/components/animated/LoadingIcon.tsx +43 -0
- package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
- package/src/icons/components/media/PauseIcon.tsx +39 -0
- package/src/icons/components/media/PlayIcon.tsx +38 -0
- package/src/icons/components/volume/VolumeIcon.tsx +39 -0
- package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
- package/src/icons/index.ts +56 -0
- package/src/icons/provider.tsx +64 -0
- package/src/icons/types.ts +40 -0
- package/src/index-enhanced.ts +35 -0
- package/src/index-legacy.ts +326 -0
- package/src/index.css +102 -0
- package/src/index.ts +216 -0
- package/src/lib/deprecation.ts +51 -0
- package/src/lib/format-time.ts +37 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/globals.css +462 -0
- package/src/styles/minimal-player.css +589 -0
- package/src/styles/pixel-art.css +46 -0
- package/src/styles/videojs.css +100 -0
- package/src/test/setup.ts +79 -0
- package/src/types/events.ts +47 -0
- package/src/types/formats.ts +95 -0
- package/src/types/index.ts +190 -0
- package/src/types/videojs-plugins.d.ts +14 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { useMediaSelector, useMediaDispatch, MediaActionTypes } from "../context/media-context"
|
|
5
|
+
|
|
6
|
+
// Optimized hook for media state with memoized selectors
|
|
7
|
+
export function useOptimizedMediaState() {
|
|
8
|
+
// Memoized selectors to prevent unnecessary re-renders
|
|
9
|
+
const isPlaying = useMediaSelector(React.useCallback(state => state.isPlaying, []))
|
|
10
|
+
const currentTime = useMediaSelector(React.useCallback(state => state.currentTime, []))
|
|
11
|
+
const duration = useMediaSelector(React.useCallback(state => state.duration, []))
|
|
12
|
+
const volume = useMediaSelector(React.useCallback(state => state.volume, []))
|
|
13
|
+
const isMuted = useMediaSelector(React.useCallback(state => state.isMuted, []))
|
|
14
|
+
const isLoading = useMediaSelector(React.useCallback(state => state.isLoading, []))
|
|
15
|
+
const error = useMediaSelector(React.useCallback(state => state.error, []))
|
|
16
|
+
const isFullscreen = useMediaSelector(React.useCallback(state => state.isFullscreen, []))
|
|
17
|
+
const isPictureInPicture = useMediaSelector(React.useCallback(state => state.isPictureInPicture, []))
|
|
18
|
+
const buffered = useMediaSelector(React.useCallback(state => state.buffered, []))
|
|
19
|
+
const playbackRate = useMediaSelector(React.useCallback(state => state.playbackRate, []))
|
|
20
|
+
|
|
21
|
+
// Derived state with memoization
|
|
22
|
+
const progress = React.useMemo(() => {
|
|
23
|
+
return duration > 0 ? (currentTime / duration) * 100 : 0
|
|
24
|
+
}, [currentTime, duration])
|
|
25
|
+
|
|
26
|
+
const remainingTime = React.useMemo(() => {
|
|
27
|
+
return Math.max(0, duration - currentTime)
|
|
28
|
+
}, [duration, currentTime])
|
|
29
|
+
|
|
30
|
+
const bufferedRanges = React.useMemo(() => {
|
|
31
|
+
if (!buffered || duration === 0) return []
|
|
32
|
+
const ranges = []
|
|
33
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
34
|
+
ranges.push({
|
|
35
|
+
start: (buffered.start(i) / duration) * 100,
|
|
36
|
+
end: (buffered.end(i) / duration) * 100
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
return ranges
|
|
40
|
+
}, [buffered, duration])
|
|
41
|
+
|
|
42
|
+
const canPlay = React.useMemo(() => {
|
|
43
|
+
return !isLoading && !error && duration > 0
|
|
44
|
+
}, [isLoading, error, duration])
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
// Basic state
|
|
48
|
+
isPlaying,
|
|
49
|
+
currentTime,
|
|
50
|
+
duration,
|
|
51
|
+
volume,
|
|
52
|
+
isMuted,
|
|
53
|
+
isLoading,
|
|
54
|
+
error,
|
|
55
|
+
isFullscreen,
|
|
56
|
+
isPictureInPicture,
|
|
57
|
+
buffered,
|
|
58
|
+
playbackRate,
|
|
59
|
+
|
|
60
|
+
// Derived state
|
|
61
|
+
progress,
|
|
62
|
+
remainingTime,
|
|
63
|
+
bufferedRanges,
|
|
64
|
+
canPlay
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Optimized hook for media controls with memoized callbacks
|
|
69
|
+
export function useOptimizedMediaControls() {
|
|
70
|
+
const dispatch = useMediaDispatch()
|
|
71
|
+
|
|
72
|
+
// Memoized control functions
|
|
73
|
+
const play = React.useCallback(() => {
|
|
74
|
+
dispatch({ type: MediaActionTypes.PLAY })
|
|
75
|
+
}, [dispatch])
|
|
76
|
+
|
|
77
|
+
const pause = React.useCallback(() => {
|
|
78
|
+
dispatch({ type: MediaActionTypes.PAUSE })
|
|
79
|
+
}, [dispatch])
|
|
80
|
+
|
|
81
|
+
const toggle = React.useCallback(() => {
|
|
82
|
+
dispatch({ type: MediaActionTypes.PLAY })
|
|
83
|
+
}, [dispatch])
|
|
84
|
+
|
|
85
|
+
const seek = React.useCallback((time: number) => {
|
|
86
|
+
dispatch({ type: MediaActionTypes.SEEK, detail: time })
|
|
87
|
+
}, [dispatch])
|
|
88
|
+
|
|
89
|
+
const setVolume = React.useCallback((volume: number) => {
|
|
90
|
+
dispatch({ type: MediaActionTypes.SET_VOLUME, detail: volume })
|
|
91
|
+
}, [dispatch])
|
|
92
|
+
|
|
93
|
+
const mute = React.useCallback(() => {
|
|
94
|
+
dispatch({ type: MediaActionTypes.MUTE })
|
|
95
|
+
}, [dispatch])
|
|
96
|
+
|
|
97
|
+
const unmute = React.useCallback(() => {
|
|
98
|
+
dispatch({ type: MediaActionTypes.UNMUTE })
|
|
99
|
+
}, [dispatch])
|
|
100
|
+
|
|
101
|
+
const toggleMute = React.useCallback(() => {
|
|
102
|
+
dispatch({ type: MediaActionTypes.TOGGLE_MUTE })
|
|
103
|
+
}, [dispatch])
|
|
104
|
+
|
|
105
|
+
const enterFullscreen = React.useCallback(() => {
|
|
106
|
+
dispatch({ type: MediaActionTypes.ENTER_FULLSCREEN })
|
|
107
|
+
}, [dispatch])
|
|
108
|
+
|
|
109
|
+
const exitFullscreen = React.useCallback(() => {
|
|
110
|
+
dispatch({ type: MediaActionTypes.EXIT_FULLSCREEN })
|
|
111
|
+
}, [dispatch])
|
|
112
|
+
|
|
113
|
+
const toggleFullscreen = React.useCallback(() => {
|
|
114
|
+
dispatch({ type: MediaActionTypes.ENTER_FULLSCREEN })
|
|
115
|
+
}, [dispatch])
|
|
116
|
+
|
|
117
|
+
const enterPictureInPicture = React.useCallback(() => {
|
|
118
|
+
dispatch({ type: MediaActionTypes.ENTER_PIP })
|
|
119
|
+
}, [dispatch])
|
|
120
|
+
|
|
121
|
+
const exitPictureInPicture = React.useCallback(() => {
|
|
122
|
+
dispatch({ type: MediaActionTypes.EXIT_PIP })
|
|
123
|
+
}, [dispatch])
|
|
124
|
+
|
|
125
|
+
const togglePictureInPicture = React.useCallback(() => {
|
|
126
|
+
dispatch({ type: MediaActionTypes.ENTER_PIP })
|
|
127
|
+
}, [dispatch])
|
|
128
|
+
|
|
129
|
+
const setPlaybackRate = React.useCallback((rate: number) => {
|
|
130
|
+
dispatch({ type: MediaActionTypes.SET_PLAYBACK_RATE, detail: rate })
|
|
131
|
+
}, [dispatch])
|
|
132
|
+
|
|
133
|
+
const skipForward = React.useCallback((seconds: number = 10) => {
|
|
134
|
+
dispatch({ type: MediaActionTypes.SEEK, detail: seconds })
|
|
135
|
+
}, [dispatch])
|
|
136
|
+
|
|
137
|
+
const skipBackward = React.useCallback((seconds: number = 10) => {
|
|
138
|
+
dispatch({ type: MediaActionTypes.SEEK, detail: -seconds })
|
|
139
|
+
}, [dispatch])
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
play,
|
|
143
|
+
pause,
|
|
144
|
+
toggle,
|
|
145
|
+
seek,
|
|
146
|
+
setVolume,
|
|
147
|
+
mute,
|
|
148
|
+
unmute,
|
|
149
|
+
toggleMute,
|
|
150
|
+
enterFullscreen,
|
|
151
|
+
exitFullscreen,
|
|
152
|
+
toggleFullscreen,
|
|
153
|
+
enterPictureInPicture,
|
|
154
|
+
exitPictureInPicture,
|
|
155
|
+
togglePictureInPicture,
|
|
156
|
+
setPlaybackRate,
|
|
157
|
+
skipForward,
|
|
158
|
+
skipBackward
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Hook for debounced seek operations
|
|
163
|
+
export function useDebouncedSeek(delay: number = 100) {
|
|
164
|
+
const dispatch = useMediaDispatch()
|
|
165
|
+
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
166
|
+
|
|
167
|
+
const debouncedSeek = React.useCallback((time: number) => {
|
|
168
|
+
if (timeoutRef.current) {
|
|
169
|
+
clearTimeout(timeoutRef.current)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
timeoutRef.current = setTimeout(() => {
|
|
173
|
+
dispatch({ type: MediaActionTypes.SEEK, detail: time })
|
|
174
|
+
}, delay)
|
|
175
|
+
}, [dispatch, delay])
|
|
176
|
+
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
return () => {
|
|
179
|
+
if (timeoutRef.current) {
|
|
180
|
+
clearTimeout(timeoutRef.current)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, [])
|
|
184
|
+
|
|
185
|
+
return debouncedSeek
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Hook for throttled volume changes
|
|
189
|
+
export function useThrottledVolume(delay: number = 50) {
|
|
190
|
+
const dispatch = useMediaDispatch()
|
|
191
|
+
const lastUpdateRef = React.useRef<number>(0)
|
|
192
|
+
|
|
193
|
+
const throttledSetVolume = React.useCallback((volume: number) => {
|
|
194
|
+
const now = Date.now()
|
|
195
|
+
if (now - lastUpdateRef.current >= delay) {
|
|
196
|
+
dispatch({ type: MediaActionTypes.SET_VOLUME, detail: volume })
|
|
197
|
+
lastUpdateRef.current = now
|
|
198
|
+
}
|
|
199
|
+
}, [dispatch, delay])
|
|
200
|
+
|
|
201
|
+
return throttledSetVolume
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Hook for optimized time formatting
|
|
205
|
+
export function useFormattedTime(time: number) {
|
|
206
|
+
return React.useMemo(() => {
|
|
207
|
+
const hours = Math.floor(time / 3600)
|
|
208
|
+
const minutes = Math.floor((time % 3600) / 60)
|
|
209
|
+
const seconds = Math.floor(time % 60)
|
|
210
|
+
|
|
211
|
+
if (hours > 0) {
|
|
212
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
213
|
+
}
|
|
214
|
+
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
215
|
+
}, [time])
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Hook for intersection observer (for lazy loading)
|
|
219
|
+
export function useIntersectionObserver(
|
|
220
|
+
elementRef: React.RefObject<Element>,
|
|
221
|
+
options?: IntersectionObserverInit
|
|
222
|
+
) {
|
|
223
|
+
const [isIntersecting, setIsIntersecting] = React.useState(false)
|
|
224
|
+
|
|
225
|
+
React.useEffect(() => {
|
|
226
|
+
const element = elementRef.current
|
|
227
|
+
if (!element) return
|
|
228
|
+
|
|
229
|
+
const observer = new IntersectionObserver(
|
|
230
|
+
([entry]) => {
|
|
231
|
+
setIsIntersecting(entry?.isIntersecting ?? false)
|
|
232
|
+
},
|
|
233
|
+
options
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
observer.observe(element)
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
observer.unobserve(element)
|
|
240
|
+
}
|
|
241
|
+
}, [elementRef, options])
|
|
242
|
+
|
|
243
|
+
return isIntersecting
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Hook for media queries (responsive behavior)
|
|
247
|
+
export function useMediaQuery(query: string) {
|
|
248
|
+
const [matches, setMatches] = React.useState(false)
|
|
249
|
+
|
|
250
|
+
React.useEffect(() => {
|
|
251
|
+
const mediaQuery = window.matchMedia(query)
|
|
252
|
+
setMatches(mediaQuery.matches)
|
|
253
|
+
|
|
254
|
+
const handler = (event: MediaQueryListEvent) => {
|
|
255
|
+
setMatches(event.matches)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
mediaQuery.addEventListener('change', handler)
|
|
259
|
+
return () => mediaQuery.removeEventListener('change', handler)
|
|
260
|
+
}, [query])
|
|
261
|
+
|
|
262
|
+
return matches
|
|
263
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useDeviceInfo, useHapticFeedback } from "@rp/ui/hooks/use-mobile"
|
|
5
|
+
|
|
6
|
+
// Gesture types
|
|
7
|
+
export type GestureType =
|
|
8
|
+
| 'tap'
|
|
9
|
+
| 'double-tap'
|
|
10
|
+
| 'long-press'
|
|
11
|
+
| 'swipe-left'
|
|
12
|
+
| 'swipe-right'
|
|
13
|
+
| 'swipe-up'
|
|
14
|
+
| 'swipe-down'
|
|
15
|
+
| 'pinch-in'
|
|
16
|
+
| 'pinch-out'
|
|
17
|
+
|
|
18
|
+
// Gesture event data
|
|
19
|
+
export interface GestureEvent {
|
|
20
|
+
type: GestureType
|
|
21
|
+
startPoint: { x: number; y: number }
|
|
22
|
+
currentPoint: { x: number; y: number }
|
|
23
|
+
velocity?: { x: number; y: number }
|
|
24
|
+
scale?: number
|
|
25
|
+
rotation?: number
|
|
26
|
+
distance?: number
|
|
27
|
+
duration: number
|
|
28
|
+
preventDefault: () => void
|
|
29
|
+
target: HTMLElement
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Gesture configuration
|
|
33
|
+
export interface GestureConfig {
|
|
34
|
+
enabledGestures?: GestureType[]
|
|
35
|
+
tapThreshold?: number // ms
|
|
36
|
+
doubleTapThreshold?: number // ms
|
|
37
|
+
longPressThreshold?: number // ms
|
|
38
|
+
swipeThreshold?: number // px
|
|
39
|
+
swipeVelocityThreshold?: number // px/ms
|
|
40
|
+
pinchThreshold?: number // scale difference
|
|
41
|
+
rotationThreshold?: number // degrees
|
|
42
|
+
enableHapticFeedback?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Default configuration
|
|
46
|
+
const defaultConfig: Required<GestureConfig> = {
|
|
47
|
+
enabledGestures: ['tap', 'double-tap', 'long-press', 'swipe-left', 'swipe-right', 'swipe-up', 'swipe-down', 'pinch-in', 'pinch-out'],
|
|
48
|
+
tapThreshold: 200,
|
|
49
|
+
doubleTapThreshold: 300,
|
|
50
|
+
longPressThreshold: 500,
|
|
51
|
+
swipeThreshold: 50,
|
|
52
|
+
swipeVelocityThreshold: 0.5,
|
|
53
|
+
pinchThreshold: 0.1,
|
|
54
|
+
rotationThreshold: 15,
|
|
55
|
+
enableHapticFeedback: true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Touch point interface
|
|
59
|
+
interface TouchPoint {
|
|
60
|
+
id: number
|
|
61
|
+
x: number
|
|
62
|
+
y: number
|
|
63
|
+
timestamp: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Gesture state
|
|
67
|
+
interface GestureState {
|
|
68
|
+
isTracking: boolean
|
|
69
|
+
touches: TouchPoint[]
|
|
70
|
+
startTouches: TouchPoint[]
|
|
71
|
+
lastTap?: { timestamp: number; point: { x: number; y: number } }
|
|
72
|
+
longPressTimer?: NodeJS.Timeout
|
|
73
|
+
initialDistance?: number
|
|
74
|
+
initialScale?: number
|
|
75
|
+
lastScale?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Touch gesture hook
|
|
79
|
+
export function useTouchGestures(
|
|
80
|
+
ref: React.RefObject<HTMLElement>,
|
|
81
|
+
onGesture: (event: GestureEvent) => void,
|
|
82
|
+
config: GestureConfig = {}
|
|
83
|
+
) {
|
|
84
|
+
const mergedConfig = { ...defaultConfig, ...config }
|
|
85
|
+
const { isTouchDevice } = useDeviceInfo()
|
|
86
|
+
const { triggerHaptic } = useHapticFeedback()
|
|
87
|
+
const gestureState = React.useRef<GestureState>({
|
|
88
|
+
isTracking: false,
|
|
89
|
+
touches: [],
|
|
90
|
+
startTouches: []
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Helper functions
|
|
94
|
+
const getTouchPoint = (touch: Touch): TouchPoint => ({
|
|
95
|
+
id: touch.identifier,
|
|
96
|
+
x: touch.clientX,
|
|
97
|
+
y: touch.clientY,
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const getDistance = (p1: TouchPoint, p2: TouchPoint): number =>
|
|
102
|
+
Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
|
|
103
|
+
|
|
104
|
+
const getVelocity = (start: TouchPoint, end: TouchPoint): { x: number; y: number } => {
|
|
105
|
+
const timeDiff = Math.max(end.timestamp - start.timestamp, 1)
|
|
106
|
+
return {
|
|
107
|
+
x: (end.x - start.x) / timeDiff,
|
|
108
|
+
y: (end.y - start.y) / timeDiff
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const createGestureEvent = (
|
|
113
|
+
type: GestureType,
|
|
114
|
+
startPoint: TouchPoint,
|
|
115
|
+
currentPoint: TouchPoint,
|
|
116
|
+
additionalData: Partial<GestureEvent> = {}
|
|
117
|
+
): GestureEvent => ({
|
|
118
|
+
type,
|
|
119
|
+
startPoint: { x: startPoint.x, y: startPoint.y },
|
|
120
|
+
currentPoint: { x: currentPoint.x, y: currentPoint.y },
|
|
121
|
+
duration: currentPoint.timestamp - startPoint.timestamp,
|
|
122
|
+
target: ref.current!,
|
|
123
|
+
preventDefault: () => {},
|
|
124
|
+
...additionalData
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Handle touch start
|
|
128
|
+
const handleTouchStart = React.useCallback((e: TouchEvent) => {
|
|
129
|
+
if (!mergedConfig.enabledGestures.length) return
|
|
130
|
+
|
|
131
|
+
const touches = Array.from(e.touches).map(getTouchPoint)
|
|
132
|
+
const state = gestureState.current
|
|
133
|
+
|
|
134
|
+
state.isTracking = true
|
|
135
|
+
state.touches = touches
|
|
136
|
+
state.startTouches = [...touches]
|
|
137
|
+
|
|
138
|
+
// Clear any existing long press timer
|
|
139
|
+
if (state.longPressTimer) {
|
|
140
|
+
clearTimeout(state.longPressTimer)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Set up long press detection for single touch
|
|
144
|
+
if (touches.length === 1 && mergedConfig.enabledGestures.includes('long-press')) {
|
|
145
|
+
state.longPressTimer = setTimeout(() => {
|
|
146
|
+
if (state.touches.length === 1 && touches[0]) {
|
|
147
|
+
const gestureEvent = createGestureEvent('long-press', touches[0], touches[0])
|
|
148
|
+
onGesture(gestureEvent)
|
|
149
|
+
|
|
150
|
+
if (mergedConfig.enableHapticFeedback) {
|
|
151
|
+
triggerHaptic('heavy')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, mergedConfig.longPressThreshold)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle multi-touch gestures
|
|
158
|
+
if (touches.length === 2 && touches[0] && touches[1]) {
|
|
159
|
+
state.initialDistance = getDistance(touches[0], touches[1])
|
|
160
|
+
state.initialScale = 1
|
|
161
|
+
state.lastScale = 1
|
|
162
|
+
}
|
|
163
|
+
}, [mergedConfig, onGesture, triggerHaptic])
|
|
164
|
+
|
|
165
|
+
// Handle touch move
|
|
166
|
+
const handleTouchMove = React.useCallback((e: TouchEvent) => {
|
|
167
|
+
const state = gestureState.current
|
|
168
|
+
if (!state.isTracking) return
|
|
169
|
+
|
|
170
|
+
const touches = Array.from(e.touches).map(getTouchPoint)
|
|
171
|
+
state.touches = touches
|
|
172
|
+
|
|
173
|
+
// Handle pinch gesture
|
|
174
|
+
if (touches.length === 2 && state.initialDistance && touches[0] && touches[1]) {
|
|
175
|
+
const currentDistance = getDistance(touches[0], touches[1])
|
|
176
|
+
const scale = currentDistance / state.initialDistance
|
|
177
|
+
const scaleDiff = Math.abs(scale - (state.lastScale || 1))
|
|
178
|
+
|
|
179
|
+
if (scaleDiff > mergedConfig.pinchThreshold) {
|
|
180
|
+
const gestureType = scale > (state.lastScale || 1) ? 'pinch-out' : 'pinch-in'
|
|
181
|
+
|
|
182
|
+
if (mergedConfig.enabledGestures.includes(gestureType)) {
|
|
183
|
+
const centerPoint = {
|
|
184
|
+
x: (touches[0].x + touches[1].x) / 2,
|
|
185
|
+
y: (touches[0].y + touches[1].y) / 2,
|
|
186
|
+
id: -1,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const startCenterPoint = {
|
|
191
|
+
x: ((state.startTouches[0]?.x ?? 0) + (state.startTouches[1]?.x ?? 0)) / 2,
|
|
192
|
+
y: ((state.startTouches[0]?.y ?? 0) + (state.startTouches[1]?.y ?? 0)) / 2,
|
|
193
|
+
id: -1,
|
|
194
|
+
timestamp: state.startTouches[0]?.timestamp ?? Date.now()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const gestureEvent = createGestureEvent(gestureType, startCenterPoint, centerPoint, {
|
|
198
|
+
scale,
|
|
199
|
+
distance: currentDistance
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
onGesture(gestureEvent)
|
|
203
|
+
state.lastScale = scale
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Clear long press timer on move
|
|
209
|
+
if (state.longPressTimer && touches.length === 1 && state.startTouches[0] && touches[0]) {
|
|
210
|
+
const startTouch = state.startTouches[0]
|
|
211
|
+
const currentTouch = touches[0]
|
|
212
|
+
const distance = getDistance(startTouch, currentTouch)
|
|
213
|
+
|
|
214
|
+
if (distance > mergedConfig.swipeThreshold) {
|
|
215
|
+
clearTimeout(state.longPressTimer)
|
|
216
|
+
state.longPressTimer = undefined
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, [mergedConfig, onGesture])
|
|
220
|
+
|
|
221
|
+
// Handle touch end
|
|
222
|
+
const handleTouchEnd = React.useCallback((e: TouchEvent) => {
|
|
223
|
+
const state = gestureState.current
|
|
224
|
+
if (!state.isTracking) return
|
|
225
|
+
|
|
226
|
+
const endTouches = Array.from(e.changedTouches).map(getTouchPoint)
|
|
227
|
+
const currentTime = Date.now()
|
|
228
|
+
|
|
229
|
+
// Clear long press timer
|
|
230
|
+
if (state.longPressTimer) {
|
|
231
|
+
clearTimeout(state.longPressTimer)
|
|
232
|
+
state.longPressTimer = undefined
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Handle single touch gestures
|
|
236
|
+
if (state.startTouches.length === 1 && endTouches.length === 1 && state.startTouches[0] && endTouches[0]) {
|
|
237
|
+
const startTouch = state.startTouches[0]
|
|
238
|
+
const endTouch = endTouches[0]
|
|
239
|
+
const distance = getDistance(startTouch, endTouch)
|
|
240
|
+
const duration = endTouch.timestamp - startTouch.timestamp
|
|
241
|
+
|
|
242
|
+
// Check for swipe gestures
|
|
243
|
+
if (distance > mergedConfig.swipeThreshold) {
|
|
244
|
+
const velocity = getVelocity(startTouch, endTouch)
|
|
245
|
+
const absVelX = Math.abs(velocity.x)
|
|
246
|
+
const absVelY = Math.abs(velocity.y)
|
|
247
|
+
|
|
248
|
+
if (absVelX > mergedConfig.swipeVelocityThreshold || absVelY > mergedConfig.swipeVelocityThreshold) {
|
|
249
|
+
let gestureType: GestureType
|
|
250
|
+
|
|
251
|
+
if (absVelX > absVelY) {
|
|
252
|
+
gestureType = velocity.x > 0 ? 'swipe-right' : 'swipe-left'
|
|
253
|
+
} else {
|
|
254
|
+
gestureType = velocity.y > 0 ? 'swipe-down' : 'swipe-up'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (mergedConfig.enabledGestures.includes(gestureType)) {
|
|
258
|
+
const gestureEvent = createGestureEvent(gestureType, startTouch, endTouch, {
|
|
259
|
+
velocity,
|
|
260
|
+
distance
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
onGesture(gestureEvent)
|
|
264
|
+
|
|
265
|
+
if (mergedConfig.enableHapticFeedback) {
|
|
266
|
+
triggerHaptic('light')
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Check for tap gestures
|
|
272
|
+
else if (duration < mergedConfig.tapThreshold && distance < mergedConfig.swipeThreshold) {
|
|
273
|
+
// Check for double tap
|
|
274
|
+
if (state.lastTap &&
|
|
275
|
+
currentTime - state.lastTap.timestamp < mergedConfig.doubleTapThreshold &&
|
|
276
|
+
getDistance({...state.lastTap.point, id: -1, timestamp: state.lastTap.timestamp}, endTouch) < mergedConfig.swipeThreshold) {
|
|
277
|
+
|
|
278
|
+
if (mergedConfig.enabledGestures.includes('double-tap')) {
|
|
279
|
+
const gestureEvent = createGestureEvent('double-tap', startTouch, endTouch)
|
|
280
|
+
onGesture(gestureEvent)
|
|
281
|
+
|
|
282
|
+
if (mergedConfig.enableHapticFeedback) {
|
|
283
|
+
triggerHaptic('medium')
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
state.lastTap = undefined
|
|
288
|
+
} else {
|
|
289
|
+
// Single tap
|
|
290
|
+
if (mergedConfig.enabledGestures.includes('tap')) {
|
|
291
|
+
const gestureEvent = createGestureEvent('tap', startTouch, endTouch)
|
|
292
|
+
onGesture(gestureEvent)
|
|
293
|
+
|
|
294
|
+
if (mergedConfig.enableHapticFeedback) {
|
|
295
|
+
triggerHaptic('light')
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
state.lastTap = {
|
|
300
|
+
timestamp: currentTime,
|
|
301
|
+
point: { x: endTouch.x, y: endTouch.y }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Reset gesture state
|
|
308
|
+
if (e.touches.length === 0) {
|
|
309
|
+
state.isTracking = false
|
|
310
|
+
state.touches = []
|
|
311
|
+
state.startTouches = []
|
|
312
|
+
state.initialDistance = undefined
|
|
313
|
+
state.initialScale = undefined
|
|
314
|
+
state.lastScale = undefined
|
|
315
|
+
}
|
|
316
|
+
}, [mergedConfig, onGesture, triggerHaptic])
|
|
317
|
+
|
|
318
|
+
// Attach event listeners
|
|
319
|
+
React.useEffect(() => {
|
|
320
|
+
const element = ref.current
|
|
321
|
+
if (!element || !isTouchDevice) return
|
|
322
|
+
|
|
323
|
+
element.addEventListener('touchstart', handleTouchStart, { passive: false })
|
|
324
|
+
element.addEventListener('touchmove', handleTouchMove, { passive: false })
|
|
325
|
+
element.addEventListener('touchend', handleTouchEnd, { passive: false })
|
|
326
|
+
element.addEventListener('touchcancel', handleTouchEnd, { passive: false })
|
|
327
|
+
|
|
328
|
+
return () => {
|
|
329
|
+
element.removeEventListener('touchstart', handleTouchStart)
|
|
330
|
+
element.removeEventListener('touchmove', handleTouchMove)
|
|
331
|
+
element.removeEventListener('touchend', handleTouchEnd)
|
|
332
|
+
element.removeEventListener('touchcancel', handleTouchEnd)
|
|
333
|
+
}
|
|
334
|
+
}, [handleTouchStart, handleTouchMove, handleTouchEnd, isTouchDevice])
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
isGestureEnabled: isTouchDevice && mergedConfig.enabledGestures.length > 0
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Media player specific gesture hook with predefined actions
|
|
342
|
+
export function useMediaGestures(
|
|
343
|
+
ref: React.RefObject<HTMLElement>,
|
|
344
|
+
actions: {
|
|
345
|
+
onPlayPause?: () => void
|
|
346
|
+
onSeek?: (direction: 'forward' | 'backward', amount: number) => void
|
|
347
|
+
onVolumeChange?: (direction: 'up' | 'down', amount: number) => void
|
|
348
|
+
onToggleFullscreen?: () => void
|
|
349
|
+
onZoom?: (scale: number) => void
|
|
350
|
+
}
|
|
351
|
+
) {
|
|
352
|
+
const handleGesture = React.useCallback((event: GestureEvent) => {
|
|
353
|
+
const { type, velocity, distance, scale } = event
|
|
354
|
+
|
|
355
|
+
switch (type) {
|
|
356
|
+
case 'double-tap':
|
|
357
|
+
// Double tap to play/pause
|
|
358
|
+
actions.onPlayPause?.()
|
|
359
|
+
break
|
|
360
|
+
|
|
361
|
+
case 'swipe-left':
|
|
362
|
+
// Swipe left to seek backward
|
|
363
|
+
const backwardAmount = Math.min((distance || 0) / 10, 30) // Max 30 seconds
|
|
364
|
+
actions.onSeek?.('backward', backwardAmount)
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
case 'swipe-right':
|
|
368
|
+
// Swipe right to seek forward
|
|
369
|
+
const forwardAmount = Math.min((distance || 0) / 10, 30) // Max 30 seconds
|
|
370
|
+
actions.onSeek?.('forward', forwardAmount)
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
case 'swipe-up':
|
|
374
|
+
// Swipe up on right side for volume up
|
|
375
|
+
if (event.startPoint.x > (ref.current?.clientWidth || 0) / 2) {
|
|
376
|
+
const volumeAmount = Math.min((distance || 0) / 100, 0.2) // Max 20%
|
|
377
|
+
actions.onVolumeChange?.('up', volumeAmount)
|
|
378
|
+
}
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
case 'swipe-down':
|
|
382
|
+
// Swipe down on right side for volume down
|
|
383
|
+
if (event.startPoint.x > (ref.current?.clientWidth || 0) / 2) {
|
|
384
|
+
const volumeAmount = Math.min((distance || 0) / 100, 0.2) // Max 20%
|
|
385
|
+
actions.onVolumeChange?.('down', volumeAmount)
|
|
386
|
+
}
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
case 'pinch-out':
|
|
390
|
+
// Pinch out to zoom in (for video zoom functionality)
|
|
391
|
+
actions.onZoom?.(scale || 1)
|
|
392
|
+
break
|
|
393
|
+
|
|
394
|
+
case 'pinch-in':
|
|
395
|
+
// Pinch in to zoom out
|
|
396
|
+
actions.onZoom?.(scale || 1)
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
case 'long-press':
|
|
400
|
+
// Long press for fullscreen toggle
|
|
401
|
+
actions.onToggleFullscreen?.()
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
default:
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
}, [actions, ref])
|
|
408
|
+
|
|
409
|
+
const gestureConfig: GestureConfig = {
|
|
410
|
+
enabledGestures: ['tap', 'double-tap', 'long-press', 'swipe-left', 'swipe-right', 'swipe-up', 'swipe-down', 'pinch-in', 'pinch-out'],
|
|
411
|
+
enableHapticFeedback: true,
|
|
412
|
+
swipeThreshold: 30,
|
|
413
|
+
swipeVelocityThreshold: 0.3
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return useTouchGestures(ref, handleGesture, gestureConfig)
|
|
417
|
+
}
|