missed lots of places we used the old channels map, fixed them up

This commit is contained in:
dekzter 2026-02-17 17:28:59 -05:00
parent f988506a79
commit b24d51c2aa
5 changed files with 172 additions and 41 deletions

View file

@ -50,7 +50,7 @@ const RecordingCard = ({
const { timeFormat: timeformat, dateFormat: dateformat } =
useDateTimeFormat();
const channel = channelProp ?? channels?.[recording.channel];
const channel = channelProp;
const customProps = recording.custom_properties || {};
const program = customProps.program || {};

View file

@ -11,6 +11,7 @@ import {
MultiSelect,
Group,
TextInput,
Loader,
} from '@mantine/core';
import { DateTimePicker, TimeInput, DatePickerInput } from '@mantine/dates';
import { CircleAlert } from 'lucide-react';
@ -87,10 +88,15 @@ const RecordingModal = ({
isOpen,
onClose,
}) => {
const channels = useChannelsStore((s) => s.channels);
const channelGroups = useChannelsStore((s) => s.channelGroups);
const fetchRecordings = useChannelsStore((s) => s.fetchRecordings);
const fetchRecurringRules = useChannelsStore((s) => s.fetchRecurringRules);
// Local state: selected group and channels for that group
const [selectedGroupId, setSelectedGroupId] = useState(null);
const [localChannels, setLocalChannels] = useState([]);
const [isChannelsLoading, setIsChannelsLoading] = useState(false);
const [mode, setMode] = useState('single');
const [submitting, setSubmitting] = useState(false);
@ -210,8 +216,79 @@ const RecordingModal = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, recording, channel]);
// Initialize group selection to the first available group when modal opens
useEffect(() => {
if (!isOpen) return;
if (selectedGroupId) return;
const firstGroup = Object.values(channelGroups || {}).find(
(g) => g?.hasChannels
);
if (firstGroup) {
setSelectedGroupId(String(firstGroup.id));
}
}, [isOpen, selectedGroupId, channelGroups]);
// Fetch channels for selected group (no global load)
useEffect(() => {
let cancelled = false;
const run = async () => {
if (!isOpen) return;
if (!selectedGroupId) return;
const group = channelGroups?.[Number(selectedGroupId)];
if (!group?.name) return;
try {
setIsChannelsLoading(true);
const params = new URLSearchParams();
params.set('channel_group', group.name);
const chans = await API.getChannelsForParams(params);
if (cancelled) return;
setLocalChannels(Array.isArray(chans) ? chans : []);
} catch (e) {
console.warn('Failed to load channels for group', group?.name, e);
if (!cancelled) setLocalChannels([]);
} finally {
if (!cancelled) setIsChannelsLoading(false);
}
};
run();
return () => {
cancelled = true;
};
}, [isOpen, selectedGroupId, channelGroups]);
// When group changes, clear selected channel in both forms
useEffect(() => {
if (!isOpen) return;
singleForm.setFieldValue('channel_id', '');
recurringForm.setFieldValue('channel_id', '');
}, [isOpen, selectedGroupId]);
// After channels load for a group, auto-select the first channel
useEffect(() => {
if (!isOpen) {
return;
}
if (!selectedGroupId) {
return;
}
const first = Array.isArray(localChannels) ? localChannels[0] : null;
if (!first) {
return;
}
if (mode === 'single') {
if (!singleForm.values.channel_id) {
singleForm.setFieldValue('channel_id', `${first.id}`);
}
} else {
if (!recurringForm.values.channel_id) {
recurringForm.setFieldValue('channel_id', `${first.id}`);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, selectedGroupId, localChannels, mode]);
const channelOptions = useMemo(() => {
const list = Object.values(channels || {});
const list = Array.isArray(localChannels) ? [...localChannels] : [];
list.sort((a, b) => {
const aNum = Number(a.channel_number) || 0;
const bNum = Number(b.channel_number) || 0;
@ -222,7 +299,16 @@ const RecordingModal = ({
value: `${item.id}`,
label: item.name || `Channel ${item.id}`,
}));
}, [channels]);
}, [localChannels]);
// Group options (no "All")
const groupOptions = useMemo(() => {
const arr = Object.values(channelGroups || {}).filter(
(g) => g?.hasChannels
);
arr.sort((a, b) => (a?.name || '').localeCompare(b?.name || ''));
return arr.map((g) => ({ value: String(g.id), label: g.name }));
}, [channelGroups]);
const resetForms = () => {
singleForm.reset();
@ -333,23 +419,38 @@ const RecordingModal = ({
<form onSubmit={onSubmit}>
<Stack gap="md">
{/* Group selection (required, no "All") */}
<Select
label="Group"
placeholder="Select group"
data={groupOptions}
value={selectedGroupId}
onChange={(value) => setSelectedGroupId(value)}
searchable
/>
{mode === 'single' ? (
<Select
{...singleForm.getInputProps('channel_id')}
key={singleForm.key('channel_id')}
key={`${singleForm.key('channel_id')}-${selectedGroupId || 'none'}`}
label="Channel"
placeholder="Select channel"
searchable
data={channelOptions}
disabled={isChannelsLoading}
rightSection={
isChannelsLoading ? <Loader size="xs" color="blue" /> : null
}
/>
) : (
<Select
{...recurringForm.getInputProps('channel_id')}
key={recurringForm.key('channel_id')}
key={`${recurringForm.key('channel_id')}-${selectedGroupId || 'none'}`}
label="Channel"
placeholder="Select channel"
searchable
data={channelOptions}
rightSection={isChannelsLoading ? 'Loading…' : null}
/>
)}

View file

@ -1,4 +1,5 @@
import useChannelsStore from '../../store/channels.jsx';
import API from '../../api';
import {
useDateTimeFormat,
useTimeHelpers,
@ -43,7 +44,8 @@ const RecordingDetailsModal = ({
onEdit,
}) => {
const allRecordings = useChannelsStore((s) => s.recordings);
const channelMap = useChannelsStore((s) => s.channels);
// Local channel cache to avoid the global channels map
const [channelsById, setChannelsById] = React.useState({});
const { toUserTime, userNow } = useTimeHelpers();
const [childOpen, setChildOpen] = React.useState(false);
const [childRec, setChildRec] = React.useState(null);
@ -91,6 +93,42 @@ const RecordingDetailsModal = ({
userNow,
]);
// Ensure channel is available for a given id
const loadChannel = React.useCallback(
async (id) => {
if (!id) {
return null;
}
const existing = channelsById[id];
if (existing) {
return existing;
}
try {
const ch = await API.getChannel(id);
if (ch && ch.id === id) {
setChannelsById((prev) => ({ ...prev, [id]: ch }));
return ch;
}
} catch (e) {
console.warn(
'Failed to fetch channel for RecordingDetailsModal',
id,
e
);
}
return null;
},
[channelsById]
);
// When opening a child episode, fetch that episode's channel
React.useEffect(() => {
if (!childOpen || !childRec) return;
loadChannel(childRec.channel);
}, [childOpen, childRec, loadChannel]);
const handleOnWatchLive = () => {
const rec = childRec;
const now = userNow();
@ -98,10 +136,11 @@ const RecordingDetailsModal = ({
const e = toUserTime(rec.end_time);
if (now.isAfter(s) && now.isBefore(e)) {
if (!channelMap[rec.channel]) return;
useVideoStore
.getState()
.showVideo(getShowVideoUrl(channelMap[rec.channel], env_mode), 'live');
const ch =
channelsById[rec.channel] ||
(rec.channel === recording?.channel ? channel : null);
if (!ch) return;
useVideoStore.getState().showVideo(getShowVideoUrl(ch, env_mode), 'live');
}
};
@ -109,13 +148,16 @@ const RecordingDetailsModal = ({
let fileUrl = getRecordingUrl(childRec.custom_properties, env_mode);
if (!fileUrl) return;
const ch =
channelsById[childRec.channel] ||
(childRec.channel === recording?.channel ? channel : null);
useVideoStore.getState().showVideo(fileUrl, 'vod', {
name: childRec.custom_properties?.program?.title || 'Recording',
logo: {
url: getPosterUrl(
childRec.custom_properties?.poster_logo_id,
undefined,
channelMap[childRec.channel]?.logo?.cache_url
ch?.logo?.cache_url
),
},
});
@ -169,6 +211,7 @@ const RecordingDetailsModal = ({
setChildRec(rec);
setChildOpen(true);
};
return (
<Card
withBorder
@ -281,11 +324,11 @@ const RecordingDetailsModal = ({
opened={childOpen}
onClose={() => setChildOpen(false)}
recording={childRec}
channel={channelMap[childRec.channel]}
channel={channelsById[childRec.channel]}
posterUrl={getPosterUrl(
childRec.custom_properties?.poster_logo_id,
childRec.custom_properties,
channelMap[childRec.channel]?.logo?.cache_url
channelsById[childRec.channel]?.logo?.cache_url
)}
env_mode={env_mode}
onWatchLive={handleOnWatchLive}

View file

@ -257,10 +257,6 @@ const ChannelsTable = ({ onReady }) => {
const theme = useMantineTheme();
const channelGroups = useChannelsStore((s) => s.channelGroups);
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
const canDeleteChannelGroup = useChannelsStore(
(s) => s.canDeleteChannelGroup
);
const hasSignaledReady = useRef(false);
/**
@ -379,28 +375,12 @@ const ChannelsTable = ({ onReady }) => {
/**
* Derived variables
*/
const activeGroupIds = new Set(
Object.values(channels).map((channel) => channel.channel_group_id)
);
const groupOptions = Object.values(channelGroups)
.filter((group) => activeGroupIds.has(group.id))
.map((group) => group.name);
const groupOptions = Object.values(channelGroups).map((group) => group.name);
// Get unique EPG sources from active channels
const activeEPGSources = new Set();
let hasUnlinkedChannels = false;
Object.values(channels).forEach((channel) => {
if (channel.epg_data_id) {
const epgObj = tvgsById[channel.epg_data_id];
if (epgObj && epgObj.epg_source) {
const epgName = epgs[epgObj.epg_source]?.name || epgObj.epg_source;
activeEPGSources.add(epgName);
}
} else {
hasUnlinkedChannels = true;
}
});
const epgOptions = Array.from(activeEPGSources).sort();
const epgOptions = Object.values(epgs)
.map((epg) => epg.name)
.sort();
if (hasUnlinkedChannels) {
epgOptions.unshift('No EPG');
}
@ -594,6 +574,10 @@ const ChannelsTable = ({ onReady }) => {
const deleteChannel = async (id) => {
console.log(`Deleting channel with ID: ${id}`);
const rows = table.getRowModel().rows;
const knownChannel = rows.find((row) => row.original.id === id)?.original;
table.setSelectedTableIds([]);
if (selectedChannelIds.length > 0) {
@ -613,7 +597,7 @@ const ChannelsTable = ({ onReady }) => {
// Single channel delete
setIsBulkDelete(false);
setDeleteTarget(id);
setChannelToDelete(channels[id]); // Store the channel object for displaying details
setChannelToDelete(knownChannel); // Store the channel object for displaying details
if (isWarningSuppressed('delete-channel')) {
// Skip warning if suppressed

View file

@ -142,7 +142,10 @@ export default function TVChannelGuide({ startDate, endDate }) {
try {
const params = new URLSearchParams();
const ids = await API.getAllChannelIds(params);
if (cancelled) return;
if (cancelled) {
return;
}
const total = Array.isArray(ids)
? ids.length
: (ids?.length ?? ids?.count ?? 0);
@ -1160,7 +1163,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
value={selectedGroupId}
onChange={handleGroupChange} // Use the new handler
w={'220px'}
clearable={true} // Allow clearing the selection
clearable={allowAllGroups} // Allow clearing the selection
/>
<Select