New Theme modal

This commit is contained in:
Sean Edmunds
2025-07-24 13:35:05 -05:00
parent e0adad34ba
commit b8c10ef75b
5 changed files with 763 additions and 501 deletions

16
package-lock.json generated
View File

@@ -32,7 +32,7 @@
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.2.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.5", "vite": "^7.0.6",
"vite-plugin-dts": "~4.5.4", "vite-plugin-dts": "~4.5.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
@@ -3468,9 +3468,9 @@
"dev": true "dev": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4506,15 +4506,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"picomatch": "^4.0.2", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.40.0", "rollup": "^4.40.0",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.14"

View File

@@ -34,7 +34,7 @@
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.2.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.5", "vite": "^7.0.6",
"vite-plugin-dts": "~4.5.4", "vite-plugin-dts": "~4.5.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"

View File

@@ -1,4 +1,4 @@
import { Button, Group, Modal, SimpleGrid, Text, TextInput } from '@mantine/core'; import { Button, Group, Modal, Text, TextInput } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { modalClasses, textInputClasses } from 'styles'; import { modalClasses, textInputClasses } from 'styles';
@@ -87,7 +87,7 @@ export function ThemePlaylistModal({
/> />
) : ( ) : (
<> <>
<SimpleGrid cols={3}> <Group w={'100%'} justify='center'>
{selectedPlaylist?.name !== 'Time of Day' && {selectedPlaylist?.name !== 'Time of Day' &&
allThemes?.map((miniTheme: Theme) => { allThemes?.map((miniTheme: Theme) => {
const newThemeList = themeList; const newThemeList = themeList;
@@ -102,7 +102,7 @@ export function ThemePlaylistModal({
/> />
); );
})} })}
</SimpleGrid> </Group>
<Group mt={'1rem'}> <Group mt={'1rem'}>
<Button color={'green'} onClick={handleSavePlaylist}> <Button color={'green'} onClick={handleSavePlaylist}>
Save Save

View File

@@ -1,12 +1,15 @@
import { import {
ActionIcon, ActionIcon,
Box,
Button, Button,
Checkbox, Checkbox,
ColorPicker, ColorPicker,
ColorSwatch, ColorSwatch,
Divider,
Group, Group,
Image, Image,
Modal, Modal,
Overlay,
ScrollArea, ScrollArea,
Stack, Stack,
Text, Text,
@@ -14,13 +17,17 @@ import {
Tooltip, Tooltip,
UnstyledButton, UnstyledButton,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure, useLongPress } from '@mantine/hooks';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import { FiImage, FiSave, FiX } from 'react-icons/fi'; import { FiImage, FiSave, FiX } from 'react-icons/fi';
import { modalClasses, scrollAreaClasses, textClasses, textInputClasses } from 'styles'; import { modalClasses, scrollAreaClasses, textClasses, textInputClasses } from 'styles';
import { BackgroundProperties, type Theme } from 'types'; import { BackgroundProperties, type Theme } from 'types';
import { ThemeCard } from './ThemeCard';
import { ThemeMiniCard } from './ThemeMiniCard';
import { ThemeModalContent } from './ThemeModalContent'; import { ThemeModalContent } from './ThemeModalContent';
import { ThemePreviewMiddleware } from './ThemePreviewMiddleware';
export function ThemeModal({ export function ThemeModal({
opened, opened,
@@ -32,6 +39,9 @@ export function ThemeModal({
pickerOpen, pickerOpen,
closePicker, closePicker,
handleSaveTheme, handleSaveTheme,
isNew,
setIsNew,
allThemes,
}: { }: {
opened: boolean; opened: boolean;
handleClose: () => void; handleClose: () => void;
@@ -42,7 +52,14 @@ export function ThemeModal({
pickerOpen: boolean; pickerOpen: boolean;
closePicker: () => void; closePicker: () => void;
handleSaveTheme: () => 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 { isMobile, theme, getObjectDiff } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
@@ -56,6 +73,7 @@ export function ThemeModal({
const filtered = backgroundList.filter((item: string) => const filtered = backgroundList.filter((item: string) =>
item.toLowerCase().includes(query.toLowerCase()), item.toLowerCase().includes(query.toLowerCase()),
); );
const [duplicate, setDuplicate] = useState(false);
const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] = const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] =
useDisclosure(false); useDisclosure(false);
@@ -140,13 +158,17 @@ export function ThemeModal({
className={modalClasses.editThemeModal} className={modalClasses.editThemeModal}
classNames={modalClasses} classNames={modalClasses}
opened={opened} opened={opened}
onClose={handleClose} onClose={() => {
handleClose();
setDuplicate(false);
}}
fullScreen={isMobile} fullScreen={isMobile}
centered centered
> >
<Modal.Overlay /> <Modal.Overlay />
<Modal.Content> <Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}> <Modal.Header style={{ justifyContent: 'center' }}>
{!isNew && (
<Group pos={'absolute'}> <Group pos={'absolute'}>
<Text c={'var(--mantine-color-background-0)'}>Name:</Text> <Text c={'var(--mantine-color-background-0)'}>Name:</Text>
<TextInput <TextInput
@@ -169,9 +191,228 @@ export function ThemeModal({
}} }}
/> />
</Group> </Group>
)}
<Modal.CloseButton /> <Modal.CloseButton />
</Modal.Header> </Modal.Header>
<Modal.Body> <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-accent-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)',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '0',
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-accent-0)'}
px={'1rem'}
py={'3rem'}
style={{ borderRadius: '8px' }}
>
<ThemeCard
cardTheme={{
id: undefined,
name: 'Theme',
backgroundImage: 'none',
backgroundColor: '#263238',
searchColor: '#AABBC320',
primaryColor: '#AABBC3',
accentColor: '#aeea00',
borderColor: '#aeea00',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
}}
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={35} style={{ position: 'absolute' }} />
<Button
pos={'absolute'}
w={'275px'}
h={'115px'}
style={{
backgroundColor: 'transparent',
boxShadow: '3px 3px var(--mantine-color-border-0)',
}}
styles={{
label: {
backgroundColor: 'var(--mantine-color-primary-0)',
height: 'min-content',
padding: '.5rem',
borderRadius: 4,
position: 'absolute',
bottom: '0',
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'> <Group p={'2rem'} w={'100%'} align='flex-start' justify='space-between'>
<Stack display={'flex'} pos={'relative'} w={isMobile ? '100%' : '70%'}> <Stack display={'flex'} pos={'relative'} w={isMobile ? '100%' : '70%'}>
<ThemeModalContent selectedTheme={designerTheme} /> <ThemeModalContent selectedTheme={designerTheme} />
@@ -305,7 +546,11 @@ export function ThemeModal({
}} }}
> >
Change{' '} Change{' '}
{BackgroundProperties[colorId as keyof typeof BackgroundProperties]} {
BackgroundProperties[
colorId as keyof typeof BackgroundProperties
]
}
</Text> </Text>
{colorId === 'backgroundColor' && ( {colorId === 'backgroundColor' && (
<ColorPicker <ColorPicker
@@ -547,8 +792,9 @@ export function ThemeModal({
> >
<Stack w={'100%'} gap={0}> <Stack w={'100%'} gap={0}>
{getObjectDiff(defaultTheme, designerTheme) !== null && {getObjectDiff(defaultTheme, designerTheme) !== null &&
Object.keys(getObjectDiff(defaultTheme, designerTheme)).map( Object.keys(
(key: any) => { getObjectDiff(defaultTheme, designerTheme),
).map((key: any) => {
return ( return (
<Stack <Stack
w={'100%'} w={'100%'}
@@ -588,7 +834,9 @@ export function ThemeModal({
w={'40%'} w={'40%'}
src={ src={
( (
designerTheme[key as keyof Theme] as string designerTheme[
key as keyof Theme
] as string
).toLocaleLowerCase() === 'none' ).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None' ? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}` : `../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}`
@@ -628,8 +876,7 @@ export function ThemeModal({
)} )}
</Stack> </Stack>
); );
}, })}
)}
</Stack> </Stack>
</ScrollArea.Autosize> </ScrollArea.Autosize>
</> </>
@@ -648,7 +895,9 @@ export function ThemeModal({
</Group> </Group>
<Group> <Group>
<Button <Button
disabled={JSON.stringify(defaultTheme) === JSON.stringify(selectedTheme)} disabled={
JSON.stringify(defaultTheme) === JSON.stringify(selectedTheme)
}
color={'green'} color={'green'}
onClick={handleSaveTheme} onClick={handleSaveTheme}
> >
@@ -660,11 +909,14 @@ export function ThemeModal({
onClick={() => { onClick={() => {
closeBackgroundPicker(); closeBackgroundPicker();
handleClose(); handleClose();
setDuplicate(false);
}} }}
> >
Cancel Cancel
</Button> </Button>
</Group> </Group>
</>
)}
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
</Modal.Root> </Modal.Root>

View File

@@ -44,7 +44,7 @@ export function Themes() {
primaryColor: '#222222', primaryColor: '#222222',
accentColor: '#5c5c5c', accentColor: '#5c5c5c',
borderColor: '#5c5c5c', borderColor: '#5c5c5c',
randomEnabled: 0, randomEnabled: 1,
textShadowsEnabled: 0, textShadowsEnabled: 0,
appShadowsEnabled: 0, appShadowsEnabled: 0,
}); });
@@ -52,6 +52,7 @@ export function Themes() {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
currentTheme, currentTheme,
@@ -70,6 +71,7 @@ export function Themes() {
const handleClose = () => { const handleClose = () => {
closeThemeModal(); closeThemeModal();
closePicker(); closePicker();
setIsNew(false);
setSelectedTheme({ setSelectedTheme({
id: undefined, id: undefined,
name: 'default', name: 'default',
@@ -79,12 +81,17 @@ export function Themes() {
primaryColor: '#222222', primaryColor: '#222222',
accentColor: '#5c5c5c', accentColor: '#5c5c5c',
borderColor: '#5c5c5c', borderColor: '#5c5c5c',
randomEnabled: 0, randomEnabled: 1,
textShadowsEnabled: 0, textShadowsEnabled: 0,
appShadowsEnabled: 0, appShadowsEnabled: 0,
}); });
}; };
const handleNewTheme = () => {
setIsNew(true);
openThemeModal();
};
const handleSaveTheme = () => { const handleSaveTheme = () => {
mutateAsync({ mutateAsync({
method: selectedTheme?.id === undefined ? 'POST' : 'PATCH', method: selectedTheme?.id === undefined ? 'POST' : 'PATCH',
@@ -147,6 +154,9 @@ export function Themes() {
pickerOpen={pickerOpen} pickerOpen={pickerOpen}
closePicker={closePicker} closePicker={closePicker}
handleSaveTheme={handleSaveTheme} handleSaveTheme={handleSaveTheme}
isNew={isNew}
setIsNew={setIsNew}
allThemes={allThemes}
/> />
<BackgroundModal <BackgroundModal
backgroundModalOpened={backgroundModalOpened} backgroundModalOpened={backgroundModalOpened}
@@ -156,7 +166,7 @@ export function Themes() {
/> />
<SettingsHeader <SettingsHeader
setDragging={undefined} setDragging={undefined}
openModal={openThemeModal} openModal={handleNewTheme}
title={'Themes'} title={'Themes'}
tooltip={{ tooltip={{
title: 'Themes can be created here and applied to the homepage.', title: 'Themes can be created here and applied to the homepage.',