mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 07:43:43 +08:00
feat: Persist player volume and mute settings and integrate source selection directly into the episode list.
This commit is contained in:
+25
-38
@@ -7,7 +7,7 @@ import { VideoPlayer } from '@/components/player/VideoPlayer';
|
||||
import { VideoMetadata } from '@/components/player/VideoMetadata';
|
||||
import { EpisodeList } from '@/components/player/EpisodeList';
|
||||
import { PlayerError } from '@/components/player/PlayerError';
|
||||
import { SourceSelector, SourceInfo } from '@/components/player/SourceSelector';
|
||||
import { SourceInfo } from '@/components/player/EpisodeList';
|
||||
import { useVideoPlayer } from '@/lib/hooks/useVideoPlayer';
|
||||
import { useHistory } from '@/lib/store/history-store';
|
||||
import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar';
|
||||
@@ -16,7 +16,6 @@ import { PlayerNavbar } from '@/components/player/PlayerNavbar';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { premiumModeSettingsStore } from '@/lib/store/premium-mode-settings';
|
||||
import { SegmentedControl } from '@/components/ui/SegmentedControl';
|
||||
import Image from 'next/image';
|
||||
|
||||
function PlayerContent() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -37,7 +36,7 @@ function PlayerContent() {
|
||||
);
|
||||
|
||||
// Mobile tab state
|
||||
const [activeTab, setActiveTab] = useState<'episodes' | 'info' | 'sources'>('episodes');
|
||||
const [activeTab, setActiveTab] = useState<'episodes' | 'info'>('episodes');
|
||||
|
||||
// Sync with store changes if any (though usually it's one-way from UI to store)
|
||||
useEffect(() => {
|
||||
@@ -218,18 +217,15 @@ function PlayerContent() {
|
||||
<div className="lg:col-span-1">
|
||||
<div className="lg:sticky lg:top-32 space-y-6">
|
||||
{/* Mobile Tabs */}
|
||||
{groupedSources.length > 0 && (
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ label: '选集', value: 'episodes' },
|
||||
{ label: '简介', value: 'info' },
|
||||
...(groupedSources.length > 1 ? [{ label: '来源', value: 'sources' as const }] : []),
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="lg:hidden mb-4"
|
||||
/>
|
||||
)}
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ label: '选集', value: 'episodes' },
|
||||
{ label: '简介', value: 'info' },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="lg:hidden mb-4"
|
||||
/>
|
||||
|
||||
{/* Info Tab Content - Mobile Only */}
|
||||
<div className={activeTab !== 'info' ? 'hidden' : 'block lg:hidden'}>
|
||||
@@ -240,7 +236,7 @@ function PlayerContent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Episode List - Visible if desktop OR active mobile tab */}
|
||||
{/* Episode List with integrated source selector - Visible if desktop OR active mobile tab */}
|
||||
<div className={activeTab !== 'episodes' ? 'hidden lg:block' : 'block'}>
|
||||
<EpisodeList
|
||||
episodes={videoData?.episodes || null}
|
||||
@@ -248,30 +244,21 @@ function PlayerContent() {
|
||||
isReversed={isReversed}
|
||||
onEpisodeClick={handleEpisodeClick}
|
||||
onToggleReverse={handleToggleReverse}
|
||||
sources={groupedSources.length > 0 ? groupedSources : undefined}
|
||||
currentSource={currentSourceId || source || ''}
|
||||
onSourceChange={(newSource) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('id', String(newSource.id));
|
||||
params.set('source', newSource.source);
|
||||
params.set('title', title || '');
|
||||
if (groupedSourcesParam) {
|
||||
params.set('groupedSources', groupedSourcesParam);
|
||||
}
|
||||
setCurrentSourceId(newSource.source);
|
||||
router.replace(`/player?${params.toString()}`, { scroll: false });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Source Selector - Visible if (desktop AND grouped sources) OR (active mobile tab AND grouped sources) */}
|
||||
{groupedSources.length > 0 && (
|
||||
<div className={activeTab !== 'sources' ? 'hidden lg:block' : 'block'}>
|
||||
<SourceSelector
|
||||
sources={groupedSources}
|
||||
currentSource={currentSourceId || source || ''}
|
||||
onSourceChange={(newSource) => {
|
||||
// Navigate to same video with different source
|
||||
const params = new URLSearchParams();
|
||||
params.set('id', String(newSource.id));
|
||||
params.set('source', newSource.source);
|
||||
params.set('title', title || '');
|
||||
if (groupedSourcesParam) {
|
||||
params.set('groupedSources', groupedSourcesParam);
|
||||
}
|
||||
setCurrentSourceId(newSource.source);
|
||||
router.replace(`/player?${params.toString()}`, { scroll: false });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
import { LatencyBadge } from '@/components/ui/LatencyBadge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
|
||||
interface Episode {
|
||||
name?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SourceInfo {
|
||||
id: string | number;
|
||||
source: string;
|
||||
sourceName?: string;
|
||||
latency?: number;
|
||||
pic?: string;
|
||||
}
|
||||
|
||||
interface EpisodeListProps {
|
||||
episodes: Episode[] | null;
|
||||
currentEpisode: number;
|
||||
isReversed?: boolean;
|
||||
onEpisodeClick: (episode: Episode, index: number) => void;
|
||||
onToggleReverse?: (reversed: boolean) => void;
|
||||
// Optional source integration props
|
||||
sources?: SourceInfo[];
|
||||
currentSource?: string;
|
||||
onSourceChange?: (source: SourceInfo) => void;
|
||||
}
|
||||
|
||||
export function EpisodeList({
|
||||
@@ -25,10 +39,82 @@ export function EpisodeList({
|
||||
currentEpisode,
|
||||
isReversed = false,
|
||||
onEpisodeClick,
|
||||
onToggleReverse
|
||||
onToggleReverse,
|
||||
sources,
|
||||
currentSource,
|
||||
onSourceChange,
|
||||
}: EpisodeListProps) {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const [sourceExpanded, setSourceExpanded] = useState(false);
|
||||
|
||||
// Source latency state
|
||||
const [latencies, setLatencies] = useState<Record<string, number>>({});
|
||||
const [isLoadingLatency, setIsLoadingLatency] = useState(false);
|
||||
|
||||
const showSourceSelector = sources && sources.length > 1 && onSourceChange;
|
||||
|
||||
// Current source info
|
||||
const currentSourceInfo = useMemo(() => {
|
||||
if (!sources || !currentSource) return null;
|
||||
return sources.find(s => s.source === currentSource) || null;
|
||||
}, [sources, currentSource]);
|
||||
|
||||
// Sort sources by latency
|
||||
const sortedSources = useMemo(() => {
|
||||
if (!sources) return [];
|
||||
return [...sources].sort((a, b) => {
|
||||
const latA = latencies[a.source] ?? a.latency ?? Infinity;
|
||||
const latB = latencies[b.source] ?? b.latency ?? Infinity;
|
||||
return latA - latB;
|
||||
});
|
||||
}, [sources, latencies]);
|
||||
|
||||
// Initialize latencies from sources
|
||||
useEffect(() => {
|
||||
if (!sources) return;
|
||||
const initial: Record<string, number> = {};
|
||||
sources.forEach(s => {
|
||||
if (s.latency !== undefined) {
|
||||
initial[s.source] = s.latency;
|
||||
}
|
||||
});
|
||||
setLatencies(initial);
|
||||
}, [sources]);
|
||||
|
||||
// Refresh latencies
|
||||
const refreshLatencies = useCallback(async () => {
|
||||
if (!sources) return;
|
||||
setIsLoadingLatency(true);
|
||||
|
||||
const results = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
try {
|
||||
const response = await fetch('/api/ping', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: source.source }),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return { source: source.source, latency: data.latency };
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return { source: source.source, latency: undefined };
|
||||
})
|
||||
);
|
||||
|
||||
const newLatencies: Record<string, number> = {};
|
||||
results.forEach(({ source, latency }) => {
|
||||
if (latency !== undefined) {
|
||||
newLatencies[source] = latency;
|
||||
}
|
||||
});
|
||||
setLatencies(newLatencies);
|
||||
setIsLoadingLatency(false);
|
||||
}, [sources]);
|
||||
|
||||
// Memoized display episodes - reversed if toggle is on
|
||||
const displayEpisodes = useMemo(() => {
|
||||
@@ -76,6 +162,117 @@ export function EpisodeList({
|
||||
|
||||
return (
|
||||
<Card hover={false}>
|
||||
{/* Integrated Source Selector Header */}
|
||||
{showSourceSelector && (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={() => setSourceExpanded(!sourceExpanded)}
|
||||
className="w-full flex items-center justify-between p-3 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] hover:bg-[var(--glass-hover)] transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Icons.Layers size={16} className="flex-shrink-0 text-[var(--text-color-secondary)]" />
|
||||
<span className="text-sm font-medium text-[var(--text-color)] truncate">
|
||||
{currentSourceInfo?.sourceName || currentSourceInfo?.source || '当前来源'}
|
||||
</span>
|
||||
<Badge variant="primary" className="flex-shrink-0">{sources!.length}</Badge>
|
||||
</div>
|
||||
<Icons.ChevronDown
|
||||
size={16}
|
||||
className={`flex-shrink-0 text-[var(--text-color-secondary)] transition-transform duration-200 ${sourceExpanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Expanded source list */}
|
||||
{sourceExpanded && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
refreshLatencies();
|
||||
}}
|
||||
disabled={isLoadingLatency}
|
||||
className="flex items-center gap-1.5 text-xs px-2.5 py-1"
|
||||
>
|
||||
<Icons.RefreshCw size={12} className={isLoadingLatency ? 'animate-spin' : ''} />
|
||||
刷新延迟
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1.5 max-h-[200px] overflow-y-auto">
|
||||
{sortedSources.map((source, index) => {
|
||||
const isCurrent = source.source === currentSource;
|
||||
const latency = latencies[source.source] ?? source.latency;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${source.source}-${index}`}
|
||||
onClick={() => {
|
||||
if (!isCurrent) {
|
||||
onSourceChange!(source);
|
||||
setSourceExpanded(false);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
w-full p-2.5 rounded-[var(--radius-2xl)] text-left transition-all duration-200
|
||||
flex items-center gap-2.5
|
||||
${isCurrent
|
||||
? 'bg-[var(--accent-color)] text-white shadow-[0_4px_12px_color-mix(in_srgb,var(--accent-color)_50%,transparent)]'
|
||||
: 'bg-[var(--glass-bg)] hover:bg-[var(--glass-hover)] text-[var(--text-color)] border border-[var(--glass-border)] cursor-pointer'
|
||||
}
|
||||
`}
|
||||
aria-current={isCurrent ? 'true' : undefined}
|
||||
>
|
||||
{source.pic && (
|
||||
<div className="w-10 h-14 rounded-[var(--radius-2xl)] overflow-hidden flex-shrink-0 bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)]">
|
||||
<Image
|
||||
src={source.pic}
|
||||
alt=""
|
||||
width={40}
|
||||
height={56}
|
||||
className="w-full h-full object-cover"
|
||||
unoptimized
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">
|
||||
{source.sourceName || source.source}
|
||||
</div>
|
||||
{latency !== undefined && (
|
||||
<div className="mt-0.5">
|
||||
<LatencyBadge latency={latency} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<Icons.Play size={14} className="flex-shrink-0" />
|
||||
)}
|
||||
{!isCurrent && index < 3 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`flex-shrink-0 ${index === 0 ? 'bg-yellow-500/20 text-yellow-600 border-yellow-500' :
|
||||
index === 1 ? 'bg-gray-400/20 text-gray-600 border-gray-400' :
|
||||
'bg-orange-400/20 text-orange-600 border-orange-400'
|
||||
}`}
|
||||
>
|
||||
#{index + 1}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Episode List Header */}
|
||||
<h3 className="text-lg sm:text-xl font-bold text-[var(--text-color)] mb-4 flex items-center gap-2">
|
||||
<Icons.List size={20} className="sm:w-6 sm:h-6" />
|
||||
<span>选集</span>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
@@ -75,14 +74,16 @@ export function VideoMetadata({ videoData, source, title }: VideoMetadataProps)
|
||||
<span className="font-semibold">主演:</span>
|
||||
<span className="inline-flex flex-wrap gap-1">
|
||||
{splitPersonNames(videoData.vod_actor).map((name) => (
|
||||
<Link
|
||||
<a
|
||||
key={name}
|
||||
href={`/?q=${encodeURIComponent(name)}`}
|
||||
data-focusable
|
||||
className="inline-block px-2 py-0.5 rounded-full bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] hover:border-[var(--accent-color)] hover:text-[var(--accent-color)] transition-all duration-200"
|
||||
href={`https://movie.douban.com/celebrities/search?search_text=${encodeURIComponent(name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] hover:border-[var(--accent-color)] hover:text-[var(--accent-color)] transition-all duration-200"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
<Icons.ExternalLink size={10} />
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
@@ -92,14 +93,16 @@ export function VideoMetadata({ videoData, source, title }: VideoMetadataProps)
|
||||
<span className="font-semibold">导演:</span>
|
||||
<span className="inline-flex flex-wrap gap-1">
|
||||
{splitPersonNames(videoData.vod_director).map((name) => (
|
||||
<Link
|
||||
<a
|
||||
key={name}
|
||||
href={`/?q=${encodeURIComponent(name)}`}
|
||||
data-focusable
|
||||
className="inline-block px-2 py-0.5 rounded-full bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] hover:border-[var(--accent-color)] hover:text-[var(--accent-color)] transition-all duration-200"
|
||||
href={`https://movie.douban.com/celebrities/search?search_text=${encodeURIComponent(name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] hover:border-[var(--accent-color)] hover:text-[var(--accent-color)] transition-all duration-200"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
<Icons.ExternalLink size={10} />
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +89,8 @@ export function useDesktopShortcuts({
|
||||
setVolume(newVolUp);
|
||||
if (videoRef.current) videoRef.current.volume = newVolUp;
|
||||
setIsMuted(newVolUp === 0);
|
||||
localStorage.setItem('kvideo-volume', String(newVolUp));
|
||||
localStorage.setItem('kvideo-muted', String(newVolUp === 0));
|
||||
showVolumeBarTemporarily();
|
||||
break;
|
||||
case 'arrowdown':
|
||||
@@ -97,6 +99,8 @@ export function useDesktopShortcuts({
|
||||
setVolume(newVolDown);
|
||||
if (videoRef.current) videoRef.current.volume = newVolDown;
|
||||
setIsMuted(newVolDown === 0);
|
||||
localStorage.setItem('kvideo-volume', String(newVolDown));
|
||||
localStorage.setItem('kvideo-muted', String(newVolDown === 0));
|
||||
showVolumeBarTemporarily();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,11 @@ export function useVolumeControls({
|
||||
if (isMuted) {
|
||||
videoRef.current.volume = volume;
|
||||
setIsMuted(false);
|
||||
localStorage.setItem('kvideo-muted', 'false');
|
||||
} else {
|
||||
videoRef.current.volume = 0;
|
||||
setIsMuted(true);
|
||||
localStorage.setItem('kvideo-muted', 'true');
|
||||
}
|
||||
}, [videoRef, isMuted, volume, setIsMuted]);
|
||||
|
||||
@@ -51,6 +53,8 @@ export function useVolumeControls({
|
||||
setVolume(pos);
|
||||
videoRef.current.volume = pos;
|
||||
setIsMuted(pos === 0);
|
||||
localStorage.setItem('kvideo-volume', String(pos));
|
||||
localStorage.setItem('kvideo-muted', String(pos === 0));
|
||||
}, [videoRef, volumeBarRef, setVolume, setIsMuted]);
|
||||
|
||||
const handleVolumeMouseDown = useCallback((e: any) => {
|
||||
@@ -68,6 +72,8 @@ export function useVolumeControls({
|
||||
setVolume(pos);
|
||||
videoRef.current.volume = pos;
|
||||
setIsMuted(pos === 0);
|
||||
localStorage.setItem('kvideo-volume', String(pos));
|
||||
localStorage.setItem('kvideo-muted', String(pos === 0));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
|
||||
@@ -22,8 +22,19 @@ export function useDesktopPlayerState() {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [volume, setVolume] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('kvideo-volume');
|
||||
return saved ? parseFloat(saved) : 1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
const [isMuted, setIsMuted] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('kvideo-muted') === 'true';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -189,4 +189,12 @@ export const UtilityIcons = {
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
),
|
||||
|
||||
ExternalLink: ({ 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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.3.3",
|
||||
"version": "4.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvideo",
|
||||
"version": "4.3.3",
|
||||
"version": "4.3.4",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.3.3",
|
||||
"version": "4.3.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user