Style updates and bugfixes
This commit is contained in:
205
src/pages/settings/components/Theming/Themes/ThemeCard.tsx
Normal file
205
src/pages/settings/components/Theming/Themes/ThemeCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
549
src/pages/settings/components/Theming/Themes/ThemeModal.tsx
Normal file
549
src/pages/settings/components/Theming/Themes/ThemeModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
126
src/pages/settings/components/Theming/Themes/ThemeSelect.tsx
Normal file
126
src/pages/settings/components/Theming/Themes/ThemeSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
381
src/pages/settings/components/Theming/Themes/Themes.tsx
Normal file
381
src/pages/settings/components/Theming/Themes/Themes.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
src/pages/settings/components/Theming/Themes/TimeOfDay.tsx
Normal file
52
src/pages/settings/components/Theming/Themes/TimeOfDay.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
src/pages/settings/components/Theming/Themes/index.ts
Normal file
11
src/pages/settings/components/Theming/Themes/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user