Merge branch 'logosstore' of https://github.com/Dispatcharr/Dispatcharr into vod-relationtest

This commit is contained in:
SergeantPanda 2025-08-22 12:09:45 -05:00
commit 48c4cd8ca9
16 changed files with 409 additions and 108 deletions

View file

@ -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':

View file

@ -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(() => {

View file

@ -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;

View 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;

View file

@ -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>

View file

@ -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>

View file

@ -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 }}
/>

View file

@ -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

View file

@ -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;

View 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,
};
};

View file

@ -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);

View file

@ -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 }}>

View file

@ -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({});

View file

@ -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);
}

View file

@ -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,
})),

View 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;