mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement mobile double-tap gestures for video player controls and apply CSS optimizations.
This commit is contained in:
@@ -23,13 +23,17 @@
|
|||||||
.video-card-wrapper {
|
.video-card-wrapper {
|
||||||
/* CSS containment for isolation */
|
/* CSS containment for isolation */
|
||||||
contain: layout style paint;
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (content-visibility: auto) {
|
||||||
|
.video-card-wrapper {
|
||||||
/* Content visibility for lazy rendering */
|
/* Content visibility for lazy rendering */
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
|
|
||||||
/* Reduce layout shifts */
|
/* Reduce layout shifts */
|
||||||
contain-intrinsic-size: auto 400px;
|
contain-intrinsic-size: auto 400px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduce repaints on images */
|
/* Reduce repaints on images */
|
||||||
.video-card-image {
|
.video-card-image {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ select:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(128, 128, 128, 0.5);
|
||||||
background: color-mix(in srgb, var(--glass-bg) 80%, transparent);
|
background: color-mix(in srgb, var(--glass-bg) 80%, transparent);
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -61,6 +62,7 @@ select:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 122, 255, 0.6);
|
||||||
background: color-mix(in srgb, var(--accent-color) 60%, transparent);
|
background: color-mix(in srgb, var(--accent-color) 60%, transparent);
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,7 @@ select:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-history-dropdown::-webkit-scrollbar-thumb {
|
.search-history-dropdown::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(128, 128, 128, 0.5);
|
||||||
background: color-mix(in srgb, var(--glass-bg) 80%, transparent);
|
background: color-mix(in srgb, var(--glass-bg) 80%, transparent);
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
}
|
}
|
||||||
@@ -118,8 +121,13 @@ nav:where(.sticky, .fixed),
|
|||||||
animation: fadeInUp 0.2s ease-out;
|
animation: fadeInUp 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@supports (content-visibility: auto) {
|
||||||
img {
|
img {
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
.glass-input:focus {
|
.glass-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 30%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-history-item:hover {
|
.search-history-item:hover {
|
||||||
|
background: rgba(0, 122, 255, 0.1);
|
||||||
background: color-mix(in srgb, var(--accent-color) 10%, transparent);
|
background: color-mix(in srgb, var(--accent-color) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-history-item.highlighted {
|
.search-history-item.highlighted {
|
||||||
|
background: rgba(0, 122, 255, 0.15);
|
||||||
background: color-mix(in srgb, var(--accent-color) 15%, transparent);
|
background: color-mix(in srgb, var(--accent-color) 15%, transparent);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 122, 255, 0.3);
|
||||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-color) 30%, transparent);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-color) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-history-remove:hover {
|
.search-history-remove:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.2);
|
||||||
background: color-mix(in srgb, var(--text-color-secondary) 20%, transparent);
|
background: color-mix(in srgb, var(--text-color-secondary) 20%, transparent);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.spinner {
|
.spinner {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
border: 5px solid rgba(128, 128, 128, 0.35);
|
||||||
border: 5px solid color-mix(in srgb, var(--glass-bg) 50%, transparent);
|
border: 5px solid color-mix(in srgb, var(--glass-bg) 50%, transparent);
|
||||||
border-top-color: var(--accent-color);
|
border-top-color: var(--accent-color);
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DesktopControlsWrapper } from './desktop/DesktopControlsWrapper';
|
|||||||
import { DesktopOverlayWrapper } from './desktop/DesktopOverlayWrapper';
|
import { DesktopOverlayWrapper } from './desktop/DesktopOverlayWrapper';
|
||||||
import { usePlayerSettings } from './hooks/usePlayerSettings';
|
import { usePlayerSettings } from './hooks/usePlayerSettings';
|
||||||
import { useIsIOS, useIsMobile } from '@/lib/hooks/mobile/useDeviceDetection';
|
import { useIsIOS, useIsMobile } from '@/lib/hooks/mobile/useDeviceDetection';
|
||||||
|
import { useDoubleTap } from '@/lib/hooks/mobile/useDoubleTap';
|
||||||
import './web-fullscreen.css';
|
import './web-fullscreen.css';
|
||||||
|
|
||||||
interface DesktopVideoPlayerProps {
|
interface DesktopVideoPlayerProps {
|
||||||
@@ -141,6 +142,7 @@ export function DesktopVideoPlayer({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
handleMouseMove,
|
handleMouseMove,
|
||||||
|
handleTouchToggleControls,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
handlePlay,
|
handlePlay,
|
||||||
handlePause,
|
handlePause,
|
||||||
@@ -149,6 +151,28 @@ export function DesktopVideoPlayer({
|
|||||||
handleVideoError,
|
handleVideoError,
|
||||||
} = logic;
|
} = logic;
|
||||||
|
|
||||||
|
// Mobile double-tap gesture for skip forward/backward
|
||||||
|
const { handleTap } = useDoubleTap({
|
||||||
|
onSingleTap: handleTouchToggleControls,
|
||||||
|
onDoubleTapLeft: () => {
|
||||||
|
logic.skipBackward();
|
||||||
|
handleMouseMove(); // Reset 3s auto-hide timer
|
||||||
|
},
|
||||||
|
onDoubleTapRight: () => {
|
||||||
|
logic.skipForward();
|
||||||
|
handleMouseMove(); // Reset 3s auto-hide timer
|
||||||
|
},
|
||||||
|
onSkipContinueLeft: () => {
|
||||||
|
logic.skipBackward();
|
||||||
|
handleMouseMove();
|
||||||
|
},
|
||||||
|
onSkipContinueRight: () => {
|
||||||
|
logic.skipForward();
|
||||||
|
handleMouseMove();
|
||||||
|
},
|
||||||
|
isSkipModeActive: data.showSkipForwardIndicator || data.showSkipBackwardIndicator,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -176,12 +200,10 @@ export function DesktopVideoPlayer({
|
|||||||
onError={handleVideoError}
|
onError={handleVideoError}
|
||||||
onWaiting={() => setIsLoading(true)}
|
onWaiting={() => setIsLoading(true)}
|
||||||
onCanPlay={() => setIsLoading(false)}
|
onCanPlay={() => setIsLoading(false)}
|
||||||
onClick={(e) => {
|
onClick={!isMobile ? (e) => {
|
||||||
// Prevent native behavior on iOS
|
|
||||||
// e.preventDefault();
|
|
||||||
// React synthetic event doesn't always stop native video toggle on iOS, but good practice
|
|
||||||
togglePlay();
|
togglePlay();
|
||||||
}}
|
} : undefined}
|
||||||
|
onTouchStart={isMobile ? handleTap : undefined}
|
||||||
{...({ 'webkit-playsinline': 'true' } as any)} // Legacy iOS support
|
{...({ 'webkit-playsinline': 'true' } as any)} // Legacy iOS support
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -142,14 +142,14 @@ export function DesktopOverlay({
|
|||||||
<div
|
<div
|
||||||
className={`absolute left-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
|
className={`absolute left-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: showNavButtons ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSkipBackward();
|
onSkipBackward();
|
||||||
}}
|
}}
|
||||||
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 pointer-events-auto cursor-pointer"
|
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 cursor-pointer"
|
||||||
aria-label="后退 10 秒"
|
aria-label="后退 10 秒"
|
||||||
>
|
>
|
||||||
<Icons.SkipBack className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
|
<Icons.SkipBack className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
|
||||||
@@ -160,14 +160,14 @@ export function DesktopOverlay({
|
|||||||
<div
|
<div
|
||||||
className={`absolute right-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
|
className={`absolute right-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: showNavButtons ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSkipForward();
|
onSkipForward();
|
||||||
}}
|
}}
|
||||||
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 pointer-events-auto cursor-pointer"
|
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 cursor-pointer"
|
||||||
aria-label="前进 10 秒"
|
aria-label="前进 10 秒"
|
||||||
>
|
>
|
||||||
<Icons.FastForward className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
|
<Icons.FastForward className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
|
||||||
|
|||||||
@@ -107,11 +107,33 @@ export function useControlsVisibility({
|
|||||||
return () => clearSpeedMenuTimeout();
|
return () => clearSpeedMenuTimeout();
|
||||||
}, [showSpeedMenu, startSpeedMenuTimeout, clearSpeedMenuTimeout]);
|
}, [showSpeedMenu, startSpeedMenuTimeout, clearSpeedMenuTimeout]);
|
||||||
|
|
||||||
|
const handleTouchToggleControls = useCallback(() => {
|
||||||
|
if (showControls && isPlaying) {
|
||||||
|
// Controls visible + playing = hide immediately
|
||||||
|
if (controlsTimeoutRef.current) {
|
||||||
|
clearTimeout(controlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
setShowControls(false);
|
||||||
|
} else if (!showControls) {
|
||||||
|
// Controls hidden = show + start 3s auto-hide timer
|
||||||
|
setShowControls(true);
|
||||||
|
if (isPlaying) {
|
||||||
|
if (controlsTimeoutRef.current) {
|
||||||
|
clearTimeout(controlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
controlsTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowControls(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [showControls, isPlaying, setShowControls, controlsTimeoutRef]);
|
||||||
|
|
||||||
const visibilityActions = useMemo(() => ({
|
const visibilityActions = useMemo(() => ({
|
||||||
handleMouseMove,
|
handleMouseMove,
|
||||||
|
handleTouchToggleControls,
|
||||||
startSpeedMenuTimeout,
|
startSpeedMenuTimeout,
|
||||||
clearSpeedMenuTimeout
|
clearSpeedMenuTimeout
|
||||||
}), [handleMouseMove, startSpeedMenuTimeout, clearSpeedMenuTimeout]);
|
}), [handleMouseMove, handleTouchToggleControls, startSpeedMenuTimeout, clearSpeedMenuTimeout]);
|
||||||
|
|
||||||
return visibilityActions;
|
return visibilityActions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ export function useDesktopPlayerLogic({
|
|||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
handleMouseMove: controlsVisibility.handleMouseMove,
|
handleMouseMove: controlsVisibility.handleMouseMove,
|
||||||
|
handleTouchToggleControls: controlsVisibility.handleTouchToggleControls,
|
||||||
togglePlay: playbackControls.togglePlay,
|
togglePlay: playbackControls.togglePlay,
|
||||||
handlePlay: playbackControls.handlePlay,
|
handlePlay: playbackControls.handlePlay,
|
||||||
handlePause: playbackControls.handlePause,
|
handlePause: playbackControls.handlePause,
|
||||||
|
|||||||
Generated
+2254
-5
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kvideo",
|
"name": "kvideo",
|
||||||
"version": "4.1.5",
|
"version": "4.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"eslint": "^10",
|
"eslint": "^10",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-preset-env": "^11.1.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
|
"postcss-preset-env": {
|
||||||
|
features: {
|
||||||
|
"cascade-layers": true,
|
||||||
|
"color-mix": true,
|
||||||
|
},
|
||||||
|
browsers: "Safari >= 15, iOS >= 15",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user