From ffce247cbc328b55f34ad083bfbd9aa352b121f5 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 8 Apr 2025 20:05:40 -0500 Subject: [PATCH] Instead of a popup when clicking on a program, expand the row and show the information. --- frontend/src/pages/Guide.jsx | 276 ++++++++++++++++++++++------------- frontend/src/pages/guide.css | 48 +++++- 2 files changed, 224 insertions(+), 100 deletions(-) diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index 38c4539e..9bbb9826 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -10,32 +10,28 @@ import useSettingsStore from '../store/settings'; import { Title, Box, - Modal, Flex, Button, Text, Paper, - Grid, Group, TextInput, Select, ActionIcon, Tooltip, + Transition, } from '@mantine/core'; -import { Search, X, Clock } from 'lucide-react'; +import { Search, X, Clock, Video, Calendar } from 'lucide-react'; import './guide.css'; /** Layout constants */ const CHANNEL_WIDTH = 120; // Width of the channel/logo column const PROGRAM_HEIGHT = 90; // Height of each channel row +const EXPANDED_PROGRAM_HEIGHT = 180; // Height for expanded program rows const HOUR_WIDTH = 450; // Increased from 300 to 450 to make each program wider const MINUTE_INCREMENT = 15; // For positioning programs every 15 min const MINUTE_BLOCK_WIDTH = HOUR_WIDTH / (60 / MINUTE_INCREMENT); -// Modal size constants -const MODAL_WIDTH = 600; -const MODAL_HEIGHT = 400; - export default function TVChannelGuide({ startDate, endDate }) { const { channels, recordings, channelGroups, profiles } = useChannelsStore(); @@ -43,8 +39,8 @@ export default function TVChannelGuide({ startDate, endDate }) { const [guideChannels, setGuideChannels] = useState([]); const [filteredChannels, setFilteredChannels] = useState([]); const [now, setNow] = useState(dayjs()); - const [selectedProgram, setSelectedProgram] = useState(null); - const [recording, setRecording] = useState(null); + const [expandedProgramId, setExpandedProgramId] = useState(null); // Track expanded program + const [recordingForProgram, setRecordingForProgram] = useState(null); const [loading, setLoading] = useState(true); const [initialScrollComplete, setInitialScrollComplete] = useState(false); @@ -250,32 +246,74 @@ export default function TVChannelGuide({ startDate, endDate }) { } showVideo(vidUrl); - - // Optionally close the modal - setSelectedProgram(null); } - // On program click, open the details modal + // On program click, toggle the expanded state function handleProgramClick(program, event) { - setSelectedProgram(program); - setRecording( - recordings.find((recording) => { - if (recording.custom_properties) { - const customProps = JSON.parse(recording.custom_properties); - if (customProps.program && customProps.program.id == program.id) { - return recording; - } + // Prevent event from bubbling up to parent elements + event.stopPropagation(); + + // Get the program's start time and calculate its position + const programStart = dayjs(program.start_time); + const startOffsetMinutes = programStart.diff(start, 'minute'); + const leftPx = (startOffsetMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; + + // Calculate desired scroll position (account for channel column width) + const desiredScrollPosition = Math.max(0, leftPx - 20); // 20px buffer + + // If already expanded, collapse it + if (expandedProgramId === program.id) { + setExpandedProgramId(null); + setRecordingForProgram(null); + return; + } + + // Otherwise expand this program + setExpandedProgramId(program.id); + + // Check if this program has a recording + const programRecording = recordings.find((recording) => { + if (recording.custom_properties) { + const customProps = JSON.parse(recording.custom_properties); + if (customProps.program && customProps.program.id == program.id) { + return true; } + } + return false; + }); - return null; - }) - ); + setRecordingForProgram(programRecording); + + // Scroll to show the start of the program if it's not already fully visible + if (guideRef.current && timelineRef.current) { + const currentScrollPosition = guideRef.current.scrollLeft; + + // Check if we need to scroll (if program start is before current view or too close to edge) + if (desiredScrollPosition < currentScrollPosition || + leftPx - currentScrollPosition < 100) { // 100px from left edge + + // Smooth scroll to the program's start + guideRef.current.scrollTo({ + left: desiredScrollPosition, + behavior: 'smooth' + }); + + // Also sync the timeline scroll + timelineRef.current.scrollTo({ + left: desiredScrollPosition, + behavior: 'smooth' + }); + } + } } - // Close the modal - function handleCloseModal() { - setSelectedProgram(null); - } + // Close the expanded program when clicking elsewhere + const handleClickOutside = () => { + if (expandedProgramId) { + setExpandedProgramId(null); + setRecordingForProgram(null); + } + }; // Function to scroll to current time - matches initial loading position const scrollToNow = () => { @@ -380,6 +418,8 @@ export default function TVChannelGuide({ startDate, endDate }) { const durationMinutes = programEnd.diff(programStart, 'minute'); const leftPx = (startOffsetMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; const widthPx = (durationMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; + + // Check if we have a recording for this program const recording = recordings.find((recording) => { if (recording.custom_properties) { const customProps = JSON.parse(recording.custom_properties); @@ -387,7 +427,6 @@ export default function TVChannelGuide({ startDate, endDate }) { return recording; } } - return null; }); @@ -397,10 +436,21 @@ export default function TVChannelGuide({ startDate, endDate }) { // Determine if the program has ended const isPast = now.isAfter(programEnd); + // Check if this program is expanded + const isExpanded = expandedProgramId === program.id; + // Calculate how much of the program is cut off const cutOffMinutes = Math.max(0, channelStart.diff(programStart, 'minute')); const cutOffPx = (cutOffMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH; + // Set the height based on expanded state + const rowHeight = isExpanded ? EXPANDED_PROGRAM_HEIGHT : PROGRAM_HEIGHT; + + // Determine expanded width - if program is short, ensure it has a minimum expanded width + // This will allow it to overlap programs to the right + const MIN_EXPANDED_WIDTH = 450; // Minimum width in pixels when expanded + const expandedWidthPx = Math.max(widthPx, MIN_EXPANDED_WIDTH); + return ( handleProgramClick(program, e)} > + + {/* Description is always shown but expands when row is expanded */} {program.description && ( {program.description} )} + + {/* Expanded content */} + {isExpanded && ( + + + {/* Only show Record button if not already recording AND not in the past */} + {!recording && !isPast && ( + + )} + + {isLive && ( + + )} + + + )} ); @@ -541,6 +643,7 @@ export default function TVChannelGuide({ startDate, endDate }) { color: '#fff', fontFamily: 'Roboto, sans-serif', }} + onClick={handleClickOutside} // Close expanded program when clicking outside > {/* Sticky top bar */} p.tvg_id === channel.epg_data?.tvg_id ); + // Check if any program in this channel is expanded + const hasExpandedProgram = channelPrograms.some(prog => prog.id === expandedProgramId); + const rowHeight = hasExpandedProgram ? EXPANDED_PROGRAM_HEIGHT : PROGRAM_HEIGHT; + return ( {/* Channel logo - sticky horizontally */} - {/* Logo content - unchanged */} + {/* Logo content */} {/* Programs for this channel */} - + {channelPrograms.map((prog) => renderProgram(prog, start))} @@ -856,48 +977,7 @@ export default function TVChannelGuide({ startDate, endDate }) { - {/* Modal for program details */} - - {selectedProgram && ( - <> - - {dayjs(selectedProgram.start_time).format('h:mma')} -{' '} - {dayjs(selectedProgram.end_time).format('h:mma')} - - - {selectedProgram.description || 'No description available.'} - - {/* Only show the Watch button if currently live */} - - {!recording && ( - - )} - - {now.isAfter(dayjs(selectedProgram.start_time)) && - now.isBefore(dayjs(selectedProgram.end_time)) && ( - - )} - - - )} - + {/* Modal removed since we're using expanded rows instead */} ); } diff --git a/frontend/src/pages/guide.css b/frontend/src/pages/guide.css index 93161a5e..5b6568c1 100644 --- a/frontend/src/pages/guide.css +++ b/frontend/src/pages/guide.css @@ -7,9 +7,10 @@ white-space: nowrap; text-overflow: ellipsis; border-radius: 8px; - background: linear-gradient(to right, #2d3748, #2d3748); /* Default background */ + background: linear-gradient(to right, #2d3748, #2d3748); + /* Default background */ color: #fff; - transition: background 0.3s ease; + transition: all 0.2s ease-out; } .tv-guide .guide-program-container .guide-program.live { @@ -23,3 +24,46 @@ .tv-guide .guide-program-container .guide-program.not-live:hover { background: linear-gradient(to right, #2d3748, #1a202c); } + +/* New styles for expanded programs */ +.tv-guide .guide-program-container .guide-program.expanded { + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + z-index: 26; + /* Above other programs but below channel logo */ +} + +.tv-guide .guide-program-container .guide-program.expanded.live { + background: linear-gradient(to right, #1a365d, #1e40af); +} + +.tv-guide .guide-program-container .guide-program.expanded.not-live { + background: linear-gradient(to right, #1a365d, #1e3a8a); +} + +.tv-guide .guide-program-container .guide-program.expanded.past { + background: linear-gradient(to right, #2d3748, #4a5568); +} + +/* Ensure channel logo is always on top */ +.tv-guide .channel-logo { + z-index: 30; + position: relative; + position: sticky !important; + left: 0; + z-index: 30; +} + +/* Give focus to expanded elements */ +.tv-guide .guide-program-container { + transform-origin: left center; + transition: all 0.2s ease-out; +} + +.tv-guide .guide-program-container.expanded { + z-index: 25; +} + +/* Make sure the main scrolling container establishes the right stacking context */ +.tv-guide { + position: relative; +} \ No newline at end of file