feat: add toast notification functionality and copy link option in video players

This commit is contained in:
kuekhaoyang
2025-11-18 13:20:22 +08:00
parent df437ce4ab
commit bda49259f1
4 changed files with 196 additions and 0 deletions
+15
View File
@@ -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);
}
+93
View File
@@ -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>
);
}
+53
View File
@@ -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>
);
}
+35
View File
@@ -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>
),
};