Style updates and bugfixes

This commit is contained in:
Sean Edmunds
2025-07-17 17:01:31 -05:00
parent ea216e9da2
commit 5e5a077ec3
48 changed files with 565 additions and 524 deletions

View File

@@ -0,0 +1,205 @@
import { Box, Button, Group, Overlay, Stack, Text } from '@mantine/core';
import {
useDebouncedState,
useDisclosure,
useElementSize,
useHover,
} from '@mantine/hooks';
import { DeleteItemModal } from 'components';
import { useCommonHooks } from 'hooks';
import { boxClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import { ThemeManagementPill } from '.';
export function ThemeCard({
cardTheme,
handleOpen,
allThemes,
themePlaylists,
}: {
cardTheme: Theme;
handleOpen: (cardTheme: Theme) => void;
allThemes: Theme[];
themePlaylists: ThemePlaylist[];
}) {
const { hovered, ref: hoverRef } = useHover();
const { ref, width } = useElementSize();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { currentTheme, setCurrentTheme, setCurrentThemePlaylist, handleDelete } =
useCommonHooks();
const [hoverPreview, setHoverPreview] = useDebouncedState(false, 1000);
return (
<Group
gap={0}
align='center'
justify='center'
display={'grid'}
pos={'relative'}
ref={hoverRef}
>
<DeleteItemModal
modalOpened={modalOpened}
handleClose={closeModal}
handleDelete={() => handleDelete(cardTheme, 'api/themes')}
title={'Delete Theme'}
body={
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
<Text>Are you sure you want to delete this theme?</Text>
<Text
style={{
fontWeight: 'bold',
fontSize: '1.25rem',
padding: '.5rem',
margin: '.5rem',
borderRadius: '10px',
backgroundColor: 'var(--mantine-color-accent-0)',
color: 'var(--mantine-color-background-0)',
}}
>{`"${cardTheme.name}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
</Stack>
}
/>
<Text
style={{
position: 'absolute',
top: -10,
left: 6,
paddingRight: '.25rem',
paddingLeft: '.25rem',
backgroundColor: cardTheme.backgroundColor,
borderRadius: '4px',
color:
cardTheme.borderColor === cardTheme.primaryColor
? cardTheme.accentColor
: cardTheme.primaryColor,
zIndex: 3,
boxShadow: `2px -2px ${cardTheme.borderColor}`,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
maxWidth: width,
}}
>
{cardTheme.name}
</Text>
<Group
style={{
width: '100%',
height: '100%',
overflow: 'hidden',
transition: 'all .2s ease',
position: 'absolute',
}}
>
<ThemeManagementPill
allThemes={allThemes}
cardTheme={cardTheme}
handleOpen={handleOpen}
themePlaylists={themePlaylists}
openModal={openModal}
hovered={hovered}
setHoverPreview={setHoverPreview}
/>
</Group>
<Button
ref={ref}
w={'275px'}
h={'115px'}
disabled={currentTheme === 'random'}
style={{
backgroundImage:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../api/backgroundImages/${cardTheme?.backgroundImage})`
: undefined,
backgroundSize:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? 'cover'
: undefined,
backgroundColor:
cardTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? cardTheme?.backgroundColor
: undefined,
backgroundPosition: 'center',
border: `solid ${cardTheme.borderColor}`,
filter:
cardTheme.randomEnabled === 0 && !hoverPreview ? 'saturate(0)' : undefined,
transition: 'all .2s ease',
}}
onClick={() => {
setCurrentTheme(cardTheme.name);
setCurrentThemePlaylist('All');
}}
>
{hovered && currentTheme !== 'random' && <Overlay opacity={0.5} zIndex={1} />}
<Group w={'100%'} gap={0} display={'flex'} align='center' justify='center'>
<Stack align='center'>
<Box
style={{
height: '10px',
width: '100px',
backgroundColor: cardTheme.searchColor,
borderRadius: '4px',
borderBottom: `solid 1px ${cardTheme.borderColor}`,
}}
/>
<Group justify='space-between'>
<Box
style={{
height: '.5rem',
width: '100px',
backgroundColor: cardTheme.accentColor,
borderRadius: '4px',
}}
/>
<Box
style={{
height: '.5rem',
width: '50px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'}>
<Box
style={{
height: '.25rem',
width: '40px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'}>
<Box
className={boxClasses.appItemExampleText}
style={{
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
<Box
className={boxClasses.appItemExampleText}
style={{
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
<Box
className={boxClasses.appItemExampleText}
style={{
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
</Group>
</Stack>
</Group>
</Button>
</Group>
);
}

View File

@@ -0,0 +1,55 @@
import { Box, Stack, Text } from '@mantine/core';
import { useState } from 'react';
import { FiCheck } from 'react-icons/fi';
import { boxClasses, textClasses } from 'styles';
import type { Theme } from 'types';
export function ThemeExampleApp({ selectedTheme }: { selectedTheme: Theme }) {
const [textDecoration, setTextDecoration] = useState<string>('none');
return (
<Box
component='a'
onMouseOver={() => setTextDecoration('underline')}
onMouseOut={() => setTextDecoration('none')}
className={boxClasses.appItemBox}
h={'64px'}
>
<Stack
style={{
marginRight: '1rem',
marginLeft: '1rem',
}}
justify='center'
>
<FiCheck style={{ color: selectedTheme?.primaryColor }} />
</Stack>
<Stack
style={{
justifyContent: 'center',
overflow: 'hidden',
textOverflow: 'ellipsis',
gap: 0,
}}
>
<Text
c={selectedTheme?.primaryColor}
className={textClasses.appName}
classNames={textClasses}
>
{'Example App'}
</Text>
<Text
c={selectedTheme?.accentColor}
className={textClasses.appUrl}
classNames={textClasses}
style={{
textDecoration: textDecoration,
}}
>
{'https://www.example.com'}
</Text>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,227 @@
import { ActionIcon, Group, isLightColor, Stack, Text } from '@mantine/core';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useState } from 'react';
import { FaTrashAlt } from 'react-icons/fa';
import { FaPencil } from 'react-icons/fa6';
import { FiEdit, FiPower, FiShuffle } from 'react-icons/fi';
import { actionIconClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
export function ThemeManagementPill({
allThemes,
cardTheme,
handleOpen,
themePlaylists,
openModal,
hovered,
setHoverPreview,
}: {
allThemes: Theme[];
cardTheme: Theme;
handleOpen: (cardTheme: Theme) => void;
themePlaylists: ThemePlaylist[];
openModal: () => void;
hovered: boolean;
setHoverPreview: (hoverPreview: boolean) => void;
}) {
const { currentTheme, currentThemePlaylist, handleThemeChange, theme } =
useCommonHooks();
const [shiftEdit, setShiftEdit] = useState<number>(0);
const [shiftShuffle, setShiftShuffle] = useState<number>(0);
const { mutateAsync } = useMutationAPI();
return (
<>
{currentTheme === 'random' && cardTheme.name === theme.name ? (
<Group>
<Group
onMouseEnter={() => {
setShiftEdit(2);
}}
onMouseLeave={() => {
setShiftEdit(0);
}}
onClick={() => {
handleOpen(cardTheme);
}}
gap={0}
style={{
zIndex: 300,
position: 'absolute',
left: 0,
top: 27 - shiftEdit,
backgroundColor:
shiftEdit === 0 ? 'white' : 'var(--mantine-color-background-0)',
borderRadius: '50%',
padding: '.5rem',
margin: '.25rem',
border: 'solid 2px',
borderColor: shiftEdit === 0 ? 'black' : 'var(--mantine-color-primary-0)',
transition: 'all 0.2s',
}}
>
<FiEdit
style={{
fontSize: '2rem',
fontWeight: 'bold',
color: shiftEdit === 0 ? 'black' : 'var(--mantine-color-primary-0)',
cursor: 'pointer',
}}
/>
</Group>
<Group
onMouseEnter={() => {
setShiftShuffle(2);
}}
onMouseLeave={() => {
setShiftShuffle(0);
}}
onClick={() => {
if (currentThemePlaylist === 'All') {
handleThemeChange(allThemes, false);
} else if (currentThemePlaylist === 'Backgrounds') {
handleThemeChange(
allThemes.filter(
(a: any) => !a.backgroundImage.toLowerCase().includes('none'),
),
false,
);
} else if (currentThemePlaylist === 'No Backgrounds') {
handleThemeChange(
allThemes.filter((a: any) =>
a.backgroundImage.toLowerCase().includes('none'),
),
false,
);
} else {
const themeList = JSON.parse(
themePlaylists?.find(
(t: ThemePlaylist) => t?.name === currentThemePlaylist,
)?.themeList ?? '[]',
);
const filteredThemes = allThemes.filter((t: Theme) =>
themeList.includes(t.id),
);
handleThemeChange(filteredThemes, false);
}
}}
gap={0}
style={{
zIndex: 300,
position: 'absolute',
right: 0,
top: 27 - shiftShuffle,
backgroundColor:
shiftShuffle === 0 ? 'white' : 'var(--mantine-color-background-0)',
borderRadius: '50%',
padding: '.5rem',
margin: '.25rem',
border: 'solid 2px',
borderColor:
shiftShuffle === 0 ? 'black' : 'var(--mantine-color-primary-0)',
transition: 'all 0.2s',
}}
>
<FiShuffle
style={{
fontSize: '2rem',
fontWeight: 'bold',
color: shiftShuffle === 0 ? 'black' : 'var(--mantine-color-primary-0)',
cursor: 'pointer',
}}
/>
</Group>
</Group>
) : (
<>
<Stack
gap={0}
style={{
zIndex: 2,
paddingRight: 0,
position: 'absolute',
right: 3,
top: 3,
marginRight: !hovered ? 0 : '-2.5rem',
transition: 'all 0.2s',
}}
align='center'
justify='center'
h={'100%'}
>
{!cardTheme.randomEnabled && (
<Text
style={{
writingMode: 'sideways-lr',
color: isLightColor(cardTheme.backgroundColor) ? 'black' : 'white',
opacity: 0.5,
}}
>
DISABLED
</Text>
)}
</Stack>
<Stack
gap={0}
style={{
zIndex: 2,
padding: '.25rem',
paddingRight: 0,
position: 'absolute',
right: 3,
backgroundColor: '#ffffff33',
borderTopLeftRadius: '10px',
borderBottomLeftRadius: '10px',
marginRight: hovered ? 0 : '-2.5rem',
transition: 'all 0.2s',
backdropFilter: 'blur(10px)',
}}
align='center'
justify='center'
>
<ActionIcon
className={actionIconClasses.themeEditButton}
classNames={actionIconClasses}
onMouseEnter={() => setHoverPreview(true)}
onMouseLeave={() => setHoverPreview(false)}
onClick={() => {
mutateAsync({
method: 'PATCH',
endpoint: 'api/themes',
body: { ...cardTheme, randomEnabled: !cardTheme.randomEnabled },
});
}}
mb={'.125rem'}
>
<FiPower
color={cardTheme.randomEnabled ? 'DarkOliveGreen' : '#33333388'}
style={{ strokeWidth: 4, filter: 'brightness(1.5)' }}
/>
</ActionIcon>
<ActionIcon
className={actionIconClasses.themeEditButton}
classNames={actionIconClasses}
onClick={() => handleOpen(cardTheme)}
mb={'.125rem'}
>
<FaPencil
style={{ padding: '.25rem', filter: 'brightness(1.5)' }}
color='DarkOliveGreen'
/>
</ActionIcon>
<ActionIcon
className={actionIconClasses.themeEditButton}
classNames={actionIconClasses}
onClick={openModal}
disabled={cardTheme.name === theme.name}
>
<FaTrashAlt
style={{ padding: '.25rem', filter: 'brightness(1.25)' }}
color='Brown'
/>
</ActionIcon>
</Stack>
</>
)}
</>
);
}

View File

@@ -0,0 +1,43 @@
import { Group, Text } from '@mantine/core';
import type { Theme } from 'types';
export function ThemeMiniCard({ miniTheme }: { miniTheme: Theme | undefined }) {
return (
<Group
gap={0}
align='center'
justify='center'
display={'grid'}
pos={'relative'}
w={'175px'}
h={'60px'}
style={{
backgroundImage:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${miniTheme?.backgroundColor}33, ${miniTheme?.backgroundColor}33), url(../../api/backgroundImages/${miniTheme?.backgroundImage})`
: undefined,
backgroundSize:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined,
backgroundColor:
miniTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? miniTheme?.backgroundColor
: undefined,
backgroundPosition: 'center',
border: `solid ${miniTheme?.borderColor}`,
borderRadius: '8px',
}}
>
<Text
bg={miniTheme?.backgroundColor}
c={miniTheme?.primaryColor}
size='md'
style={{
padding: '.25rem',
borderRadius: '8px',
}}
>
{miniTheme?.name}
</Text>
</Group>
);
}

View File

@@ -0,0 +1,549 @@
import {
ActionIcon,
Button,
Checkbox,
ColorPicker,
ColorSwatch,
Group,
Modal,
ScrollArea,
Stack,
Text,
TextInput,
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react';
import { FiImage, FiSave, FiX } from 'react-icons/fi';
import { modalClasses, scrollAreaClasses, textClasses, textInputClasses } from 'styles';
import type { Theme } from 'types';
import { ThemeModalContent } from './ThemeModalContent';
export function ThemeModal({
opened,
handleClose,
selectedTheme,
setSelectedTheme,
openPicker,
backgroundList,
pickerOpen,
closePicker,
handleSaveTheme,
}: {
opened: boolean;
handleClose: () => void;
selectedTheme: Theme;
setSelectedTheme: (selectedTheme: Theme) => void;
openPicker: () => void;
backgroundList: string[];
pickerOpen: boolean;
closePicker: () => void;
handleSaveTheme: () => void;
}) {
const { isMobile, theme } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000');
const [defaultTheme, setDefaultTheme] = useState<Theme>({ ...selectedTheme });
const [designerTheme, setDesignerTheme] = useState<Theme>({
...selectedTheme,
});
const filtered = backgroundList.filter((item: string) =>
item.toLowerCase().includes(query.toLowerCase()),
);
const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] =
useDisclosure(false);
const re = /(?:\.([^.]+))?$/;
const items = filtered.map((item: string, index: number) => {
return (
<UnstyledButton
data-list-item
key={item}
style={{
width: '100%',
height: '75px',
background:
item !== 'none' && item !== 'None'
? index === hovered
? `linear-gradient(${selectedTheme.backgroundColor}88, ${selectedTheme.backgroundColor}88), no-repeat center url(../../api/backgroundImages/${item})`
: `no-repeat center url(../../api/backgroundImages/${item})`
: 'repeating-linear-gradient(45deg, #33333366, #33333366 10px, #88888866 10px, #88888866 20px)',
backgroundSize: item !== 'none' ? 'cover' : undefined,
backgroundColor: item === 'none' ? '#00000088' : undefined,
paddingLeft: '.5rem',
alignItems: 'center',
}}
onClick={() =>
setDesignerTheme({
...designerTheme,
backgroundImage: item,
})
}
>
<Group h={'100%'} w={'100%'} justify='space-between'>
<Text
style={{
width: 'min-content',
backdropFilter: 'blur(5px) brightness(.25)',
padding: '.5rem',
borderBottomRightRadius: 4,
marginTop: '-2.15rem',
marginLeft: '-.5rem',
color: 'white',
}}
>
{item.replace(/\.[^/.]+$/, '')}
</Text>
{item !== 'None' && (
<Text
style={{
width: 'min-content',
backdropFilter: 'blur(5px) brightness(.25)',
padding: '.5rem',
marginTop: '2rem',
marginBottom: -1,
borderTopLeftRadius: 4,
color: 'white',
}}
>
{re.exec(item.toUpperCase())?.[1]}
</Text>
)}
</Group>
</UnstyledButton>
);
});
const handleSwatchClick = () => {
closeBackgroundPicker();
openPicker();
setDesignerTheme(selectedTheme);
};
useEffect(() => {
setDesignerTheme(selectedTheme);
if (defaultTheme.name !== selectedTheme.name) {
setDefaultTheme(selectedTheme);
}
}, [selectedTheme]);
return (
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={handleClose}
fullScreen={isMobile}
centered
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text c={'var(--mantine-color-background-0)'}>Name:</Text>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
value={selectedTheme.name}
onChange={(e) =>
setSelectedTheme({ ...selectedTheme, name: e.target.value })
}
styles={{
label: {
color: 'var(--mantine-color-primary-0)',
},
input: {
color: selectedTheme.primaryColor,
backgroundColor: selectedTheme.backgroundColor,
border: 'none',
borderBottom: '1px solid var(--mantine-color-primary-0)',
},
}}
/>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<Group p={'2rem'} w={'100%'} align='flex-start' justify='space-between'>
<Stack display={'flex'} pos={'relative'} w={isMobile ? '100%' : '70%'}>
<ThemeModalContent selectedTheme={designerTheme} />
<Tooltip
label={'Change Primary Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('primaryColor');
setColorValue(selectedTheme.primaryColor);
handleSwatchClick();
}}
color={selectedTheme.primaryColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'6.25rem'}
left={'1.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Accent Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('accentColor');
setColorValue(selectedTheme.accentColor);
handleSwatchClick();
}}
color={selectedTheme.accentColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
bottom={'6rem'}
left={'11rem'}
/>
</Tooltip>
<Tooltip
label={'Change Background Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('backgroundColor');
setColorValue(selectedTheme.backgroundColor);
handleSwatchClick();
}}
color={selectedTheme.backgroundColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
bottom={'10rem'}
right={'3.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Border Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('borderColor');
setColorValue(selectedTheme.borderColor);
handleSwatchClick();
}}
color={selectedTheme.borderColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'4.5rem'}
right={'15%'}
/>
</Tooltip>
<Tooltip
label={'Change Search Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('searchColor');
setColorValue(selectedTheme.searchColor);
handleSwatchClick();
}}
color={selectedTheme.searchColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'3.25rem'}
left={'6.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Background'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ActionIcon
onClick={() => {
closePicker();
openBackgroundPicker();
setDesignerTheme(selectedTheme);
}}
pos={'absolute'}
right={0}
>
<FiImage size={20} />
</ActionIcon>
</Tooltip>
</Stack>
<Stack
w={isMobile ? '100%' : '28%'}
h={'436px'}
p={'1rem'}
style={{
borderRadius: '1rem',
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0) 20%, transparent)',
}}
>
{pickerOpen && (
<Stack h={'100%'} align='center' justify='space-between'>
<Text
className={textClasses.sectionTitle}
classNames={textClasses}
style={{
textShadow:
theme.textShadowsEnabled &&
'1px 1px 1px color-mix(in srgb, var(--mantine-color-accent-0), transparent 50%)',
}}
>
Change {colorId}
</Text>
{colorId === 'backgroundColor' && (
<ColorPicker
value={colorValue}
onChange={(e) => {
setColorValue(e);
setDesignerTheme({
...designerTheme,
backgroundColor: e,
});
}}
format={'hex'}
w={'90%'}
mb={'1.5rem'}
/>
)}
{colorId !== 'backgroundColor' && (
<ColorPicker
value={colorValue}
onChange={(e) => {
setColorValue(e);
setDesignerTheme({
...designerTheme,
[colorId]: e,
});
}}
format={'hexa'}
w={'90%'}
/>
)}
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
w={'90%'}
value={colorValue}
onChange={(e) => setColorValue(e.target.value)}
/>
<Group mt={'auto'} justify='center'>
<Button
color={'green'}
onClick={() => {
switch (colorId) {
case 'primaryColor':
setSelectedTheme({
...selectedTheme,
primaryColor: colorValue,
});
break;
case 'accentColor':
setSelectedTheme({
...selectedTheme,
accentColor: colorValue,
});
break;
case 'backgroundColor':
setSelectedTheme({
...selectedTheme,
backgroundColor: colorValue,
});
break;
case 'borderColor':
setSelectedTheme({
...selectedTheme,
borderColor: colorValue,
});
break;
case 'searchColor':
setSelectedTheme({
...selectedTheme,
searchColor: colorValue,
});
break;
}
closePicker();
}}
>
<FiSave />
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closePicker();
setDesignerTheme({
...designerTheme,
[colorId]: selectedTheme[colorId as keyof Theme],
});
}}
>
<FiX />
</Button>
</Group>
</Stack>
)}
{backgroundPicker && (
<Stack h={'100%'} align='center' justify='center'>
<Group gap={0}>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
placeholder='Search'
value={query.replace(/\.[^/.]+$/, '')}
rightSection={
<FiX
style={{
cursor: 'pointer',
margin: '.25rem',
}}
onClick={() => {
setQuery('');
}}
/>
}
onChange={(event) => {
setQuery(event.currentTarget.value);
setHovered(-1);
}}
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault();
setHovered((current) => {
const nextIndex =
current + 1 >= filtered.length ? current : current + 1;
viewportRef.current
?.querySelectorAll('[data-list-item]')
?.[nextIndex]?.scrollIntoView({ block: 'nearest' });
return nextIndex;
});
}
if (event.key === 'ArrowUp') {
event.preventDefault();
setHovered((current) => {
const nextIndex = current - 1 < 0 ? current : current - 1;
viewportRef.current
?.querySelectorAll('[data-list-item]')
?.[nextIndex]?.scrollIntoView({ block: 'nearest' });
return nextIndex;
});
}
}}
/>
</Group>
<ScrollArea.Autosize
viewportRef={viewportRef}
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
style={{
height: 325,
width: '90%',
borderRadius: 8,
}}
>
<Stack gap={0} align='center' display={'block'}>
{items}
</Stack>
</ScrollArea.Autosize>
<Group justify='center'>
<Button
color={'green'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
backgroundImage: designerTheme.backgroundImage,
});
closeBackgroundPicker();
}}
>
<FiSave />
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeBackgroundPicker();
setDesignerTheme({
...designerTheme,
backgroundImage: selectedTheme.backgroundImage,
});
}}
>
<FiX />
</Button>
</Group>
</Stack>
)}
{!pickerOpen && !backgroundPicker && (
<Stack h={'100%'} align='center' justify='center' gap={4}>
<Checkbox
label='Text Shadows'
checked={selectedTheme.textShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
/>
<Button
onClick={() => {
setSelectedTheme({ ...defaultTheme });
}}
>
Reset Changes
</Button>
</Stack>
)}
</Stack>
</Group>
<Group>
<Button
disabled={JSON.stringify(defaultTheme) === JSON.stringify(selectedTheme)}
color={'green'}
onClick={handleSaveTheme}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeBackgroundPicker();
handleClose();
}}
>
Cancel
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}

View File

@@ -0,0 +1,95 @@
import { ColorSwatch, Group, Stack, Text } from '@mantine/core';
import { colorSwatchClasses, textClasses } from 'styles';
import type { Theme } from 'types';
export function ThemeModalColorSwatches({
selectedTheme,
openPicker,
setColorId,
setColorValue,
}: {
selectedTheme: Theme;
openPicker: () => void;
setColorId: (colorId: string) => void;
setColorValue: (colorValue: string) => void;
}) {
return (
<Stack justify='center'>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Primary Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('primaryColor');
openPicker();
setColorValue(selectedTheme.primaryColor);
}}
color={selectedTheme.primaryColor}
className={colorSwatchClasses.default}
classNames={colorSwatchClasses}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Accent Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('accentColor');
openPicker();
setColorValue(selectedTheme.accentColor);
}}
color={selectedTheme.accentColor}
className={colorSwatchClasses.default}
classNames={colorSwatchClasses}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Background Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('background');
openPicker();
setColorValue(selectedTheme.backgroundColor);
}}
color={selectedTheme.backgroundColor}
className={colorSwatchClasses.default}
classNames={colorSwatchClasses}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Border Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('borderColor');
openPicker();
setColorValue(selectedTheme.borderColor);
}}
color={selectedTheme.borderColor}
className={colorSwatchClasses.default}
classNames={colorSwatchClasses}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Search Background Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('searchBackground');
openPicker();
setColorValue(selectedTheme.searchColor);
}}
color={selectedTheme.searchColor}
className={colorSwatchClasses.default}
classNames={colorSwatchClasses}
/>
</Group>
</Stack>
);
}

View File

@@ -0,0 +1,227 @@
import { Select, Stack, Text } from '@mantine/core';
import SearchSelectItem from 'pages/homepage/components/Search/SearchSelectItem';
import { selectClasses } from 'styles';
import type { Theme } from 'types';
import { ThemeExampleApp } from './ThemeExampleApp';
export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
return (
<Stack
style={{
backgroundImage:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${selectedTheme?.backgroundColor}33, ${selectedTheme?.backgroundColor}33), url(../../api/backgroundImages/${selectedTheme?.backgroundImage})`
: undefined,
backgroundSize:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? 'cover'
: undefined,
backgroundColor:
selectedTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? selectedTheme?.backgroundColor
: undefined,
backgroundPosition: 'center',
borderRadius: '10px',
border: '2px solid var(--mantine-color-primary-0)',
padding: '1rem',
}}
align='center'
>
<Select
disabled
w={'80%'}
classNames={selectClasses}
className={selectClasses.searchExample}
styles={{
input: {
color: selectedTheme?.primaryColor,
borderBottom: `thin solid ${selectedTheme?.borderColor}`,
backgroundColor: `color-mix(in srgb, ${selectedTheme?.searchColor}, transparent 50%)`,
},
}}
data={[
{
label: 'Google Search',
value: 'google',
},
]}
searchValue={'example search'}
data-active={true}
dropdownOpened={true}
autoFocus={false}
leftSection={'Google Search'}
rightSection={<div />}
renderOption={(item: any) => {
return (
<SearchSelectItem
item={item}
handleSearchHistoryItem={undefined}
searchEndpoint={undefined}
/>
);
}}
/>
<Stack gap={0} w={'100%'}>
<Text
pl={'1.5rem'}
style={{
textDecoration: 'underline',
fontSize: '1.25rem',
fontWeight: 200,
fontStyle: 'italic',
height: '10px',
textTransform: 'uppercase',
marginTop: '1.25rem',
marginBottom: '1.25rem',
color: selectedTheme?.primaryColor,
}}
>
Applications
</Text>
<Stack
style={{
gap: '.25rem',
backgroundSize: 'cover',
borderBottom: '0px solid',
display: 'grid',
gridColumnGap: '5px',
gridRowGap: '5px',
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: '64px',
padding: '1rem',
paddingTop: 0,
justifyContent: 'center',
alignContent: 'center',
}}
>
<ThemeExampleApp selectedTheme={selectedTheme} />
<ThemeExampleApp selectedTheme={selectedTheme} />
<ThemeExampleApp selectedTheme={selectedTheme} />
</Stack>
</Stack>
<Stack gap={0} w={'100%'}>
<Text
pl={'1.5rem'}
style={{
textDecoration: 'underline',
fontSize: '1.25rem',
fontWeight: 200,
fontStyle: 'italic',
height: '10px',
textTransform: 'uppercase',
marginTop: '1.25rem',
marginBottom: '1.25rem',
color: selectedTheme?.primaryColor,
}}
>
Bookmarks
</Text>
<Stack
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
gridColumnGap: '20px',
gridRowGap: '0px',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
gridTemplateRows: 'auto',
}}
>
<Stack
style={{
width: '100%',
marginBottom: 0,
display: 'block',
lineHeight: '1.5rem',
padding: '1rem',
paddingTop: 0,
}}
>
<Text
style={{
fontSize: '.875rem',
height: '10px',
width: 'auto',
color: selectedTheme?.accentColor,
textTransform: 'uppercase',
marginTop: '1.25rem',
marginBottom: '1.25rem',
}}
>
Example Category
</Text>
<Text
style={{
display: 'block',
fontSize: '.875rem',
lineHeight: 2,
color: selectedTheme?.primaryColor,
}}
component='a'
>
{'Bookmark 1'}
</Text>
<Text
style={{
display: 'block',
fontSize: '.875rem',
lineHeight: 2,
color: selectedTheme?.primaryColor,
}}
component='a'
>
{'Bookmark 2'}
</Text>
</Stack>
<Stack
style={{
width: '100%',
marginBottom: 0,
display: 'block',
lineHeight: '1.5rem',
padding: '1rem',
paddingTop: 0,
}}
>
<Text
style={{
fontSize: '.875rem',
height: '10px',
width: 'auto',
color: selectedTheme?.accentColor,
textTransform: 'uppercase',
marginTop: '1.25rem',
marginBottom: '1.25rem',
}}
>
Example Category
</Text>
<Text
style={{
display: 'block',
fontSize: '.875rem',
lineHeight: 2,
color: selectedTheme?.primaryColor,
}}
component='a'
>
{'Bookmark 1'}
</Text>
<Text
style={{
display: 'block',
fontSize: '.875rem',
lineHeight: 2,
color: selectedTheme?.primaryColor,
}}
component='a'
>
{'Bookmark 2'}
</Text>
</Stack>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,81 @@
import { Group, Overlay, Popover } from '@mantine/core';
import { useLongPress } from '@mantine/hooks';
import { useState } from 'react';
import { ThemeMiniCard } from './ThemeMiniCard';
import { ThemeModalContent } from './ThemeModalContent';
export function ThemePreviewMiddleware({
newThemeList,
miniTheme,
themeList,
selectedPlaylist,
setSelectedPlaylist,
}: {
newThemeList: number[];
miniTheme: any;
themeList: number[];
selectedPlaylist: any;
setSelectedPlaylist: (selectedPlaylist: any) => void;
}) {
const [pressed, setPressed] = useState<any>(false);
const handlers = useLongPress(() => {
setPressed(true);
});
return (
<Popover
key={miniTheme.id}
withArrow
opened={pressed}
{...handlers}
arrowSize={16}
transitionProps={{ duration: 200, timingFunction: 'ease' }}
>
<Popover.Target>
<Group
style={{
cursor: 'pointer',
filter:
newThemeList && !newThemeList?.includes(miniTheme.id)
? 'saturate(0)'
: undefined,
}}
onClick={() => {
if (!pressed) {
const newSelectedPlaylist = { ...selectedPlaylist };
if (themeList?.includes(miniTheme.id)) {
newThemeList.splice(themeList.indexOf(miniTheme.id), 1);
} else {
newThemeList.push(miniTheme.id);
}
newSelectedPlaylist.themeList = JSON.stringify(newThemeList);
setSelectedPlaylist(newSelectedPlaylist);
} else {
setPressed(false);
}
}}
>
<ThemeMiniCard miniTheme={miniTheme} />
</Group>
</Popover.Target>
<Popover.Dropdown
bg={'var(--mantine-color-primary-0)'}
p={0}
style={{ border: 'none' }}
>
<Overlay
w={'calc(300%)'}
h={'calc(300%)'}
pos={'absolute'}
left={'-100%'}
top={'-100%'}
zIndex={-1}
opacity={0.8}
bg={
'radial-gradient(circle, var(--mantine-color-primary-0) 0%, transparent 60%)'
}
/>
<ThemeModalContent selectedTheme={miniTheme} />
</Popover.Dropdown>
</Popover>
);
}

View File

@@ -0,0 +1,126 @@
import {
type ComboboxItem,
type ComboboxLikeRenderOptionInput,
Group,
Select,
Text,
} from '@mantine/core';
import type { Theme, ThemePlaylist } from 'types';
export function ThemeSelect({
allThemes,
createPlaylist,
themeList,
selectedPlaylist,
index,
timeIndex,
}: {
allThemes: Theme[];
createPlaylist: (arg0: object) => void;
themeList: Array<string>;
selectedPlaylist: ThemePlaylist;
index: number;
timeIndex: string;
}) {
const theme: Theme | undefined = allThemes.find(
(theme: Theme) => `${theme.id}` === themeList[index],
);
return (
<Group>
<Text w={'100px'}>{timeIndex}</Text>
<Select
labelProps={{ display: 'none' }}
placeholder='Select Theme'
defaultValue={themeList[index]}
data={allThemes?.map((theme: Theme) => {
return {
value: `${theme.id}`,
label: theme.name,
};
})}
onChange={(data) => {
const newThemeList = themeList;
newThemeList[index] = data as string;
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: selectedPlaylist?.id,
name: selectedPlaylist?.name,
themeList: JSON.stringify(newThemeList),
},
});
}}
styles={{
option: {
padding: '.25rem',
},
input: {
textAlign: 'center',
height: '60px',
color: theme?.primaryColor,
backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/${theme?.backgroundImage})`
: undefined,
backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined,
backgroundColor:
theme?.backgroundImage.toLocaleLowerCase() === 'none'
? theme?.backgroundColor
: undefined,
backgroundPosition: 'center',
border: `solid ${theme?.borderColor}`,
borderRadius: '8px',
},
}}
renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const theme = allThemes?.find(
(theme: Theme) => `${theme.id}` === item.option.value,
);
return (
<Group
gap={0}
align='center'
justify='center'
w={'100%'}
h={'60px'}
style={{
backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/${theme?.backgroundImage})`
: undefined,
backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'
? 'cover'
: undefined,
backgroundColor:
theme?.backgroundImage.toLocaleLowerCase() === 'none'
? theme?.backgroundColor
: undefined,
backgroundPosition: 'center',
border: `solid ${theme?.borderColor}`,
borderRadius: '8px',
}}
>
<Text
bg={theme?.backgroundColor}
c={theme?.primaryColor}
size='md'
style={{
padding: '.25rem',
borderRadius: '8px',
}}
>
{
allThemes.find((theme: Theme) => `${theme.id}` === item.option.value)
?.name
}
</Text>
</Group>
);
}}
/>
</Group>
);
}

View File

@@ -0,0 +1,381 @@
import { Box, Button, Group, Overlay, Stack, Switch, Text, Tooltip } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, 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 themesEndpoint = 'api/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 { mutateAsync } = useMutationAPI();
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: 0,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
const [pickerOpen, { open: openPicker, close: closePicker }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const {
currentTheme,
setCurrentTheme,
theme,
isMobile,
currentThemePlaylist,
setCurrentThemePlaylist,
} = useCommonHooks();
const handleOpen = (theme: Theme) => {
setSelectedTheme(theme);
openThemeModal();
};
const handleClose = () => {
closeThemeModal();
closePicker();
setSelectedTheme({
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 0,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
};
const handleBackgroundModalClose = () => {
closeBackgroundModal();
};
const handleSaveTheme = () => {
mutateAsync({
method: selectedTheme?.id === undefined ? 'POST' : 'PATCH',
endpoint: themesEndpoint,
body: selectedTheme,
});
handleClose();
};
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;
}
})
.map((t) => t.backgroundImage),
),
]);
}
}, [allThemes]);
return (
<Stack>
<ThemeModal
opened={themeModalOpened}
handleClose={handleClose}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
openPicker={openPicker}
backgroundList={backgroundList}
pickerOpen={pickerOpen}
closePicker={closePicker}
handleSaveTheme={handleSaveTheme}
/>
<BackgroundModal
backgroundModalOpened={backgroundModalOpened}
handleBackgroundModalClose={handleBackgroundModalClose}
backgroundList={backgroundList}
inUseBackgrounds={inUseBackgrounds}
/>
<SettingsHeader
setDragging={undefined}
openModal={openThemeModal}
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}
handleOpen={handleOpen}
themePlaylists={themePlaylists}
/>
</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: isMobile ? '.5rem' : '1.5rem',
paddingBottom: '1.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>
<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>
<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>
);
}

View File

@@ -0,0 +1,52 @@
import { Stack } from '@mantine/core';
import type { Theme, ThemePlaylist } from 'types';
import { ThemeSelect } from './ThemeSelect';
export function TimeOfDay({
createPlaylist,
themeList,
allThemes,
selectedPlaylist,
}: {
createPlaylist: () => void;
themeList: Array<string>;
allThemes: Theme[];
selectedPlaylist: ThemePlaylist;
}) {
return (
<Stack>
<ThemeSelect
allThemes={allThemes}
createPlaylist={createPlaylist}
themeList={themeList}
selectedPlaylist={selectedPlaylist}
index={0}
timeIndex='00:00 - 05:59'
/>
<ThemeSelect
allThemes={allThemes}
createPlaylist={createPlaylist}
themeList={themeList}
selectedPlaylist={selectedPlaylist}
index={1}
timeIndex='06:00 - 11:59'
/>
<ThemeSelect
allThemes={allThemes}
createPlaylist={createPlaylist}
themeList={themeList}
selectedPlaylist={selectedPlaylist}
index={2}
timeIndex='12:00 - 17:59'
/>
<ThemeSelect
allThemes={allThemes}
createPlaylist={createPlaylist}
themeList={themeList}
selectedPlaylist={selectedPlaylist}
index={3}
timeIndex='18:00 - 23:59'
/>
</Stack>
);
}

View File

@@ -0,0 +1,11 @@
export { ThemeCard } from './ThemeCard';
export { ThemeExampleApp } from './ThemeExampleApp';
export { ThemeManagementPill } from './ThemeManagementPill';
export { ThemeMiniCard } from './ThemeMiniCard';
export { ThemeModal } from './ThemeModal';
export { ThemeModalColorSwatches } from './ThemeModalColorSwatches';
export { ThemeModalContent } from './ThemeModalContent';
export { ThemePreviewMiddleware } from './ThemePreviewMiddleware';
export { ThemeSelect } from './ThemeSelect';
export { Themes } from './Themes';
export { TimeOfDay } from './TimeOfDay';