mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 10:45:27 +00:00
Merge branch 'logosstore' of https://github.com/Dispatcharr/Dispatcharr into vod-relationtest
This commit is contained in:
commit
48c4cd8ca9
16 changed files with 409 additions and 108 deletions
|
|
@ -1280,6 +1280,18 @@ class LogoViewSet(viewsets.ModelViewSet):
|
|||
"""Optimize queryset with prefetch and add filtering"""
|
||||
queryset = Logo.objects.prefetch_related('channels').order_by('name')
|
||||
|
||||
# Filter by specific IDs
|
||||
ids = self.request.query_params.getlist('ids')
|
||||
if ids:
|
||||
try:
|
||||
# Convert string IDs to integers and filter
|
||||
id_list = [int(id_str) for id_str in ids if id_str.isdigit()]
|
||||
if id_list:
|
||||
queryset = queryset.filter(id__in=id_list)
|
||||
except (ValueError, TypeError):
|
||||
pass # Invalid IDs, return empty queryset
|
||||
queryset = Logo.objects.none()
|
||||
|
||||
# Filter by usage
|
||||
used_filter = self.request.query_params.get('used', None)
|
||||
if used_filter == 'true':
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import React, {
|
|||
} from 'react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import useChannelsStore from './store/channels';
|
||||
import useLogosStore from './store/logos';
|
||||
import usePlaylistsStore from './store/playlists';
|
||||
import useEPGsStore from './store/epgs';
|
||||
import { Box, Button, Stack, Alert, Group } from '@mantine/core';
|
||||
|
|
@ -499,7 +500,7 @@ export const WebsocketProvider = ({ children }) => {
|
|||
const setProfilePreview = usePlaylistsStore((s) => s.setProfilePreview);
|
||||
const fetchEPGData = useEPGsStore((s) => s.fetchEPGData);
|
||||
const fetchEPGs = useEPGsStore((s) => s.fetchEPGs);
|
||||
const fetchLogos = useChannelsStore((s) => s.fetchLogos);
|
||||
const fetchLogos = useLogosStore((s) => s.fetchLogos);
|
||||
const fetchChannelProfiles = useChannelsStore((s) => s.fetchChannelProfiles);
|
||||
|
||||
const ret = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// src/api.js (updated)
|
||||
import useAuthStore from './store/auth';
|
||||
import useChannelsStore from './store/channels';
|
||||
import useLogosStore from './store/logos';
|
||||
import useUserAgentsStore from './store/userAgents';
|
||||
import usePlaylistsStore from './store/playlists';
|
||||
import useEPGsStore from './store/epgs';
|
||||
|
|
@ -1293,16 +1294,52 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async getLogosByIds(logoIds) {
|
||||
try {
|
||||
if (!logoIds || logoIds.length === 0) return [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
logoIds.forEach(id => params.append('ids', id));
|
||||
|
||||
const response = await request(
|
||||
`${host}/api/channels/logos/?${params.toString()}`
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to retrieve logos by IDs', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchLogos() {
|
||||
try {
|
||||
const response = await this.getLogos();
|
||||
useChannelsStore.getState().setLogos(response);
|
||||
useLogosStore.getState().setLogos(response);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to fetch logos', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchUsedLogos() {
|
||||
try {
|
||||
const response = await useLogosStore.getState().fetchUsedLogos();
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to fetch used logos', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchLogosByIds(logoIds) {
|
||||
try {
|
||||
const response = await useLogosStore.getState().fetchLogosByIds(logoIds);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to fetch logos by IDs', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async uploadLogo(file) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
|
@ -1340,7 +1377,7 @@ export default class API {
|
|||
}
|
||||
|
||||
const result = await response.json();
|
||||
useChannelsStore.getState().addLogo(result);
|
||||
useLogosStore.getState().addLogo(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
|
|
@ -1368,7 +1405,7 @@ export default class API {
|
|||
body: formData,
|
||||
});
|
||||
|
||||
useChannelsStore.getState().addLogo(response);
|
||||
useLogosStore.getState().addLogo(response);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -1383,7 +1420,7 @@ export default class API {
|
|||
body: values, // This will be converted to JSON in the request function
|
||||
});
|
||||
|
||||
useChannelsStore.getState().updateLogo(response);
|
||||
useLogosStore.getState().updateLogo(response);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -1403,7 +1440,7 @@ export default class API {
|
|||
method: 'DELETE',
|
||||
});
|
||||
|
||||
useChannelsStore.getState().removeLogo(id);
|
||||
useLogosStore.getState().removeLogo(id);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -1425,7 +1462,7 @@ export default class API {
|
|||
|
||||
// Remove multiple logos from store
|
||||
ids.forEach((id) => {
|
||||
useChannelsStore.getState().removeLogo(id);
|
||||
useLogosStore.getState().removeLogo(id);
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
|
|||
66
frontend/src/components/LazyLogo.jsx
Normal file
66
frontend/src/components/LazyLogo.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Skeleton } from '@mantine/core';
|
||||
import useLogosStore from '../store/logos';
|
||||
import logo from '../images/logo.png'; // Default logo
|
||||
|
||||
const LazyLogo = ({
|
||||
logoId,
|
||||
alt = 'logo',
|
||||
style = { maxHeight: 18, maxWidth: 55 },
|
||||
fallbackSrc = logo,
|
||||
...props
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds);
|
||||
|
||||
// Determine the logo source
|
||||
const logoData = logoId && logos[logoId];
|
||||
const logoSrc = logoData?.cache_url || (logoId ? `/api/channels/logos/${logoId}/cache/` : fallbackSrc);
|
||||
|
||||
useEffect(() => {
|
||||
// If we have a logoId but no logo data, try to fetch it
|
||||
if (logoId && !logoData && !isLoading && !hasError) {
|
||||
setIsLoading(true);
|
||||
fetchLogosByIds([logoId])
|
||||
.then(() => {
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Failed to load logo ${logoId}:`, error);
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
});
|
||||
}
|
||||
}, [logoId, logoData, fetchLogosByIds, isLoading, hasError]);
|
||||
|
||||
// Show skeleton while loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Skeleton
|
||||
height={style.maxHeight || 18}
|
||||
width={style.maxWidth || 55}
|
||||
style={{ ...style, borderRadius: 4 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show image (will use fallback if logo fails to load)
|
||||
return (
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt={alt}
|
||||
style={style}
|
||||
onError={(e) => {
|
||||
if (!hasError) {
|
||||
setHasError(true);
|
||||
e.target.src = fallbackSrc;
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LazyLogo;
|
||||
|
|
@ -2,12 +2,15 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import ChannelGroupForm from './ChannelGroup';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import logo from '../../images/logo.png';
|
||||
import { useLogoSelection } from '../../hooks/useSmartLogos';
|
||||
import LazyLogo from '../LazyLogo';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
|
@ -48,8 +51,8 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
const channelGroups = useChannelsStore((s) => s.channelGroups);
|
||||
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
|
||||
|
||||
const logos = useChannelsStore((s) => s.logos);
|
||||
const fetchLogos = useChannelsStore((s) => s.fetchLogos);
|
||||
const { logos, ensureLogosLoaded, isLoading: logosLoading } = useLogoSelection();
|
||||
const fetchLogos = useLogosStore((s) => s.fetchLogos);
|
||||
const streams = useStreamsStore((state) => state.streams);
|
||||
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
|
||||
const playlists = usePlaylistsStore((s) => s.playlists);
|
||||
|
|
@ -193,10 +196,10 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
|
||||
formik.resetForm();
|
||||
API.requeryChannels();
|
||||
|
||||
|
||||
// Refresh channel profiles to update the membership information
|
||||
useChannelsStore.getState().fetchChannelProfiles();
|
||||
|
||||
|
||||
setSubmitting(false);
|
||||
setTvgFilter('');
|
||||
setLogoFilter('');
|
||||
|
|
@ -471,7 +474,13 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
<Group justify="space-between">
|
||||
<Popover
|
||||
opened={logoPopoverOpened}
|
||||
onChange={setLogoPopoverOpened}
|
||||
onChange={(opened) => {
|
||||
setLogoPopoverOpened(opened);
|
||||
// Load all logos when popover is opened
|
||||
if (opened) {
|
||||
ensureLogosLoaded();
|
||||
}
|
||||
}}
|
||||
// position="bottom-start"
|
||||
withArrow
|
||||
>
|
||||
|
|
@ -530,13 +539,10 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<img
|
||||
src={
|
||||
logos[formik.values.logo_id]
|
||||
? logos[formik.values.logo_id].cache_url
|
||||
: logo
|
||||
}
|
||||
height="40"
|
||||
<LazyLogo
|
||||
logoId={formik.values.logo_id}
|
||||
alt="channel logo"
|
||||
style={{ height: 40 }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import { useLogoSelection } from '../../hooks/useSmartLogos';
|
||||
import LazyLogo from '../LazyLogo';
|
||||
import ChannelGroupForm from './ChannelGroup';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import logo from '../../images/logo.png';
|
||||
|
|
@ -45,8 +49,8 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
const groupListRef = useRef(null);
|
||||
|
||||
const channelGroups = useChannelsStore((s) => s.channelGroups);
|
||||
const logos = useChannelsStore((s) => s.logos);
|
||||
const fetchLogos = useChannelsStore((s) => s.fetchLogos);
|
||||
const { logos, ensureLogosLoaded } = useLogoSelection();
|
||||
const fetchLogos = useLogosStore((s) => s.fetchLogos);
|
||||
const streams = useStreamsStore((state) => state.streams);
|
||||
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
|
||||
const playlists = usePlaylistsStore((s) => s.playlists);
|
||||
|
|
@ -189,10 +193,10 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
|
||||
formik.resetForm();
|
||||
API.requeryChannels();
|
||||
|
||||
|
||||
// Refresh channel profiles to update the membership information
|
||||
useChannelsStore.getState().fetchChannelProfiles();
|
||||
|
||||
|
||||
setSubmitting(false);
|
||||
setTvgFilter('');
|
||||
setLogoFilter('');
|
||||
|
|
@ -448,7 +452,12 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
<Group justify="space-between">
|
||||
<Popover
|
||||
opened={logoPopoverOpened}
|
||||
onChange={setLogoPopoverOpened}
|
||||
onChange={(opened) => {
|
||||
setLogoPopoverOpened(opened);
|
||||
if (opened) {
|
||||
ensureLogosLoaded();
|
||||
}
|
||||
}}
|
||||
// position="bottom-start"
|
||||
withArrow
|
||||
>
|
||||
|
|
@ -507,13 +516,10 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<img
|
||||
src={
|
||||
logos[formik.values.logo_id]
|
||||
? logos[formik.values.logo_id].cache_url
|
||||
: logo
|
||||
}
|
||||
height="40"
|
||||
<LazyLogo
|
||||
logoId={formik.values.logo_id}
|
||||
alt="channel logo"
|
||||
style={{ height: 40 }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import API from '../../api';
|
||||
import ChannelForm from '../forms/Channel';
|
||||
|
|
@ -49,6 +50,7 @@ import { getCoreRowModel, flexRender } from '@tanstack/react-table';
|
|||
import './table.css';
|
||||
import useChannelsTableStore from '../../store/channelsTable';
|
||||
import ChannelTableStreams from './ChannelTableStreams';
|
||||
import LazyLogo from '../LazyLogo';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
import ChannelsTableOnboarding from './ChannelsTable/ChannelsTableOnboarding';
|
||||
|
|
@ -244,7 +246,7 @@ const ChannelsTable = ({ }) => {
|
|||
const channels = useChannelsStore((s) => s.channels);
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
|
||||
const logos = useChannelsStore((s) => s.logos);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const [tablePrefs, setTablePrefs] = useLocalStorage('channel-table-prefs', {
|
||||
pageSize: 50,
|
||||
});
|
||||
|
|
@ -717,18 +719,11 @@ const ChannelsTable = ({ }) => {
|
|||
header: '',
|
||||
cell: ({ getValue }) => {
|
||||
const logoId = getValue();
|
||||
let src = logo; // Default fallback
|
||||
|
||||
if (logoId && logos[logoId]) {
|
||||
// Try to use cache_url if available, otherwise construct it from the ID
|
||||
src =
|
||||
logos[logoId].cache_url || `/api/channels/logos/${logoId}/cache/`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Center style={{ width: '100%' }}>
|
||||
<img
|
||||
src={src}
|
||||
<LazyLogo
|
||||
logoId={logoId}
|
||||
alt="logo"
|
||||
style={{ maxHeight: 18, maxWidth: 55 }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import API from '../../api';
|
||||
import LogoForm from '../forms/Logo';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import {
|
||||
SquarePlus,
|
||||
|
|
@ -83,7 +83,7 @@ const LogosTable = () => {
|
|||
/**
|
||||
* STORES
|
||||
*/
|
||||
const { logos, fetchLogos } = useChannelsStore();
|
||||
const { logos, fetchLogos } = useLogosStore();
|
||||
|
||||
/**
|
||||
* useState
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import API from '../../api';
|
|||
import StreamForm from '../forms/Stream';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import { copyToClipboard, useDebounce } from '../../utils';
|
||||
import {
|
||||
SquarePlus,
|
||||
|
|
@ -59,7 +60,7 @@ const StreamRowActions = ({
|
|||
(state) =>
|
||||
state.channels.find((chan) => chan.id === selectedChannelIds[0])?.streams
|
||||
);
|
||||
const fetchLogos = useChannelsStore((s) => s.fetchLogos);
|
||||
const fetchLogos = useLogosStore((s) => s.fetchLogos);
|
||||
|
||||
const createChannelFromStream = async () => {
|
||||
const selectedChannelProfileId = useChannelsStore.getState().selectedProfileId;
|
||||
|
|
|
|||
67
frontend/src/hooks/useSmartLogos.jsx
Normal file
67
frontend/src/hooks/useSmartLogos.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import useLogosStore from '../store/logos';
|
||||
|
||||
/**
|
||||
* Hook for components that need to display all logos (like logo selection popovers)
|
||||
* Loads logos on-demand when the component is opened
|
||||
*/
|
||||
export const useLogoSelection = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogos = useLogosStore((s) => s.fetchLogos); // Check if we have a reasonable number of logos loaded
|
||||
const hasEnoughLogos = Object.keys(logos).length > 0;
|
||||
|
||||
const ensureLogosLoaded = useCallback(async () => {
|
||||
if (isLoading || (hasEnoughLogos && isInitialized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await fetchLogos();
|
||||
setIsInitialized(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to load logos for selection:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, hasEnoughLogos, isInitialized, fetchLogos]);
|
||||
|
||||
return {
|
||||
logos,
|
||||
isLoading,
|
||||
ensureLogosLoaded,
|
||||
hasLogos: hasEnoughLogos,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for components that need specific logos by IDs
|
||||
*/
|
||||
export const useLogosById = (logoIds = []) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds); // Find missing logos
|
||||
const missingIds = logoIds.filter(id => id && !logos[id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (missingIds.length > 0 && !isLoading) {
|
||||
setIsLoading(true);
|
||||
fetchLogosByIds(missingIds)
|
||||
.then(() => setIsLoading(false))
|
||||
.catch((error) => {
|
||||
console.error('Failed to load logos by IDs:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [missingIds.length, isLoading, fetchLogosByIds]);
|
||||
|
||||
return {
|
||||
logos,
|
||||
isLoading,
|
||||
missingLogos: missingIds.length,
|
||||
};
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import React, { useMemo, useState, useEffect, useRef } from 'react';
|
|||
import dayjs from 'dayjs';
|
||||
import API from '../api';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import useLogosStore from '../store/logos';
|
||||
import logo from '../images/logo.png';
|
||||
import useVideoStore from '../store/useVideoStore'; // NEW import
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
|
@ -39,7 +40,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
const recordings = useChannelsStore((s) => s.recordings);
|
||||
const channelGroups = useChannelsStore((s) => s.channelGroups);
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const logos = useChannelsStore((s) => s.logos);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
|
||||
const tvgsById = useEPGsStore((s) => s.tvgsById);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import useLogosStore from '../store/logos';
|
||||
import LogosTable from '../components/tables/LogosTable';
|
||||
|
||||
const LogosPage = () => {
|
||||
const { fetchLogos } = useChannelsStore();
|
||||
const { fetchLogos, logos } = useLogosStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadLogos();
|
||||
}, []);
|
||||
|
||||
const loadLogos = async () => {
|
||||
const loadLogos = useCallback(async () => {
|
||||
try {
|
||||
await fetchLogos();
|
||||
} catch (error) {
|
||||
// Only fetch all logos if we don't have any yet
|
||||
if (Object.keys(logos).length === 0) {
|
||||
await fetchLogos();
|
||||
}
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to load logos',
|
||||
color: 'red',
|
||||
});
|
||||
console.error('Failed to load logos:', err);
|
||||
}
|
||||
};
|
||||
}, [fetchLogos, logos]);
|
||||
|
||||
useEffect(() => {
|
||||
loadLogos();
|
||||
}, [loadLogos]);
|
||||
|
||||
return (
|
||||
<Box style={{ padding: 10 }}>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { TableHelper } from '../helpers';
|
||||
import API from '../api';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import useLogosStore from '../store/logos';
|
||||
import logo from '../images/logo.png';
|
||||
import {
|
||||
Gauge,
|
||||
|
|
@ -699,7 +700,7 @@ const ChannelsPage = () => {
|
|||
const channels = useChannelsStore((s) => s.channels);
|
||||
const channelsByUUID = useChannelsStore((s) => s.channelsByUUID);
|
||||
const channelStats = useChannelsStore((s) => s.stats);
|
||||
const logos = useChannelsStore((s) => s.logos); // Add logos from the store
|
||||
const logos = useLogosStore((s) => s.logos); // Add logos from the store
|
||||
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
|
||||
|
||||
const [activeChannels, setActiveChannels] = useState({});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { create } from 'zustand';
|
|||
import api from '../api';
|
||||
import useSettingsStore from './settings';
|
||||
import useChannelsStore from './channels';
|
||||
import useLogosStore from './logos';
|
||||
import usePlaylistsStore from './playlists';
|
||||
import useEPGsStore from './epgs';
|
||||
import useStreamProfilesStore from './streamProfiles';
|
||||
|
|
@ -47,7 +48,7 @@ const useAuthStore = create((set, get) => ({
|
|||
await useSettingsStore.getState().fetchSettings();
|
||||
|
||||
try {
|
||||
// Only after settings are loaded, fetch the dependent data
|
||||
// Load essential data first (without all logos)
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
|
|
@ -55,17 +56,25 @@ const useAuthStore = create((set, get) => ({
|
|||
usePlaylistsStore.getState().fetchPlaylists(),
|
||||
useEPGsStore.getState().fetchEPGs(),
|
||||
useEPGsStore.getState().fetchEPGData(),
|
||||
useChannelsStore.getState().fetchLogos(),
|
||||
useStreamProfilesStore.getState().fetchProfiles(),
|
||||
useUserAgentsStore.getState().fetchUserAgents(),
|
||||
useVODStore.getState().fetchCategories(), // Add VOD categories
|
||||
]);
|
||||
|
||||
// Load only logos that are currently used by channels (much faster)
|
||||
await useLogosStore.getState().fetchUsedLogos();
|
||||
|
||||
if (user.user_level >= USER_LEVELS.ADMIN) {
|
||||
await Promise.all([useUsersStore.getState().fetchUsers()]);
|
||||
}
|
||||
|
||||
set({ user, isAuthenticated: true });
|
||||
|
||||
// Start background loading of remaining logos after login is complete
|
||||
setTimeout(() => {
|
||||
useLogosStore.getState().fetchLogosInBackground();
|
||||
}, 2000); // 2 second delay to let UI settle
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing data:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ const useChannelsStore = create((set, get) => ({
|
|||
stats: {},
|
||||
activeChannels: {},
|
||||
activeClients: {},
|
||||
logos: {},
|
||||
recordings: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
|
@ -215,52 +214,6 @@ const useChannelsStore = create((set, get) => ({
|
|||
return { channelGroups: remainingGroups };
|
||||
}),
|
||||
|
||||
fetchLogos: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const logos = await api.getLogos();
|
||||
set({
|
||||
logos: logos.reduce((acc, logo) => {
|
||||
acc[logo.id] = {
|
||||
...logo,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logos:', error);
|
||||
set({ error: 'Failed to load logos.', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addLogo: (newLogo) =>
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
[newLogo.id]: {
|
||||
...newLogo,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
updateLogo: (logo) =>
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
[logo.id]: {
|
||||
...logo,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
removeLogo: (logoId) =>
|
||||
set((state) => {
|
||||
const newLogos = { ...state.logos };
|
||||
delete newLogos[logoId];
|
||||
return { logos: newLogos };
|
||||
}),
|
||||
|
||||
addProfile: (profile) =>
|
||||
set((state) => ({
|
||||
profiles: {
|
||||
|
|
@ -348,10 +301,10 @@ const useChannelsStore = create((set, get) => ({
|
|||
}),
|
||||
|
||||
setChannelsPageSelection: (channelsPageSelection) =>
|
||||
set((state) => ({ channelsPageSelection })),
|
||||
set(() => ({ channelsPageSelection })),
|
||||
|
||||
setSelectedProfileId: (id) =>
|
||||
set((state) => ({
|
||||
set(() => ({
|
||||
selectedProfileId: id,
|
||||
})),
|
||||
|
||||
|
|
|
|||
142
frontend/src/store/logos.jsx
Normal file
142
frontend/src/store/logos.jsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api';
|
||||
|
||||
const useLogosStore = create((set, get) => ({
|
||||
logos: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Basic CRUD operations
|
||||
setLogos: (logos) => {
|
||||
set({
|
||||
logos: logos.reduce((acc, logo) => {
|
||||
acc[logo.id] = { ...logo };
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
},
|
||||
|
||||
addLogo: (newLogo) =>
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
[newLogo.id]: { ...newLogo },
|
||||
},
|
||||
})),
|
||||
|
||||
updateLogo: (logo) =>
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
[logo.id]: { ...logo },
|
||||
},
|
||||
})),
|
||||
|
||||
removeLogo: (logoId) =>
|
||||
set((state) => {
|
||||
const newLogos = { ...state.logos };
|
||||
delete newLogos[logoId];
|
||||
return { logos: newLogos };
|
||||
}),
|
||||
|
||||
// Smart loading methods
|
||||
fetchLogos: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const logos = await api.getLogos();
|
||||
set({
|
||||
logos: logos.reduce((acc, logo) => {
|
||||
acc[logo.id] = { ...logo };
|
||||
return acc;
|
||||
}, {}),
|
||||
isLoading: false,
|
||||
});
|
||||
return logos;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logos:', error);
|
||||
set({ error: 'Failed to load logos.', isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
fetchUsedLogos: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const logos = await api.getLogos({ used: 'true' });
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
...logos.reduce((acc, logo) => {
|
||||
acc[logo.id] = { ...logo };
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
isLoading: false,
|
||||
}));
|
||||
return logos;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch used logos:', error);
|
||||
set({ error: 'Failed to load used logos.', isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
fetchLogosByIds: async (logoIds) => {
|
||||
if (!logoIds || logoIds.length === 0) return [];
|
||||
|
||||
try {
|
||||
// Filter out logos we already have
|
||||
const missingIds = logoIds.filter(id => !get().logos[id]);
|
||||
if (missingIds.length === 0) return [];
|
||||
|
||||
const logos = await api.getLogosByIds(missingIds);
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
...logos.reduce((acc, logo) => {
|
||||
acc[logo.id] = { ...logo };
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
}));
|
||||
return logos;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logos by IDs:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
fetchLogosInBackground: async () => {
|
||||
try {
|
||||
// Load all remaining logos in background
|
||||
const allLogos = await api.getLogos();
|
||||
set((state) => ({
|
||||
logos: {
|
||||
...state.logos,
|
||||
...allLogos.reduce((acc, logo) => {
|
||||
acc[logo.id] = { ...logo };
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Background logo loading failed:', error);
|
||||
// Don't throw error for background loading
|
||||
}
|
||||
},
|
||||
|
||||
// Helper methods
|
||||
getLogoById: (logoId) => {
|
||||
return get().logos[logoId] || null;
|
||||
},
|
||||
|
||||
hasLogo: (logoId) => {
|
||||
return !!get().logos[logoId];
|
||||
},
|
||||
|
||||
getLogosCount: () => {
|
||||
return Object.keys(get().logos).length;
|
||||
},
|
||||
}));
|
||||
|
||||
export default useLogosStore;
|
||||
Loading…
Add table
Add a link
Reference in a new issue