Theme modal updates

This commit is contained in:
Sean Edmunds
2025-07-25 14:41:29 -05:00
parent 677c1877a8
commit ed64cf1aac
9 changed files with 1098 additions and 988 deletions

16
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0",
"react-router-dom": "^7.7.1",
"recharts": "^3.1.0"
},
"devDependencies": {
@@ -3683,9 +3683,9 @@
}
},
"node_modules/react-router": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz",
"integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
"integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -3705,12 +3705,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.0.tgz",
"integrity": "sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz",
"integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.7.0"
"react-router": "7.7.1"
},
"engines": {
"node": ">=20.0.0"

View File

@@ -23,7 +23,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.0",
"react-router-dom": "^7.7.1",
"recharts": "^3.1.0"
},
"devDependencies": {

View File

@@ -12,14 +12,16 @@ import { MiniAppItem, MiniGroupItem, ThemeManagementPill } from '.';
export function ThemeCard({
cardTheme,
handleOpen,
allThemes,
themePlaylists,
setSelectedTheme,
openThemeModal,
}: {
cardTheme: Theme;
handleOpen: (cardTheme: Theme) => void;
allThemes: Theme[];
themePlaylists: ThemePlaylist[];
setSelectedTheme: (theme: Theme) => void;
openThemeModal: () => void;
}) {
const { hovered, ref: hoverRef } = useHover();
const { ref, width } = useElementSize();
@@ -29,6 +31,10 @@ export function ThemeCard({
const [hoverPreview, setHoverPreview] = useDebouncedState(false, 1000);
const [transition, setTransition] = useDebouncedState(false, 200);
const handleOpen = (theme: Theme) => {
setSelectedTheme(theme);
openThemeModal();
};
return (
<Group
gap={0}

View File

@@ -1,928 +0,0 @@
import {
ActionIcon,
Box,
Button,
Checkbox,
ColorPicker,
ColorSwatch,
Divider,
Group,
Image,
Modal,
Overlay,
ScrollArea,
Stack,
Text,
TextInput,
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useDisclosure, useLongPress } from '@mantine/hooks';
import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import { FiImage, FiSave, FiX } from 'react-icons/fi';
import { modalClasses, scrollAreaClasses, textClasses, textInputClasses } from 'styles';
import { BackgroundProperties, type Theme } from 'types';
import { ThemeCard } from './ThemeCard';
import { ThemeMiniCard } from './ThemeMiniCard';
import { ThemeModalContent } from './ThemeModalContent';
import { ThemePreviewMiddleware } from './ThemePreviewMiddleware';
export function ThemeModal({
opened,
handleClose,
selectedTheme,
setSelectedTheme,
openPicker,
backgroundList,
pickerOpen,
closePicker,
handleSaveTheme,
isNew,
setIsNew,
allThemes,
}: {
opened: boolean;
handleClose: () => void;
selectedTheme: Theme;
setSelectedTheme: (selectedTheme: Theme) => void;
openPicker: () => void;
backgroundList: string[];
pickerOpen: boolean;
closePicker: () => void;
handleSaveTheme: () => void;
isNew: boolean;
setIsNew: (isNew: boolean) => void;
allThemes: Theme[];
}) {
const [pressed, setPressed] = useState<any>(false);
const handlers = useLongPress(() => {
setPressed(true);
});
const { isMobile, theme, getObjectDiff } = 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 [duplicate, setDuplicate] = useState(false);
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/thumbs/${item})`
: `no-repeat center url(../../api/backgroundImages/thumbs/${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.id !== selectedTheme.id) {
setDefaultTheme(selectedTheme);
}
}, [selectedTheme]);
return (
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={() => {
handleClose();
setDuplicate(false);
}}
fullScreen={isMobile}
centered
keepMounted={false}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
{!isNew && (
<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>
{isNew ? (
<Stack align='center' p={'2rem'}>
{!duplicate ? (
<>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Please choose an option:
</Text>
<Group>
<Stack
justify='center'
align='center'
bg={'var(--mantine-color-primary-0)'}
px={'1rem'}
py={'3rem'}
style={{ borderRadius: '8px' }}
>
<ThemeCard
cardTheme={{
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
}}
handleOpen={() => {}}
allThemes={[]}
themePlaylists={[]}
/>
<Button
pos={'absolute'}
w={'275px'}
h={'115px'}
style={{
backgroundColor: 'transparent',
boxShadow: '3px 3px var(--mantine-color-border-0)',
overflow: 'visible',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-background-0)',
color: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '-16px',
fontSize: '1rem',
},
}}
onClick={() => setIsNew(false)}
>
New Theme from Scratch
</Button>
</Stack>
<Divider
color={'var(--mantine-color-primary-0)'}
my={'sm'}
orientation='vertical'
m={'2rem'}
/>
<Stack
justify='center'
align='center'
pos={'relative'}
bg={'var(--mantine-color-primary-0)'}
px={'1rem'}
py={'3rem'}
style={{ borderRadius: '8px' }}
>
<ThemeCard
cardTheme={{
...allThemes[Math.floor(Math.random() * allThemes.length)],
randomEnabled: 1,
}}
handleOpen={() => {}}
allThemes={[]}
themePlaylists={[]}
/>
<Overlay
bg={'transparent'}
zIndex={0}
pos={'absolute'}
w={'275px'}
h={'115px'}
top={'3rem'}
left={'1rem'}
style={{
backdropFilter: 'grayscale(80%) brightness(80%)',
mask: 'linear-gradient(105deg, rgba(0, 0, 0, 1) 48%, rgba(0, 0, 0, 0) 52%)',
alignContent: 'center',
justifyContent: 'center',
display: 'flex',
}}
/>
<FaArrowRight
size={45}
style={{
color: 'var(--mantine-color-background-0)',
position: 'absolute',
stroke: 'var(--mantine-color-accent-0)',
strokeWidth: 25,
}}
/>
<Button
pos={'absolute'}
w={'275px'}
h={'115px'}
style={{
backgroundColor: 'transparent',
boxShadow: '3px 3px var(--mantine-color-border-0)',
overflow: 'visible',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-background-0)',
color: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '-16px',
fontSize: '1rem',
},
}}
onClick={() => setDuplicate(true)}
>
Duplicate existing Theme
</Button>
</Stack>
</Group>
</>
) : (
<Stack align='center'>
<Group
w={'100%'}
justify='center'
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-accent-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
{allThemes?.map((miniTheme: Theme) => {
return (
<Box
{...handlers}
key={miniTheme.id}
onClick={() => {
!pressed && setSelectedTheme(miniTheme);
setPressed(false);
}}
>
<ThemePreviewMiddleware
newThemeList={[miniTheme.id as number]}
miniTheme={miniTheme}
themeList={[]}
selectedPlaylist={undefined}
setSelectedPlaylist={() => {}}
/>
</Box>
);
})}
</Group>
<Group>
<Group
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
<ThemeMiniCard miniTheme={selectedTheme} />
<Button
disabled={selectedTheme.name === 'default'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
name: `${selectedTheme.name} copy`,
});
setIsNew(false);
}}
>
Confirm
</Button>
</Group>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
setDuplicate(false);
setSelectedTheme({
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
}}
>
Back
</Button>
</Group>
</Stack>
)}
</Stack>
) : (
<>
<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%)'
: undefined,
}}
>
Change{' '}
{
BackgroundProperties[
colorId as keyof typeof BackgroundProperties
]
}
</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%'} justify='space-between' align='center' gap={4}>
<Stack w={'100%'} align='center' gap={4}>
<Checkbox
label='Text Shadows'
checked={selectedTheme.textShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
</Stack>
{JSON.stringify(defaultTheme) !== JSON.stringify(selectedTheme) && (
<>
<Text fz={'1.5rem'}>Changes:</Text>
<ScrollArea.Autosize
w={'100%'}
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
style={{
backgroundColor: 'var(--mantine-color-accent-0)',
borderRadius: 8,
padding: '1rem',
}}
>
<Stack w={'100%'} gap={0}>
{getObjectDiff(defaultTheme, designerTheme) !== null &&
Object.keys(
getObjectDiff(defaultTheme, designerTheme),
).map((key: any) => {
return (
<Stack
w={'100%'}
align='center'
justify='center'
gap={0}
key={key}
>
<Text key={key}>
{
BackgroundProperties[
key as keyof typeof BackgroundProperties
]
}
</Text>
{key === 'name' && (
<Group w={'100%'} justify='center'>
<Text>{defaultTheme[key as keyof Theme]}</Text>
<Text>→</Text>
<Text>{designerTheme[key as keyof Theme]}</Text>
</Group>
)}
{key === 'backgroundImage' && (
<Group w={'100%'} justify='center'>
<Image
w={'40%'}
src={
(
defaultTheme[key as keyof Theme] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${defaultTheme[key as keyof Theme] as string}`
}
/>
<Text>→</Text>
<Image
w={'40%'}
src={
(
designerTheme[
key as keyof Theme
] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}`
}
/>
</Group>
)}
{key.includes('Color') && (
<Group w={'100%'} justify='center'>
<ColorSwatch
color={
defaultTheme[key as keyof Theme] as string
}
/>
<Text>→</Text>
<ColorSwatch
color={
designerTheme[key as keyof Theme] as string
}
/>
</Group>
)}
{key.includes('Shadow') && (
<Group w={'100%'} justify='center'>
{defaultTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
<Text>→</Text>
{designerTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
</Group>
)}
</Stack>
);
})}
</Stack>
</ScrollArea.Autosize>
</>
)}
<Button
onClick={() => {
setSelectedTheme({ ...defaultTheme });
}}
mih={'36px'}
>
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();
setDuplicate(false);
}}
>
Cancel
</Button>
</Group>
</>
)}
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}

View File

@@ -0,0 +1,256 @@
import { Box, Button, Divider, Group, Overlay, Stack, Text } from '@mantine/core';
import { useLongPress } from '@mantine/hooks';
import { useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import { textClasses } from 'styles';
import type { Theme } from 'types';
import { ThemeCard } from '../ThemeCard';
import { ThemeMiniCard } from '../ThemeMiniCard';
import { ThemePreviewMiddleware } from '../ThemePreviewMiddleware';
export function NewThemeStarter({
allThemes,
setIsNew,
duplicate,
setDuplicate,
selectedTheme,
setSelectedTheme,
}: {
allThemes: Theme[];
setIsNew: (isNew: boolean) => void;
duplicate: boolean;
setDuplicate: (duplicate: boolean) => void;
selectedTheme: Theme;
setSelectedTheme: (selectedTheme: Theme) => void;
}) {
const [pressed, setPressed] = useState<any>(false);
const handlers = useLongPress(() => {
setPressed(true);
});
const randomTheme = {
...allThemes[Math.floor(Math.random() * allThemes.length)],
randomEnabled: 1,
};
return (
<Stack align='center' p={'2rem'}>
{!duplicate ? (
<>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Please choose an option:
</Text>
<Group>
<Stack
justify='center'
align='center'
bg={'var(--mantine-color-primary-0)'}
px={'1rem'}
py={'3rem'}
style={{ borderRadius: '8px' }}
>
<ThemeCard
cardTheme={{
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
}}
allThemes={[]}
themePlaylists={[]}
setSelectedTheme={setSelectedTheme}
openThemeModal={() => {}}
/>
<Button
pos={'absolute'}
w={'275px'}
h={'115px'}
style={{
backgroundColor: 'transparent',
boxShadow: '3px 3px var(--mantine-color-border-0)',
overflow: 'visible',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-background-0)',
color: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '-16px',
fontSize: '1rem',
},
}}
onClick={() => setIsNew(false)}
>
New Theme from Scratch
</Button>
</Stack>
<Divider
color={'var(--mantine-color-primary-0)'}
my={'sm'}
orientation='vertical'
m={'2rem'}
/>
<Stack
justify='center'
align='center'
pos={'relative'}
bg={'var(--mantine-color-primary-0)'}
px={'1rem'}
py={'3rem'}
style={{ borderRadius: '8px' }}
>
<ThemeCard
cardTheme={randomTheme}
allThemes={[]}
themePlaylists={[]}
setSelectedTheme={setSelectedTheme}
openThemeModal={() => {}}
/>
<Overlay
bg={'transparent'}
zIndex={0}
pos={'absolute'}
w={'275px'}
h={'115px'}
top={'3rem'}
left={'1rem'}
style={{
backdropFilter: 'grayscale(80%) brightness(80%)',
mask: 'linear-gradient(105deg, rgba(0, 0, 0, 1) 48%, rgba(0, 0, 0, 0) 52%)',
alignContent: 'center',
justifyContent: 'center',
display: 'flex',
}}
/>
<FaArrowRight
size={45}
style={{
color: 'var(--mantine-color-background-0)',
position: 'absolute',
stroke: 'var(--mantine-color-accent-0)',
strokeWidth: 25,
}}
/>
<Button
pos={'absolute'}
w={'275px'}
h={'115px'}
style={{
backgroundColor: 'transparent',
boxShadow: '3px 3px var(--mantine-color-border-0)',
overflow: 'visible',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-background-0)',
color: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '-16px',
fontSize: '1rem',
},
}}
onClick={() => {
setDuplicate(true);
setSelectedTheme(randomTheme);
}}
>
Duplicate existing Theme
</Button>
</Stack>
</Group>
</>
) : (
<Stack align='center'>
<Group
w={'100%'}
justify='center'
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-accent-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
{allThemes?.map((miniTheme: Theme) => {
return (
<Box
{...handlers}
key={miniTheme.id}
onClick={() => {
!pressed && setSelectedTheme(miniTheme);
setPressed(false);
}}
>
<ThemePreviewMiddleware
newThemeList={[miniTheme.id as number]}
miniTheme={miniTheme}
themeList={[]}
selectedPlaylist={undefined}
setSelectedPlaylist={() => {}}
/>
</Box>
);
})}
</Group>
<Group>
<Group
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
<ThemeMiniCard miniTheme={selectedTheme} />
<Button
disabled={selectedTheme.name === 'default'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
name: `${selectedTheme.name} copy`,
});
setIsNew(false);
}}
>
Confirm
</Button>
</Group>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
setDuplicate(false);
setSelectedTheme({
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
}}
>
Back
</Button>
</Group>
</Stack>
)}
</Stack>
);
}

View File

@@ -0,0 +1,643 @@
import {
ActionIcon,
Button,
Checkbox,
ColorPicker,
ColorSwatch,
Group,
Image,
ScrollArea,
Stack,
Text,
TextInput,
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks } from 'hooks';
import { useRef, useState } from 'react';
import { FaShuffle } from 'react-icons/fa6';
import { FiImage, FiSave, FiShuffle, FiX } from 'react-icons/fi';
import { scrollAreaClasses, textClasses, textInputClasses } from 'styles';
import { BackgroundProperties, type Theme } from 'types';
import { ThemeModalContent } from '../ThemeModalContent';
export function ThemeEditor({
backgroundList,
defaultTheme,
designerTheme,
setDesignerTheme,
selectedTheme,
setSelectedTheme,
pickerOpen,
openPicker,
closePicker,
handleClose,
handleSaveTheme,
setDuplicate,
}: {
backgroundList: string[];
defaultTheme: Theme;
designerTheme: Theme;
setDesignerTheme: (theme: Theme) => void;
selectedTheme: Theme;
setSelectedTheme: (theme: Theme) => void;
pickerOpen: boolean;
openPicker: () => void;
closePicker: () => void;
handleClose: () => void;
handleSaveTheme: () => void;
setDuplicate: (duplicate: boolean) => void;
}) {
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const { isMobile, theme, getObjectDiff } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null);
const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000');
const re = /(?:\.([^.]+))?$/;
const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] =
useDisclosure(false);
const filtered = backgroundList.filter((item: string) =>
item.toLowerCase().includes(query.toLowerCase()),
);
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/thumbs/${item})`
: `no-repeat center url(../../api/backgroundImages/thumbs/${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);
};
const randomColor = (withAlpha?: boolean) => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < (withAlpha ? 8 : 6); i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
return (
<>
<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: 'var(--mantine-color-accent-0)',
}}
>
{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%)'
: undefined,
}}
>
Change{' '}
{BackgroundProperties[colorId as keyof typeof BackgroundProperties]}
</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
c={'white'}
onClick={() => {
const rc = randomColor();
setColorValue(rc);
setDesignerTheme({
...selectedTheme,
[colorId]: rc,
});
}}
>
<FiShuffle />
</Button>
<Button
color={'green'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
[colorId]: colorValue,
});
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%'} justify='space-between' align='center' gap={4}>
<Stack w={'100%'} align='center' gap={4}>
<Checkbox
label='Text Shadows'
checked={selectedTheme.textShadowsEnabled === 1}
c={'var(--mantine-color-background-0)'}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
c={'var(--mantine-color-background-0)'}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
</Stack>
{JSON.stringify(defaultTheme) !== JSON.stringify(selectedTheme) && (
<>
<Text c={'var(--mantine-color-background-0)'} fz={'1.5rem'}>
Changes:
</Text>
<ScrollArea.Autosize
w={'100%'}
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
style={{
backgroundColor: 'var(--mantine-color-background-0)',
borderRadius: 8,
padding: '1rem',
}}
>
<Stack w={'100%'} gap={0}>
{getObjectDiff(defaultTheme, designerTheme) !== null &&
Object.keys(getObjectDiff(defaultTheme, designerTheme)).map(
(key: any) => {
return (
<Stack
w={'100%'}
align='center'
justify='center'
gap={0}
key={key}
>
<Text key={key}>
{
BackgroundProperties[
key as keyof typeof BackgroundProperties
]
}
</Text>
{key === 'name' && (
<Group w={'100%'} justify='center'>
<Text>{defaultTheme[key as keyof Theme]}</Text>
<Text>→</Text>
<Text>{designerTheme[key as keyof Theme]}</Text>
</Group>
)}
{key === 'backgroundImage' && (
<Group w={'100%'} justify='center'>
<Image
w={'40%'}
src={
(
defaultTheme[key as keyof Theme] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${defaultTheme[key as keyof Theme] as string}`
}
/>
<Text>→</Text>
<Image
w={'40%'}
src={
(
designerTheme[key as keyof Theme] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}`
}
/>
</Group>
)}
{key.includes('Color') && (
<Group w={'100%'} justify='center'>
<ColorSwatch
color={defaultTheme[key as keyof Theme] as string}
/>
<Text>→</Text>
<ColorSwatch
color={designerTheme[key as keyof Theme] as string}
/>
</Group>
)}
{key.includes('Shadow') && (
<Group w={'100%'} justify='center'>
{defaultTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
<Text>→</Text>
{designerTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
</Group>
)}
</Stack>
);
},
)}
</Stack>
</ScrollArea.Autosize>
</>
)}
<Group mih={'36px'}>
<Tooltip
label={'Randomize Colors!'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<Button
onClick={() => {
setSelectedTheme({
...selectedTheme,
backgroundColor: randomColor(),
searchColor: randomColor(),
primaryColor: randomColor(),
accentColor: randomColor(),
borderColor: randomColor(),
});
}}
>
<FaShuffle />
</Button>
</Tooltip>
<Button
onClick={() => {
setSelectedTheme({ ...defaultTheme });
}}
>
Reset Changes
</Button>
</Group>
</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={() => {
handleClose();
closeBackgroundPicker();
setDuplicate(false);
}}
>
Cancel
</Button>
</Group>
</>
);
}

View File

@@ -0,0 +1,148 @@
import { Group, Modal, Text, TextInput } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { modalClasses, textInputClasses } from 'styles';
import type { Theme } from 'types';
import { NewThemeStarter } from './NewThemeStarter';
import { ThemeEditor } from './ThemeEditor';
export function ThemeModal({
opened,
selectedTheme,
setSelectedTheme,
backgroundList,
isNew,
setIsNew,
allThemes,
closeThemeModal,
}: {
opened: boolean;
selectedTheme: Theme;
setSelectedTheme: (selectedTheme: Theme) => void;
backgroundList: string[];
isNew: boolean;
setIsNew: (isNew: boolean) => void;
allThemes: Theme[];
closeThemeModal: () => void;
}) {
const themesEndpoint = 'api/themes';
const { isMobile } = useCommonHooks();
const [defaultTheme, setDefaultTheme] = useState<Theme>({ ...selectedTheme });
const [designerTheme, setDesignerTheme] = useState<Theme>({
...selectedTheme,
});
const { mutateAsync } = useMutationAPI();
const [pickerOpen, { open: openPicker, close: closePicker }] = useDisclosure(false);
const [duplicate, setDuplicate] = useState(false);
const handleClose = () => {
closeThemeModal();
closePicker();
setIsNew(false);
setSelectedTheme({
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
};
const handleSaveTheme = () => {
mutateAsync({
method: selectedTheme?.id === undefined ? 'POST' : 'PATCH',
endpoint: themesEndpoint,
body: selectedTheme,
});
handleClose();
};
useEffect(() => {
setDesignerTheme(selectedTheme);
if (defaultTheme.id !== selectedTheme.id) {
setDefaultTheme(selectedTheme);
}
}, [selectedTheme]);
return (
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={() => {
handleClose();
setDuplicate(false);
}}
fullScreen={isMobile}
centered
keepMounted={true}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
{!isNew && (
<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>
{isNew ? (
<NewThemeStarter
allThemes={allThemes}
setIsNew={setIsNew}
duplicate={duplicate}
setDuplicate={setDuplicate}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
/>
) : (
<ThemeEditor
designerTheme={designerTheme}
setDesignerTheme={setDesignerTheme}
selectedTheme={selectedTheme}
closePicker={closePicker}
handleSaveTheme={handleSaveTheme}
backgroundList={backgroundList}
defaultTheme={defaultTheme}
openPicker={openPicker}
pickerOpen={pickerOpen}
setSelectedTheme={setSelectedTheme}
handleClose={handleClose}
setDuplicate={setDuplicate}
/>
)}
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}

View File

@@ -1,7 +1,17 @@
import { Box, Button, Group, Overlay, Stack, Switch, Text, Tooltip } from '@mantine/core';
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, useMutationAPI, useQueryAPI } from 'hooks';
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';
@@ -10,10 +20,9 @@ import { switchClasses, textClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import { BackgroundModal } from '../Backgrounds';
import { ThemeCard } from './ThemeCard';
import { ThemeModal } from './ThemeModal';
import { ThemeModal } from './ThemeModal/ThemeModal';
export function Themes() {
const themesEndpoint = 'api/themes';
const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData, isFetched: isAllThemeDataFetched } = useQueryAPI({
endpoint: 'api/themes',
@@ -26,7 +35,6 @@ export function Themes() {
endpoint: 'api/themePlaylists',
});
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
const { mutateAsync } = useMutationAPI();
const [themeModalOpened, { open: openThemeModal, close: closeThemeModal }] =
useDisclosure(false);
const [
@@ -48,7 +56,6 @@ export function Themes() {
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);
@@ -63,44 +70,11 @@ export function Themes() {
setCurrentThemePlaylist,
} = useCommonHooks();
const handleOpen = (theme: Theme) => {
setSelectedTheme(theme);
openThemeModal();
};
const handleClose = () => {
closeThemeModal();
closePicker();
setIsNew(false);
setSelectedTheme({
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
};
const handleNewTheme = () => {
setIsNew(true);
openThemeModal();
};
const handleSaveTheme = () => {
mutateAsync({
method: selectedTheme?.id === undefined ? 'POST' : 'PATCH',
endpoint: themesEndpoint,
body: selectedTheme,
});
handleClose();
};
useEffect(() => {
if (backgroundData && isBackgroundFetched) {
setBackgroundList(['None', ...backgroundData]);
@@ -146,17 +120,13 @@ export function Themes() {
<Stack>
<ThemeModal
opened={themeModalOpened}
handleClose={handleClose}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
openPicker={openPicker}
backgroundList={backgroundList}
pickerOpen={pickerOpen}
closePicker={closePicker}
handleSaveTheme={handleSaveTheme}
isNew={isNew}
setIsNew={setIsNew}
allThemes={allThemes}
closeThemeModal={closeThemeModal}
/>
<BackgroundModal
backgroundModalOpened={backgroundModalOpened}
@@ -239,8 +209,9 @@ export function Themes() {
<ThemeCard
allThemes={allThemeData}
cardTheme={cardTheme}
handleOpen={handleOpen}
themePlaylists={themePlaylists}
setSelectedTheme={setSelectedTheme}
openThemeModal={openThemeModal}
/>
</Box>
</Group>
@@ -259,8 +230,8 @@ export function Themes() {
style={{
backdropFilter: 'blur(5px) brightness(.75)',
zIndex: 199,
paddingTop: isMobile ? '.5rem' : '1.5rem',
paddingBottom: '1.5rem',
paddingTop: '.5rem',
paddingBottom: '.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
width: '100%',
@@ -291,7 +262,16 @@ export function Themes() {
>
Current Settings:
</Text>
<Group>
<Group
style={{
backgroundColor: 'var(--mantine-color-search-0)',
borderRadius: '8px',
paddingTop: '.5rem',
paddingBottom: '.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
}}
>
<Group>
<Text
style={{
@@ -311,6 +291,11 @@ export function Themes() {
{currentThemePlaylist === 'All' ? 'All' : currentThemePlaylist}
</Text>
</Group>
<Divider
orientation='vertical'
size={'xs'}
color={'var(--mantine-color-primary-0)'}
/>
<Button
onClick={() => {
if (currentThemePlaylist === 'All') {

View File

@@ -4,7 +4,7 @@ export { ThemeCard } from './ThemeCard';
export { ThemeExampleApp } from './ThemeExampleApp';
export { ThemeManagementPill } from './ThemeManagementPill';
export { ThemeMiniCard } from './ThemeMiniCard';
export { ThemeModal } from './ThemeModal';
export { ThemeModal } from './ThemeModal/ThemeModal';
export { ThemeModalColorSwatches } from './ThemeModalColorSwatches';
export { ThemeModalContent } from './ThemeModalContent';
export { ThemePreviewMiddleware } from './ThemePreviewMiddleware';