mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-18 02:03:43 +08:00
feat: add toast notification functionality and copy link option in video players
This commit is contained in:
@@ -482,6 +482,21 @@ nav:where(.sticky, .fixed),
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
/* Toast slide-up animation */
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ export function DesktopVideoPlayer({
|
||||
const [isSkipForwardAnimatingOut, setIsSkipForwardAnimatingOut] = useState(false);
|
||||
const [isSkipBackwardAnimatingOut, setIsSkipBackwardAnimatingOut] = useState(false);
|
||||
const [showVolumeBar, setShowVolumeBar] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [showMoreMenu, setShowMoreMenu] = useState(false);
|
||||
|
||||
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const speedMenuTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -51,6 +54,8 @@ export function DesktopVideoPlayer({
|
||||
const isDraggingProgressRef = useRef(false);
|
||||
const isDraggingVolumeRef = useRef(false);
|
||||
const mouseMoveThrottleRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const moreMenuTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Check for PiP and AirPlay support
|
||||
useEffect(() => {
|
||||
@@ -521,6 +526,32 @@ export function DesktopVideoPlayer({
|
||||
}
|
||||
};
|
||||
|
||||
// Show toast notification
|
||||
const showToastNotification = (message: string) => {
|
||||
setToastMessage(message);
|
||||
setShowToast(true);
|
||||
|
||||
if (toastTimeoutRef.current) {
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
}
|
||||
|
||||
toastTimeoutRef.current = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
setTimeout(() => setToastMessage(null), 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Copy video link
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(src);
|
||||
showToastNotification('链接已复制到剪贴板');
|
||||
} catch (error) {
|
||||
console.error('Copy failed:', error);
|
||||
showToastNotification('复制失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-hide speed menu after 1.5s of inactivity
|
||||
const startSpeedMenuTimeout = () => {
|
||||
if (speedMenuTimeoutRef.current) {
|
||||
@@ -793,6 +824,58 @@ export function DesktopVideoPlayer({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* More Menu */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowMoreMenu(!showMoreMenu)}
|
||||
onMouseEnter={() => {
|
||||
if (moreMenuTimeoutRef.current) {
|
||||
clearTimeout(moreMenuTimeoutRef.current);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
moreMenuTimeoutRef.current = setTimeout(() => {
|
||||
setShowMoreMenu(false);
|
||||
}, 300);
|
||||
}}
|
||||
className="btn-icon"
|
||||
aria-label="More options"
|
||||
title="更多选项"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="12" cy="5" r="1"/>
|
||||
<circle cx="12" cy="19" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* More Menu Dropdown */}
|
||||
{showMoreMenu && (
|
||||
<div
|
||||
className="absolute bottom-full right-0 mb-2 bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 min-w-[180px]"
|
||||
onMouseEnter={() => {
|
||||
if (moreMenuTimeoutRef.current) {
|
||||
clearTimeout(moreMenuTimeoutRef.current);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowMoreMenu(false);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMoreMenu(false);
|
||||
handleCopyLink();
|
||||
}}
|
||||
className="w-full px-4 py-2.5 text-left text-sm text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] rounded-[var(--radius-2xl)] transition-colors flex items-center gap-3"
|
||||
>
|
||||
<Icons.Link size={18} />
|
||||
<span>复制链接</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen */}
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
@@ -805,6 +888,16 @@ export function DesktopVideoPlayer({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && toastMessage && (
|
||||
<div className="absolute bottom-24 left-1/2 -translate-x-1/2 z-[200] animate-slide-up">
|
||||
<div className="bg-[rgba(28,28,30,0.95)] backdrop-blur-[25px] rounded-[var(--radius-2xl)] border border-white/20 shadow-[0_8px_32px_rgba(0,0,0,0.6)] px-6 py-3 flex items-center gap-3 min-w-[200px]">
|
||||
<Icons.Check size={18} className="text-[#34c759] flex-shrink-0" />
|
||||
<span className="text-white text-sm font-medium">{toastMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export function MobileVideoPlayer({
|
||||
const [skipSide, setSkipSide] = useState<'left' | 'right' | null>(null);
|
||||
const [showSkipIndicator, setShowSkipIndicator] = useState(false);
|
||||
const [wasPlayingBeforeMenu, setWasPlayingBeforeMenu] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -47,6 +49,7 @@ export function MobileVideoPlayer({
|
||||
const menuIdleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const submenuIdleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isTogglingRef = useRef(false);
|
||||
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Screen orientation management
|
||||
useScreenOrientation(isFullscreen);
|
||||
@@ -398,6 +401,32 @@ export function MobileVideoPlayer({
|
||||
setShowSpeedMenu(false);
|
||||
};
|
||||
|
||||
// Show toast notification
|
||||
const showToastNotification = (message: string) => {
|
||||
setToastMessage(message);
|
||||
setShowToast(true);
|
||||
|
||||
if (toastTimeoutRef.current) {
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
}
|
||||
|
||||
toastTimeoutRef.current = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
setTimeout(() => setToastMessage(null), 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Copy video link
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(src);
|
||||
showToastNotification('链接已复制到剪贴板');
|
||||
} catch (error) {
|
||||
console.error('Copy failed:', error);
|
||||
showToastNotification('复制失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
@@ -509,6 +538,20 @@ export function MobileVideoPlayer({
|
||||
{showMoreMenu && (
|
||||
<div className="absolute bottom-full right-0 mb-2 min-w-[160px] z-[100] menu-container">
|
||||
<div className="bg-[rgba(255,255,255,0.1)] backdrop-blur-[25px] rounded-[var(--radius-2xl)] border border-[rgba(255,255,255,0.2)] shadow-[0_8px_32px_rgba(0,0,0,0.4)] overflow-hidden">
|
||||
{/* Copy Link Option */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMoreMenu(false);
|
||||
handleCopyLink();
|
||||
}}
|
||||
className="w-full px-4 py-3 text-left text-sm text-white hover:bg-white/20 flex items-center gap-3 transition-all"
|
||||
>
|
||||
<Icons.Link size={18} />
|
||||
<span>复制链接</span>
|
||||
</button>
|
||||
|
||||
<div className="h-px bg-white/10 my-1" />
|
||||
|
||||
{/* Volume Option */}
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -647,6 +690,16 @@ export function MobileVideoPlayer({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && toastMessage && (
|
||||
<div className="fixed bottom-20 left-1/2 -translate-x-1/2 z-[200] animate-slide-up">
|
||||
<div className="bg-[rgba(28,28,30,0.95)] backdrop-blur-[25px] rounded-[var(--radius-2xl)] border border-white/20 shadow-[0_8px_32px_rgba(0,0,0,0.6)] px-6 py-3 flex items-center gap-3 min-w-[200px] max-w-[90vw]">
|
||||
<Icons.Check size={18} className="text-[#34c759] flex-shrink-0" />
|
||||
<span className="text-white text-sm font-medium">{toastMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,4 +558,39 @@ export const Icons = {
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
),
|
||||
|
||||
Download: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
),
|
||||
|
||||
Link: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user