Files
TriggerFrontend/src/pages/settings/components/Theming/Themes/Themes.tsx
Sean Edmunds 2f8d3ad58e
All checks were successful
Deploy to production / build (push) Successful in 18s
Added persistent sort to more locations
2026-10-20 18:58:52 -05:00

392 lines
11 KiB
TypeScript

import {
Box,
Button,
Divider,
Group,
LoadingOverlay,
Overlay,
Stack,
Switch,
Text,
Tooltip,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FaDice } from 'react-icons/fa';
import { FiChevronRight, FiEdit, FiMousePointer, FiStopCircle } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { switchClasses, textClasses, toolTipClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import { BackgroundModal } from '../Backgrounds';
import { ThemeCard } from './ThemeCard';
import { ThemeModal } from './ThemeModal';
export function Themes() {
const { settings, updateSetting, theme, isMobile } = useCommonHooks();
const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData, isFetching: allThemeDataIsFetching } = useQueryAPI({
endpoint: 'api/themes',
});
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const allThemes: Theme[] = allThemeData
?.filter((a: Theme) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Theme, b: Theme) => {
switch (settings.themesSettingsSort) {
case 'A>Z':
return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1;
case 'Z>A':
return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? 1 : -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
});
const { data: backgroundData, isFetching: backgroundDataIsFetching } = useQueryAPI({
endpoint: backgroundsEndpoint,
});
const { data: themePlaylistData } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const themePlaylists: ThemePlaylist[] = themePlaylistData || [];
const [themeModalOpened, { open: openThemeModal, close: closeThemeModal }] =
useDisclosure(false);
const [
backgroundModalOpened,
{ open: openBackgroundModal, close: closeBackgroundModal },
] = useDisclosure(false);
const backgroundList = backgroundData
? ['None', ...backgroundData.sort((a: string, b: string) => a.localeCompare(b))]
: ['None'];
const inUseBackgrounds: string[] = [
...new Set(
allThemes
?.filter((t) => {
if (t.backgroundImage !== 'none') {
return t.backgroundImage;
}
return undefined;
})
.map((t) => t.backgroundImage),
),
] as string[];
const [selectedTheme, setSelectedTheme] = useState<Theme>({
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
themeFont: settings.defaultFont,
});
const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false);
const navigate = useNavigate();
const handleNewTheme = () => {
setIsNew(true);
openThemeModal();
};
useEffect(() => {
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [debouncedSearch]);
return (
<Stack>
{themeModalOpened && (
<ThemeModal
opened={themeModalOpened}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
backgroundList={backgroundList}
isNew={isNew}
setIsNew={setIsNew}
allThemes={allThemes}
closeThemeModal={closeThemeModal}
themePlaylists={themePlaylists}
/>
)}
{backgroundModalOpened && (
<BackgroundModal
backgroundModalOpened={backgroundModalOpened}
closeModal={closeBackgroundModal}
backgroundList={backgroundList}
inUseBackgrounds={inUseBackgrounds}
/>
)}
<SettingsHeader
setDragging={undefined}
openModal={handleNewTheme}
title={'Themes'}
tooltip={{
title: 'Themes can be created here and applied to the homepage.',
highlightList: ['theme', 'themes', 'homepage', 'playlist', 'playlists'],
description:
'Themes are combinations of 5 colors and a background image, and can be added to a playlist. If a theme has been disabled, it will only be disabled with random/shuffle is enabled. If a theme is part of a playlist, it will appear while the playlist is active, regardless of random/shuffle.\n\nThemes can also have custom fonts that will override the user default found on the Personalization page.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Themes...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
sortBy={settings.themesSettingsSort}
setSortBy={(value) => {
updateSetting('themesSettingsSort', value);
}}
customSearchEnabled={false}
/>
<Stack mt={isMobile ? '11rem' : '7rem'} align='center'>
<LoadingOverlay
visible={allThemeDataIsFetching || backgroundDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Group
w={'85%'}
justify='center'
pos={'relative'}
mb={isMobile ? '10rem' : '7.5rem'}
>
<Overlay
display={
settings.theme === 'random' && debouncedSearch === '' ? 'flex' : 'none'
}
blur={5}
style={{
zIndex: 190,
borderRadius: '10px',
marginTop: '-1rem',
marginBottom: '-1rem',
boxShadow: '0px 0px 10px 3px rgba(0, 0, 0, .8)',
}}
pos={'absolute'}
/>
{allThemes?.length > 0 ? (
allThemes.map((cardTheme: Theme) => (
<Group key={cardTheme.name} pos={'relative'}>
{cardTheme.name === settings.theme && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 1,
fontSize: '2rem',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: cardTheme.borderColor,
color:
cardTheme.borderColor === cardTheme.primaryColor
? cardTheme.accentColor
: cardTheme.primaryColor,
}}
/>
)}
<Box
style={{
zIndex:
settings.theme === 'random' &&
(themePlaylists
.find(
(t: ThemePlaylist) =>
t.name.toLowerCase() ===
settings.currentThemePlaylist.toLowerCase(),
)
?.themeList.includes(`${cardTheme.id}`) ||
cardTheme.name === theme.name)
? 195
: 0,
}}
>
<ThemeCard
allThemes={allThemeData}
cardTheme={cardTheme}
themePlaylists={themePlaylists}
setSelectedTheme={setSelectedTheme}
openThemeModal={openThemeModal}
/>
</Box>
</Group>
))
) : (
<Group w={'100%'} style={{ display: 'flex', justifyContent: 'center' }}>
<NoItemsFound />
</Group>
)}
</Group>
</Stack>
<Group
style={{
backdropFilter: 'blur(5px) brightness(.75)',
zIndex: 199,
paddingTop: '.5rem',
paddingBottom: '.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
width: '100%',
position: 'absolute',
bottom: isMobile ? '3.25rem' : 0,
}}
>
<Group w={'100%'} justify={isMobile ? 'center' : 'space-between'}>
<Text
w={'fit-content'}
className={textClasses.settingsSubTitle}
classNames={textClasses}
fz={isMobile ? '1rem' : undefined}
style={
isMobile
? {
position: 'absolute',
top: -16,
left: 4,
padding: '.25rem',
backgroundColor: 'var(--mantine-color-background-0)',
backdropFilter: 'blur(5px) brightness(.25)',
borderRadius: '10px',
boxShadow: '2px -2px var(--mantine-color-accent-0)',
}
: undefined
}
>
Current Settings:
</Text>
<Group
style={{
backgroundColor: 'var(--mantine-color-search-0)',
borderRadius: '8px',
paddingTop: '.5rem',
paddingBottom: '.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
}}
>
<Group>
<Text
style={{
color: 'var(--mantine-color-primary-0)',
fontWeight: 'bold',
}}
>
Active Playlist:
</Text>
<Text
style={{
color: 'var(--mantine-color-primary-0)',
fontWeight: 'bold',
fontSize: isMobile ? '.8rem' : '1.2rem',
}}
>
{settings.currentThemePlaylist === 'All'
? 'All'
: settings.currentThemePlaylist}
</Text>
</Group>
<Divider
orientation='vertical'
size={'xs'}
color={'var(--mantine-color-primary-0)'}
/>
<Button
onClick={() => {
if (settings.currentThemePlaylist === 'All') {
void navigate(
`/${import.meta.env.VITE_BASE_URL}/settings/themePlaylists`,
);
} else {
updateSetting('currentThemePlaylist', 'All');
updateSetting('theme', theme.name);
}
}}
style={{
aspectRatio: isMobile ? 1 : undefined,
padding: isMobile ? 0 : undefined,
}}
>
{settings.currentThemePlaylist === 'All' ? (
isMobile ? (
<FiMousePointer />
) : (
'Select Playlist'
)
) : isMobile ? (
<FiStopCircle />
) : (
'Disable Playlist'
)}
</Button>
<Tooltip
className={toolTipClasses.default}
classNames={toolTipClasses}
label='Shuffle is always on when a playlist is active.'
disabled={settings.currentThemePlaylist === 'All'}
>
<Group>
<Switch
disabled={settings.currentThemePlaylist !== 'All'}
label={
isMobile ? (
<FaDice size={20} color={'var(--mantine-color-primary-0)'} />
) : (
<Text c={'var(--mantine-color-primary-0)'}>Shuffle Enabled</Text>
)
}
labelPosition='left'
checked={settings.theme === 'random'}
onChange={(e) => {
updateSetting('theme', e.target.checked ? 'random' : theme.name);
updateSetting('currentThemePlaylist', 'All');
}}
className={switchClasses.default}
classNames={switchClasses}
style={{
zIndex: 250,
}}
/>
</Group>
</Tooltip>
</Group>
<Button
style={{
zIndex: 200,
aspectRatio: isMobile ? 1 : undefined,
padding: isMobile ? 0 : undefined,
}}
onClick={() => {
openBackgroundModal();
}}
>
{isMobile ? <FiEdit /> : 'Manage Backgrounds'}
</Button>
</Group>
</Group>
</Stack>
);
}