mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
missed lots of places we used the old channels map, fixed them up
This commit is contained in:
parent
f988506a79
commit
b24d51c2aa
5 changed files with 172 additions and 41 deletions
|
|
@ -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 || {};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue