fix remaining issue 72 ui and iptv regressions

This commit is contained in:
kuekhaoyang
2026-04-02 22:14:46 +08:00
parent 5829a79ee3
commit 37b6e160d7
6 changed files with 410 additions and 100 deletions
+106 -22
View File
@@ -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 */}