Files
TriggerFrontend/src/pages/settings/components/Theming/Themes/Themes.tsx
2025-09-03 16:11:10 -05:00

375 lines
10 KiB
TypeScript

import {
Box,
Button,
Divider,
Group,
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 } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import { BackgroundModal } from '../Backgrounds';
import { ThemeCard } from './ThemeCard';
import { ThemeModal } from './ThemeModal';
export function Themes() {
const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData, isFetched: isAllThemeDataFetched } = useQueryAPI({
endpoint: 'api/themes',
});
const [allThemes, setAllThemes] = useState<Theme[]>([]);
const { data: backgroundData, isFetched: isBackgroundFetched } = useQueryAPI({
endpoint: backgroundsEndpoint,
});
const { data: themePlaylistData } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
const [themeModalOpened, { open: openThemeModal, close: closeThemeModal }] =
useDisclosure(false);
const [
backgroundModalOpened,
{ open: openBackgroundModal, close: closeBackgroundModal },
] = useDisclosure(false);
const [backgroundList, setBackgroundList] = useState<string[]>(['None']);
const [inUseBackgrounds, setInUseBackgrounds] = useState<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,
});
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false);
const navigate = useNavigate();
const {
currentTheme,
setCurrentTheme,
theme,
isMobile,
currentThemePlaylist,
setCurrentThemePlaylist,
} = useCommonHooks();
const handleNewTheme = () => {
setIsNew(true);
openThemeModal();
};
useEffect(() => {
if (backgroundData && isBackgroundFetched) {
setBackgroundList(['None', ...backgroundData]);
}
}, [backgroundData, isBackgroundFetched]);
useEffect(() => {
if (themePlaylistData) {
setThemePlaylists(themePlaylistData);
}
}, [themePlaylistData]);
useEffect(() => {
if (allThemeData && isAllThemeDataFetched) {
setAllThemes(
allThemeData.filter((a: Theme) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [allThemeData, isAllThemeDataFetched, debouncedSearch]);
useEffect(() => {
if (allThemes) {
setInUseBackgrounds([
...new Set(
allThemes
.filter((t) => {
if (t.backgroundImage !== 'none') {
return t.backgroundImage;
}
return undefined;
})
.map((t) => t.backgroundImage),
),
]);
}
}, [allThemes]);
return (
<Stack>
<ThemeModal
opened={themeModalOpened}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
backgroundList={backgroundList}
isNew={isNew}
setIsNew={setIsNew}
allThemes={allThemes}
closeThemeModal={closeThemeModal}
themePlaylists={themePlaylists}
/>
<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'],
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.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Themes...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Stack mt={isMobile ? '11rem' : '7rem'} align='center'>
<Group
w={'85%'}
justify='center'
pos={'relative'}
mb={isMobile ? '10rem' : '7.5rem'}
>
<Overlay
display={
currentTheme === '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 === currentTheme && (
<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:
currentTheme === 'random' &&
(themePlaylists
.filter(
(t: ThemePlaylist) =>
t.name.toLowerCase() === currentThemePlaylist.toLowerCase(),
)[0]
?.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={{ zIndex: 1000, 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',
}}
>
{currentThemePlaylist === 'All' ? 'All' : currentThemePlaylist}
</Text>
</Group>
<Divider
orientation='vertical'
size={'xs'}
color={'var(--mantine-color-primary-0)'}
/>
<Button
onClick={() => {
if (currentThemePlaylist === 'All') {
navigate(`/${import.meta.env.VITE_BASE_URL}/settings/themePlaylists`);
} else {
setCurrentThemePlaylist('All');
setCurrentTheme(theme.name);
}
}}
style={{
aspectRatio: isMobile ? 1 : undefined,
padding: isMobile ? 0 : undefined,
}}
>
{currentThemePlaylist === 'All' ? (
isMobile ? (
<FiMousePointer />
) : (
'Select Playlist'
)
) : isMobile ? (
<FiStopCircle />
) : (
'Disable Playlist'
)}
</Button>
<Tooltip
label='Shuffle is always on when a playlist is active.'
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
disabled={currentThemePlaylist === 'All'}
>
<Group>
<Switch
disabled={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={currentTheme === 'random'}
onChange={(e) => {
setCurrentTheme(e.target.checked ? 'random' : theme.name);
setCurrentThemePlaylist('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>
);
}