mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix remaining issue 72 ui and iptv regressions
This commit is contained in:
+106
-22
@@ -91,6 +91,7 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
const [isLive, setIsLive] = useState(true);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [seekWindow, setSeekWindow] = useState<{ start: number; end: number; duration: number } | null>(null);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [showVolumeSlider, setShowVolumeSlider] = useState(false);
|
||||
@@ -105,6 +106,12 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
|
||||
// Whether we have multi-source data
|
||||
const hasMultiSource = channelsBySource && sources && sources.length > 0;
|
||||
const activeSourceId = channel.sourceId || null;
|
||||
const activeGroupKey = activeSourceId && channel.group ? `${activeSourceId}::${channel.group}` : null;
|
||||
const activeSource = useMemo(
|
||||
() => (activeSourceId && sources ? sources.find((source) => source.id === activeSourceId) || null : null),
|
||||
[activeSourceId, sources]
|
||||
);
|
||||
|
||||
// Get current route URL
|
||||
const routes = channel.routes || [channel.url];
|
||||
@@ -167,20 +174,31 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
const onPlay = () => setIsPlaying(true);
|
||||
const onPause = () => setIsPlaying(false);
|
||||
const onTimeUpdate = () => {
|
||||
const range = getSeekRange(video);
|
||||
setCurrentTime(video.currentTime);
|
||||
const dur = video.duration;
|
||||
if (isFinite(dur) && dur > 0) {
|
||||
setDuration(dur);
|
||||
setSeekWindow(range);
|
||||
if (range) {
|
||||
setDuration(range.duration);
|
||||
setIsLive(false);
|
||||
} else {
|
||||
const dur = video.duration;
|
||||
if (isFinite(dur) && dur > 0) {
|
||||
setDuration(dur);
|
||||
}
|
||||
setIsLive(true);
|
||||
}
|
||||
};
|
||||
const onDurationChange = () => {
|
||||
const dur = video.duration;
|
||||
if (isFinite(dur) && dur > 0) {
|
||||
setDuration(dur);
|
||||
const range = getSeekRange(video);
|
||||
setSeekWindow(range);
|
||||
if (range) {
|
||||
setDuration(range.duration);
|
||||
setIsLive(false);
|
||||
} else {
|
||||
const dur = video.duration;
|
||||
if (isFinite(dur) && dur > 0) {
|
||||
setDuration(dur);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onVolumeChange = () => {
|
||||
@@ -212,6 +230,7 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
setIsLive(true);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setSeekWindow(null);
|
||||
|
||||
// Clean up previous
|
||||
if (hlsRef.current) {
|
||||
@@ -310,7 +329,7 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
hlsProxy.attachMedia(video);
|
||||
|
||||
// Filter HEVC levels for proxy attempt too
|
||||
hlsProxy.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
|
||||
hlsProxy.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
filterHEVCLevels(hlsProxy);
|
||||
markLoaded();
|
||||
video.play().catch(() => {});
|
||||
@@ -349,7 +368,7 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
// First try initial URL (direct or proxied based on custom headers)
|
||||
hls.loadSource(initialUrl);
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
// Filter HEVC levels to prevent audio-only playback
|
||||
filterHEVCLevels(hls);
|
||||
markLoaded();
|
||||
@@ -469,14 +488,12 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
};
|
||||
|
||||
const progressPercent = useMemo(() => {
|
||||
const video = videoRef.current;
|
||||
const seekRange = video ? getSeekRange(video) : null;
|
||||
if (seekRange) {
|
||||
return Math.max(0, Math.min(100, ((currentTime - seekRange.start) / seekRange.duration) * 100));
|
||||
if (seekWindow) {
|
||||
return Math.max(0, Math.min(100, ((currentTime - seekWindow.start) / seekWindow.duration) * 100));
|
||||
}
|
||||
if (!duration) return 0;
|
||||
return Math.max(0, Math.min(100, (currentTime / duration) * 100));
|
||||
}, [currentTime, duration]);
|
||||
}, [currentTime, duration, seekWindow]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (!containerRef.current) return;
|
||||
@@ -567,24 +584,35 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
const isSearchMode = sidebarSearch.trim().length > 0;
|
||||
|
||||
// Toggle source expansion
|
||||
const toggleSource = (sourceId: string) => {
|
||||
const toggleSource = useCallback((sourceId: string) => {
|
||||
setExpandedSources(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(sourceId)) next.delete(sourceId);
|
||||
else next.add(sourceId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Toggle group expansion
|
||||
const toggleGroup = (key: string) => {
|
||||
const toggleGroup = useCallback((key: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleActiveSource = useCallback(() => {
|
||||
if (!activeSourceId) return;
|
||||
toggleSource(activeSourceId);
|
||||
}, [activeSourceId, toggleSource]);
|
||||
|
||||
const toggleActiveGroup = useCallback(() => {
|
||||
if (!activeSourceId || !channel.group) return;
|
||||
setExpandedSources(prev => new Set(prev).add(activeSourceId));
|
||||
toggleGroup(`${activeSourceId}::${channel.group}`);
|
||||
}, [activeSourceId, channel.group, toggleGroup]);
|
||||
|
||||
// Render a channel button
|
||||
const renderChannelButton = (ch: M3UChannel, i: number) => {
|
||||
@@ -623,21 +651,35 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
// Render multi-level sidebar content
|
||||
const renderMultiLevelSidebar = () => {
|
||||
if (!channelsBySource || !sources) return null;
|
||||
const orderedSources = activeSourceId
|
||||
? [
|
||||
...sources.filter((source) => source.id === activeSourceId),
|
||||
...sources.filter((source) => source.id !== activeSourceId),
|
||||
]
|
||||
: sources;
|
||||
|
||||
return (
|
||||
<div className="p-1">
|
||||
{sources.map(source => {
|
||||
{orderedSources.map(source => {
|
||||
const sourceData = channelsBySource[source.id];
|
||||
if (!sourceData || sourceData.channels.length === 0) return null;
|
||||
|
||||
const isExpanded = expandedSources.has(source.id);
|
||||
const isActiveSource = source.id === activeSourceId;
|
||||
const orderedGroups = isActiveSource && channel.group
|
||||
? [channel.group, ...sourceData.groups.filter((group) => group !== channel.group)]
|
||||
: sourceData.groups;
|
||||
|
||||
return (
|
||||
<div key={source.id} className="mb-1">
|
||||
{/* Source Header */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); toggleSource(source.id); }}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium text-white/90 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
className={`w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer ${
|
||||
isActiveSource
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-white/90 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Icons.TV size={14} className="flex-shrink-0 text-[var(--accent-color)]" />
|
||||
@@ -653,18 +695,23 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
{/* Source Content */}
|
||||
{isExpanded && (
|
||||
<div className="ml-2 border-l border-white/10 pl-1">
|
||||
{sourceData.groups.length > 0 ? (
|
||||
{orderedGroups.length > 0 ? (
|
||||
// Has groups — show group-level
|
||||
sourceData.groups.map(group => {
|
||||
orderedGroups.map(group => {
|
||||
const groupKey = `${source.id}::${group}`;
|
||||
const groupExpanded = expandedGroups.has(groupKey);
|
||||
const groupChannels = sourceData.channels.filter(ch => ch.group === group);
|
||||
const isActiveGroup = groupKey === activeGroupKey;
|
||||
|
||||
return (
|
||||
<div key={groupKey} className="mb-0.5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); toggleGroup(groupKey); }}
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 rounded text-xs text-white/60 hover:bg-white/5 transition-colors cursor-pointer"
|
||||
className={`w-full flex items-center justify-between px-2 py-1.5 rounded text-xs transition-colors cursor-pointer ${
|
||||
isActiveGroup
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-white/60 hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Icons.Tag size={12} className="flex-shrink-0" />
|
||||
@@ -958,6 +1005,43 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(activeSource || channel.group) && (
|
||||
<div className="px-3 py-2 border-b border-white/10">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[10px] uppercase tracking-[0.18em] text-white/30">当前</span>
|
||||
{activeSource && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleActiveSource();
|
||||
}}
|
||||
className={`px-2 py-1 rounded-full text-[11px] border transition-colors cursor-pointer ${
|
||||
activeSourceId && expandedSources.has(activeSourceId)
|
||||
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white'
|
||||
: 'bg-white/5 border-white/10 text-white/80 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
源: {activeSource.name}
|
||||
</button>
|
||||
)}
|
||||
{channel.group && activeGroupKey && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleActiveGroup();
|
||||
}}
|
||||
className={`px-2 py-1 rounded-full text-[11px] border transition-colors cursor-pointer ${
|
||||
expandedGroups.has(activeGroupKey)
|
||||
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white'
|
||||
: 'bg-white/5 border-white/10 text-white/80 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
标签: {channel.group}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar Content */}
|
||||
|
||||
Reference in New Issue
Block a user