Compare commits

...

17 Commits

Author SHA1 Message Date
148bef7dfd Package updates
All checks were successful
Deploy to production / build (push) Successful in 20s
2026-10-23 11:27:04 -05:00
e6c5f02a50 Sort fixes
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-20 19:19:27 -05:00
2f8d3ad58e Added persistent sort to more locations
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-20 18:58:52 -05:00
2cf34f86fa Minor fixes
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-20 16:13:51 -05:00
ecf061da1f Refactor settings management and add mutation error handling
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-20 14:16:20 -05:00
69dae97a79 Minor fixes
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-15 17:05:26 -05:00
ffd241dad8 Minor updates to styles and package updates
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-15 13:58:35 -05:00
142a45ef54 Bug fix
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-15 12:14:58 -05:00
4bcd1e4858 Working preview on time of day playlist
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-13 14:05:51 -05:00
1f86881533 Performance tweaks
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-13 13:52:24 -05:00
a78ed0dad0 Improvements to playlist stacks
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-13 13:31:28 -05:00
1e614790f6 Delete bug fix
All checks were successful
Deploy to production / build (push) Successful in 19s
2026-10-13 11:58:52 -05:00
e4b07a3907 Updates to settings to allow sorting
All checks were successful
Deploy to production / build (push) Successful in 20s
2026-10-13 11:37:27 -05:00
01dd6b1aa2 Adding sorting to items in settings
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-12 17:10:03 -05:00
78e6bd3374 Minor style fix
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-12 16:17:50 -05:00
bd9e77c382 Updates to the stack for performance
All checks were successful
Deploy to production / build (push) Successful in 18s
2026-10-12 16:04:09 -05:00
ea56aa8402 New less heavy component for previews
All checks were successful
Deploy to production / build (push) Successful in 17s
2026-10-12 13:43:14 -05:00
55 changed files with 1784 additions and 1168 deletions

View File

@@ -116,11 +116,8 @@ sudo pm2 start server.cjs
### TODO ### TODO
- Mobile improvements - Mobile improvements
- New Carosel on weather description - New Carousel on weather description
- Updates to weather modal
- Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led) - Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led)
- Update Theme playlists
- Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components)
- Explore https://gfazioli.github.io/mantine-scene/ - Explore https://gfazioli.github.io/mantine-scene/
- Explore https://gfazioli.github.io/mantine-window/ - Explore https://gfazioli.github.io/mantine-window/
- Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random) - Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random)

532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,43 +10,43 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@gfazioli/mantine-border-animate": "^3.0.5", "@gfazioli/mantine-border-animate": "^3.0.7",
"@gfazioli/mantine-depth-select": "^2.0.15", "@gfazioli/mantine-depth-select": "^2.0.17",
"@gfazioli/mantine-parallax": "^4.0.13", "@gfazioli/mantine-parallax": "^4.0.15",
"@gfazioli/mantine-reflection": "^3.0.13", "@gfazioli/mantine-reflection": "^3.0.15",
"@gfazioli/mantine-text-animate": "^4.0.15", "@gfazioli/mantine-text-animate": "^4.0.17",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^9.7.1", "@mantine/core": "^9.7.3",
"@mantine/dates": "^9.7.1", "@mantine/dates": "^9.7.3",
"@mantine/form": "^9.7.1", "@mantine/form": "^9.7.3",
"@mantine/hooks": "^9.7.1", "@mantine/hooks": "^9.7.3",
"@mantine/notifications": "^9.7.1", "@mantine/notifications": "^9.7.3",
"@tanstack/react-query": "^5.104.1", "@tanstack/react-query": "^5.104.5",
"@tanstack/react-query-devtools": "^5.104.1", "@tanstack/react-query-devtools": "^5.104.5",
"@tanstack/react-query-persist-client": "^5.104.1", "@tanstack/react-query-persist-client": "^5.104.5",
"@tanstack/react-table": "^9.2.8", "@tanstack/react-table": "^9.3.1",
"@tanstack/react-virtual": "^3.14.14", "@tanstack/react-virtual": "^3.14.22",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"jotai": "^3.0.1", "jotai": "^3.0.1",
"openmeteo": "^1.2.3", "openmeteo": "^1.2.3",
"react": "^19.3.0", "react": "^19.3.1",
"react-dom": "^19.3.0", "react-dom": "^19.3.1",
"react-fontpicker-ts": "^1.3.0", "react-fontpicker-ts": "^1.3.0",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4", "react-router-dom": "^7.18.4",
"recharts": "^3.10.1" "recharts": "^3.10.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^26.6.4", "@types/node": "^26.6.5",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2", "@vitejs/plugin-react": "^6.1.2",
"@vitest/coverage-v8": "^5.0.3", "@vitest/coverage-v8": "^5.0.5",
"@vitest/ui": "^5.0.3", "@vitest/ui": "^5.0.5",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.3.4", "vite": "^8.3.5",
"vite-plugin-dts": "~5.1.2", "vite-plugin-dts": "~5.1.2",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^5.0.3" "vitest": "^5.0.5"
} }
} }

View File

@@ -37,17 +37,11 @@ function App() {
const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({ const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({
endpoint: 'api/themePlaylists', endpoint: 'api/themePlaylists',
}); });
const { data: personalData, isFetched: isPersonalDataFetched } = useQueryAPI({ const { data: personalData } = useQueryAPI({
endpoint: 'api/personalData', endpoint: 'api/personalData',
}); });
const { const { settings, updateSetting, theme, setTheme, handleThemeChange } =
theme, useCommonHooks();
setTheme,
themeName,
setThemeName,
handleThemeChange,
currentThemePlaylist,
} = useCommonHooks();
const [loaded, setLoaded] = useState<boolean>(false); const [loaded, setLoaded] = useState<boolean>(false);
const overrides = createTheme({ const overrides = createTheme({
primaryColor: 'primary', primaryColor: 'primary',
@@ -68,22 +62,23 @@ function App() {
'shift + t', 'shift + t',
() => { () => {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylists?.find((t: ThemePlaylist) => t?.name === currentThemePlaylist) themePlaylists?.find(
?.themeList ?? '[]', (t: ThemePlaylist) => t?.name === settings.currentThemePlaylist,
)?.themeList ?? '[]',
); );
if (themeList.length > 0 && currentThemePlaylist !== 'Time of Day') { if (themeList.length > 0 && settings.currentThemePlaylist !== 'Time of Day') {
const filteredThemes = allThemes.filter((t: Theme) => themeList.includes(t.id)); const filteredThemes = allThemes.filter((t: Theme) => themeList.includes(t.id));
handleThemeChange(filteredThemes, false); handleThemeChange(filteredThemes, false);
} else if (currentThemePlaylist === 'All') { } else if (settings.currentThemePlaylist === 'All') {
handleThemeChange(allThemes, true); handleThemeChange(allThemes, true);
} else if (currentThemePlaylist === 'Backgrounds') { } else if (settings.currentThemePlaylist === 'Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter( allThemes.filter(
(a: any) => !a.backgroundImage.toLowerCase().includes('none'), (a: any) => !a.backgroundImage.toLowerCase().includes('none'),
), ),
false, false,
); );
} else if (currentThemePlaylist === 'No Backgrounds') { } else if (settings.currentThemePlaylist === 'No Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter((a: any) => allThemes.filter((a: any) =>
a.backgroundImage.toLowerCase().includes('none'), a.backgroundImage.toLowerCase().includes('none'),
@@ -119,22 +114,26 @@ function App() {
useEffect(() => { useEffect(() => {
if ( if (
allThemes && allThemes &&
themeName && settings.theme &&
personalData && personalData &&
Object.keys(personalData).length > 0 && Object.keys(personalData).length > 0 &&
isThemePlaylistsFetched isThemePlaylistsFetched
) { ) {
if (currentThemePlaylist === 'All') { Object.keys(personalData[0]).forEach((key: any) => {
if (themeName === 'random') { updateSetting(key, personalData[0][key]);
});
if (settings.currentThemePlaylist === 'All') {
if (settings.theme === 'random') {
handleThemeChange(allThemes, false); handleThemeChange(allThemes, false);
} else { } else {
const temp = allThemes.find((t: Theme) => t.name === themeName); const temp = allThemes.find((t: Theme) => t.name === settings.theme);
setTheme(temp); setTheme(temp);
} }
} else if (currentThemePlaylist === 'Time of Day') { } else if (settings.currentThemePlaylist === 'Time of Day') {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylists?.find((t: ThemePlaylist) => t.name === currentThemePlaylist) themePlaylists?.find(
.themeList, (t: ThemePlaylist) => t.name === settings.currentThemePlaylist,
).themeList,
); );
setTheme( setTheme(
allThemes.find( allThemes.find(
@@ -142,34 +141,35 @@ function App() {
t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]), t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]),
), ),
); );
} else if (currentThemePlaylist === 'Backgrounds') { } else if (settings.currentThemePlaylist === 'Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter((a: any) => !a.backgroundImage.toLowerCase().includes('none')), allThemes.filter((a: any) => !a.backgroundImage.toLowerCase().includes('none')),
false, false,
); );
} else if (currentThemePlaylist === 'No Backgrounds') { } else if (settings.currentThemePlaylist === 'No Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter((a: any) => a.backgroundImage.toLowerCase().includes('none')), allThemes.filter((a: any) => a.backgroundImage.toLowerCase().includes('none')),
false, false,
); );
} else { } else {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylists?.find((t: ThemePlaylist) => t.name === currentThemePlaylist) themePlaylists?.find(
.themeList, (t: ThemePlaylist) => t.name === settings.currentThemePlaylist,
).themeList,
); );
const filteredThemes = allThemes.filter((t: Theme) => themeList.includes(t.id)); const filteredThemes = allThemes.filter((t: Theme) => themeList.includes(t.id));
setThemeName('random'); updateSetting('theme', 'random');
handleThemeChange(filteredThemes, false); handleThemeChange(filteredThemes, false);
} }
setLoaded(true); setLoaded(true);
} }
}, [ }, [
isAllThemesFetched, isAllThemesFetched,
themeName, settings.theme,
allThemes, allThemes,
currentThemePlaylist, settings.currentThemePlaylist,
isThemePlaylistsFetched, isThemePlaylistsFetched,
isPersonalDataFetched, personalData,
]); ]);
return ( return (
<> <>

View File

@@ -57,7 +57,7 @@ export function AppItem({
h={height} h={height}
w={width} w={width}
duration={3} duration={3}
beamMode={'conic'} beamMode={'wedge'}
borderWidth={scale * 3} borderWidth={scale * 3}
borderOpacity={0.75} borderOpacity={0.75}
size={(scale * 48) / (width / height < 2 ? 2 : 1)} size={(scale * 48) / (width / height < 2 ? 2 : 1)}

View File

@@ -3,6 +3,7 @@ import {
Button, Button,
Group, Group,
Highlight, Highlight,
Menu,
Popover, Popover,
Stack, Stack,
Switch, Switch,
@@ -10,7 +11,8 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useState } from 'react'; import { useState } from 'react';
import { FiHelpCircle, FiLoader, FiPlus, FiX } from 'react-icons/fi'; import { FaSort } from 'react-icons/fa';
import { FiHelpCircle, FiPlus, FiX } from 'react-icons/fi';
import { import {
actionIconClasses, actionIconClasses,
buttonClasses, buttonClasses,
@@ -31,6 +33,9 @@ export function SettingsHeader({
searchPlaceholder, searchPlaceholder,
isLoading, isLoading,
setIsLoading, setIsLoading,
sortBy,
setSortBy,
customSearchEnabled,
}: { }: {
setDragging: undefined | ((dragging: boolean) => void); setDragging: undefined | ((dragging: boolean) => void);
openModal: undefined | (() => void); openModal: undefined | (() => void);
@@ -45,6 +50,9 @@ export function SettingsHeader({
searchPlaceholder: string | undefined; searchPlaceholder: string | undefined;
isLoading: boolean; isLoading: boolean;
setIsLoading: (isLoading: boolean) => void; setIsLoading: (isLoading: boolean) => void;
sortBy: string;
setSortBy: undefined | ((sortBy: string) => void);
customSearchEnabled: boolean;
}) { }) {
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
@@ -123,52 +131,147 @@ export function SettingsHeader({
)} )}
</Group> </Group>
{searchValue !== undefined && ( {searchValue !== undefined && (
<DebouncedInput <Group
disabled={disabled} gap={4}
w={isMobile ? undefined : '35%'} w={isMobile ? undefined : '35%'}
pos={isMobile ? undefined : 'absolute'} pos={isMobile ? undefined : 'absolute'}
className={textInputClasses.default} >
classNames={textInputClasses} <DebouncedInput
autoFocus={!isMobile} disabled={disabled}
placeholder={searchPlaceholder ?? 'Search...'} w={'90%'}
value={searchValue} // pos={isMobile ? undefined : 'absolute'}
onInput={() => setIsLoading(true)} className={textInputClasses.default}
onChange={(value: any) => { classNames={textInputClasses}
setSearchValue(value); autoFocus={!isMobile}
}} placeholder={searchPlaceholder ?? 'Search...'}
style={{ value={searchValue}
zIndex: 100, onInput={() => setIsLoading(true)}
}} onChange={(value: any) => {
styles={ setSearchValue(value);
theme.textShadowsEnabled }}
? { style={{
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' }, zIndex: 100,
} }}
: undefined styles={
} theme.textShadowsEnabled
rightSection={ ? {
isLoading ? ( label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
<ActionIcon }
className={actionIconClasses.loadingSpinner} : undefined
classNames={actionIconClasses} }
> rightSection={
<FiLoader /> isLoading ? (
</ActionIcon>
) : (
searchValue && (
<ActionIcon <ActionIcon
className={actionIconClasses.settingsTooltip} className={actionIconClasses.loadingSpinner}
classNames={actionIconClasses} classNames={actionIconClasses}
onClick={() => setSearchValue('')} />
> ) : (
<FiX /> searchValue && (
</ActionIcon> <ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
onClick={() => setSearchValue('')}
>
<FiX />
</ActionIcon>
)
) )
) }
} />
/> {setSortBy !== undefined && !disabled && (
<Menu>
<Menu.Target>
<ActionIcon>
<FaSort />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Sort By</Menu.Label>
<Menu.Item
onClick={() => {
setSortBy('A>Z');
}}
style={{
color:
sortBy === 'A>Z'
? 'var(--mantine-color-background-0)'
: 'var(--mantine-color-primary-0)',
backgroundColor:
sortBy === 'A>Z' ? 'var(--mantine-color-primary-0)' : undefined,
}}
>
{'A>Z'}
</Menu.Item>
<Menu.Item
onClick={() => {
setSortBy('Z>A');
}}
style={{
color:
sortBy === 'Z>A'
? 'var(--mantine-color-background-0)'
: 'var(--mantine-color-primary-0)',
backgroundColor:
sortBy === 'Z>A' ? 'var(--mantine-color-primary-0)' : undefined,
}}
>
{'Z>A'}
</Menu.Item>
<Menu.Item
onClick={() => {
setSortBy('Newest');
}}
style={{
color:
sortBy === 'Newest'
? 'var(--mantine-color-background-0)'
: 'var(--mantine-color-primary-0)',
backgroundColor:
sortBy === 'Newest' ? 'var(--mantine-color-primary-0)' : undefined,
}}
>
{'Newest'}
</Menu.Item>
<Menu.Item
onClick={() => {
setSortBy('Oldest');
}}
style={{
color:
sortBy === 'Oldest'
? 'var(--mantine-color-background-0)'
: 'var(--mantine-color-primary-0)',
backgroundColor:
sortBy === 'Oldest' ? 'var(--mantine-color-primary-0)' : undefined,
}}
>
{'Oldest'}
</Menu.Item>
{customSearchEnabled && (
<Menu.Item
onClick={() => {
setSortBy('Custom');
}}
style={{
color:
sortBy === 'Custom'
? 'var(--mantine-color-background-0)'
: 'var(--mantine-color-primary-0)',
backgroundColor:
sortBy === 'Custom'
? 'var(--mantine-color-primary-0)'
: undefined,
}}
>
{'Custom'}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)}
</Group>
)} )}
{setDragging && ( {setDragging && sortBy === 'Custom' && (
<Switch <Switch
className={switchClasses.default} className={switchClasses.default}
classNames={switchClasses} classNames={switchClasses}

View File

@@ -19,6 +19,7 @@ export default function CurrentWeather({
<> <>
{current && ( {current && (
<Card <Card
w={'100%'}
style={{ style={{
color: 'var(--mantine-color-primary-0)', color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-search-0)', backgroundColor: 'var(--mantine-color-search-0)',

View File

@@ -1,7 +1,7 @@
import { ActionIcon, Box, Card, Group, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Card, Group, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { FaLocationArrow, FaSun, FaWind } from 'react-icons/fa'; import { FaArrowLeft, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { FaDroplet, FaX } from 'react-icons/fa6'; import { FaDroplet } from 'react-icons/fa6';
import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi'; import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import { WeatherCodes } from 'types'; import { WeatherCodes } from 'types';
@@ -32,29 +32,12 @@ export default function ExtendedForecastDetails({
borderRadius: '8px', borderRadius: '8px',
overflow: 'visible', overflow: 'visible',
cursor: 'default', cursor: 'default',
boxShadow:
'15px 15px 2px color-mix(in srgb, var(--mantine-color-border-0), transparent 50%)',
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
p={0} p={0}
> >
<ActionIcon
pos={'absolute'}
top={-12}
right={-12}
style={{
borderRadius: '100%',
boxShadow: '2px 2px var(--mantine-color-search-0)',
}}
onClick={() => {
setSelectedDay(undefined);
setSelectedHourly(undefined);
}}
>
<FaX />
</ActionIcon>
<Group <Group
w={'100%'} w={'100%'}
justify='center' justify='center'
@@ -66,11 +49,25 @@ export default function ExtendedForecastDetails({
borderTopRightRadius: '8px', borderTopRightRadius: '8px',
}} }}
> >
<ActionIcon
pos={'absolute'}
left={16}
style={{
borderRadius: '100%',
boxShadow: '2px 2px var(--mantine-color-search-0)',
}}
onClick={() => {
setSelectedDay(undefined);
setSelectedHourly(undefined);
}}
>
<FaArrowLeft />
</ActionIcon>
<Text fw={600} fz={'1.5rem'}> <Text fw={600} fz={'1.5rem'}>
{selectedDay.time.toLocaleDateString('en-US', { weekday: 'long' })} {selectedDay?.time.toLocaleDateString('en-US', { weekday: 'long' })}
</Text> </Text>
<Text fw={600} fz={'1.5rem'}> <Text fw={600} fz={'1.5rem'}>
{selectedDay.time.toLocaleDateString()} {selectedDay?.time.toLocaleDateString()}
</Text> </Text>
</Group> </Group>
<Stack align='center' p={16}> <Stack align='center' p={16}>
@@ -95,8 +92,8 @@ export default function ExtendedForecastDetails({
leftSide={<Text>Feels like</Text>} leftSide={<Text>Feels like</Text>}
rightSide={ rightSide={
<Text> <Text>
{selectedDay.apparentTemperatureMax?.toFixed(0) ?? {selectedDay?.apparentTemperatureMax?.toFixed(0) ??
selectedDay.temperatureMax?.toFixed(0)} selectedDay?.temperatureMax?.toFixed(0)}
°<sup>F</sup> °<sup>F</sup>
</Text> </Text>
} }
@@ -105,12 +102,12 @@ export default function ExtendedForecastDetails({
</Box> </Box>
<Group pos={'relative'}> <Group pos={'relative'}>
<Text fz={isMobile ? '5rem' : '7rem'} fw={400} style={{ lineHeight: 1 }}> <Text fz={isMobile ? '5rem' : '7rem'} fw={400} style={{ lineHeight: 1 }}>
{selectedDay.temperatureMax?.toFixed(0)} {selectedDay?.temperatureMax?.toFixed(0)}
<sup style={{ fontSize: '3rem' }}>°F</sup> <sup style={{ fontSize: '3rem' }}>°F</sup>
</Text> </Text>
<Text style={{ lineHeight: 1 }} pos={'absolute'} bottom={20} right={0}> <Text style={{ lineHeight: 1 }} pos={'absolute'} bottom={20} right={0}>
/ {selectedDay.temperatureMin?.toFixed(0)}°<sup>F</sup> / {selectedDay?.temperatureMin?.toFixed(0)}°<sup>F</sup>
</Text> </Text>
<Box <Box
style={{ style={{
@@ -131,7 +128,7 @@ export default function ExtendedForecastDetails({
</Group> </Group>
<Group <Group
justify={ justify={
WeatherCodes[Number(selectedDay.weatherCode)].length > 25 WeatherCodes[Number(selectedDay?.weatherCode)]?.length > 25
? 'flex-start' ? 'flex-start'
: 'center' : 'center'
} }
@@ -146,21 +143,21 @@ export default function ExtendedForecastDetails({
> >
<Text <Text
className={ className={
WeatherCodes[Number(selectedDay.weatherCode)].length > 25 WeatherCodes[Number(selectedDay?.weatherCode)]?.length > 25
? textClasses.autoScrollText ? textClasses.autoScrollText
: undefined : undefined
} }
classNames={textClasses} classNames={textClasses}
style={{ lineHeight: 1 }} style={{ lineHeight: 1 }}
> >
{selectedDay.temperature_2m?.toFixed(0) === '69' {selectedDay?.temperature_2m?.toFixed(0) === '69'
? 'Nice' ? 'Nice'
: WeatherCodes[Number(selectedDay.weatherCode)]} : WeatherCodes[Number(selectedDay?.weatherCode)]}
</Text> </Text>
</Group> </Group>
</Stack> </Stack>
<Stack> <Stack>
<WeatherIcon size={'xl'} code={selectedDay.weatherCode} /> <WeatherIcon size={'xl'} code={selectedDay?.weatherCode} />
</Stack> </Stack>
</Group> </Group>
@@ -174,7 +171,7 @@ export default function ExtendedForecastDetails({
<Text>Wind</Text> <Text>Wind</Text>
</Group> </Group>
} }
rightSide={<Text>{selectedDay.windSpeed?.toFixed(0)}mph</Text>} rightSide={<Text>{selectedDay?.windSpeed?.toFixed(0)}mph</Text>}
highlight='right' highlight='right'
/> />
<WeatherFeaturePill <WeatherFeaturePill
@@ -184,7 +181,7 @@ export default function ExtendedForecastDetails({
<Text>Gusts</Text> <Text>Gusts</Text>
</Group> </Group>
} }
rightSide={<Text>{selectedDay.windGusts?.toFixed(0)}mph</Text>} rightSide={<Text>{selectedDay?.windGusts?.toFixed(0)}mph</Text>}
highlight='right' highlight='right'
/> />
</Stack> </Stack>
@@ -203,11 +200,11 @@ export default function ExtendedForecastDetails({
<FaLocationArrow <FaLocationArrow
style={{ style={{
color: 'var(--mantine-color-accent-0)', color: 'var(--mantine-color-accent-0)',
transform: `rotate(${-48 + (selectedDay.windDirection || 0)}deg)`, transform: `rotate(${-48 + (selectedDay?.windDirection || 0)}deg)`,
}} }}
/> />
</Box> </Box>
<Text fz={12}>{selectedDay.windDirection?.toFixed(0)}°</Text> <Text fz={12}>{selectedDay?.windDirection?.toFixed(0)}°</Text>
</Stack> </Stack>
</Group> </Group>
<Group justify='space-between'> <Group justify='space-between'>
@@ -229,7 +226,7 @@ export default function ExtendedForecastDetails({
}} }}
/> />
</Box> </Box>
<Text fz={12}>UV {selectedDay.uvIndexMax?.toFixed(0)}</Text> <Text fz={12}>UV {selectedDay?.uvIndexMax?.toFixed(0)}</Text>
</Stack> </Stack>
<Stack gap={4}> <Stack gap={4}>
<WeatherFeaturePill <WeatherFeaturePill
@@ -246,7 +243,7 @@ export default function ExtendedForecastDetails({
/> />
</Group> </Group>
} }
rightSide={<Text>{selectedDay.sunrise}</Text>} rightSide={<Text>{selectedDay?.sunrise}</Text>}
highlight='left' highlight='left'
/> />
<WeatherFeaturePill <WeatherFeaturePill
@@ -263,7 +260,7 @@ export default function ExtendedForecastDetails({
/> />
</Group> </Group>
} }
rightSide={<Text>{selectedDay.sunset}</Text>} rightSide={<Text>{selectedDay?.sunset}</Text>}
highlight='left' highlight='left'
/> />
</Stack> </Stack>
@@ -297,7 +294,7 @@ export default function ExtendedForecastDetails({
<Text>Precipitation %</Text> <Text>Precipitation %</Text>
</Group> </Group>
} }
rightSide={<Text>{selectedDay.precipitationProbability}%</Text>} rightSide={<Text>{selectedDay?.precipitationProbability}%</Text>}
highlight='right' highlight='right'
/> />
<WeatherFeaturePill <WeatherFeaturePill
@@ -307,7 +304,7 @@ export default function ExtendedForecastDetails({
<Text>Precipitation In.</Text> <Text>Precipitation In.</Text>
</Group> </Group>
} }
rightSide={<Text>{selectedDay.precipitationSum.toFixed(2)} in.</Text>} rightSide={<Text>{selectedDay?.precipitationSum.toFixed(2)} in.</Text>}
highlight='right' highlight='right'
/> />
</Stack> </Stack>

View File

@@ -82,7 +82,7 @@ export default function WeatherGraphs({
</Group> </Group>
)} )}
<ResponsiveContainer width={'100%'} height={150}> <ResponsiveContainer width={'100%'} height={150}>
<ComposedChart data={hourlyForecast}> <ComposedChart data={hourlyForecast} responsive>
<XAxis <XAxis
type='category' type='category'
stroke={'var(--mantine-color-primary-0)'} stroke={'var(--mantine-color-primary-0)'}
@@ -129,7 +129,7 @@ export default function WeatherGraphs({
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
<ResponsiveContainer width={'100%'} height={150}> <ResponsiveContainer width={'100%'} height={150}>
<AreaChart data={hourlyForecast}> <AreaChart data={hourlyForecast} responsive>
<defs> <defs>
<linearGradient id={colorPrecipitation} x1='0' y1='0' x2='0' y2='1'> <linearGradient id={colorPrecipitation} x1='0' y1='0' x2='0' y2='1'>
<stop <stop
@@ -160,7 +160,7 @@ export default function WeatherGraphs({
type='category' type='category'
stroke={'var(--mantine-color-primary-0)'} stroke={'var(--mantine-color-primary-0)'}
dataKey={(data) => { dataKey={(data) => {
return new Date(data.startTime).toLocaleTimeString('en-US'); return new Date(data.startTime).toLocaleString('en-US');
}} }}
/> />
<YAxis stroke={'var(--mantine-color-primary-0)'} unit={'%'} domain={[0, 100]} /> <YAxis stroke={'var(--mantine-color-primary-0)'} unit={'%'} domain={[0, 100]} />

View File

@@ -1,4 +1,4 @@
import { Group, Modal, Overlay, Stack, Text } from '@mantine/core'; import { Box, Group, Modal, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { modalClasses } from 'styles'; import { modalClasses } from 'styles';
@@ -19,7 +19,7 @@ export function WeatherModal({
handleResetSearch: () => void; handleResetSearch: () => void;
closeWeatherModal: () => void; closeWeatherModal: () => void;
}) { }) {
const { isMobile, currentUser } = useCommonHooks(); const { isMobile, settings } = useCommonHooks();
const [selectedDay, setSelectedDay] = useState<any>(undefined); const [selectedDay, setSelectedDay] = useState<any>(undefined);
const [selectedHourly, setSelectedHourly] = useState<any>(undefined); const [selectedHourly, setSelectedHourly] = useState<any>(undefined);
@@ -79,49 +79,43 @@ export function WeatherModal({
Weather Forecast for{' '} Weather Forecast for{' '}
</Text> </Text>
<Text fz={'1.125rem'} fw={600}> <Text fz={'1.125rem'} fw={600}>
{currentUser.physicalAddress} {settings.physicalAddress}
</Text> </Text>
</Group> </Group>
</Modal.Title> </Modal.Title>
<Modal.CloseButton /> <Modal.CloseButton />
</Modal.Header> </Modal.Header>
<Modal.Body m={0}> <Modal.Body style={{ overflowX: 'hidden', whiteSpace: 'nowrap' }}>
<Stack w={'100%'} h={'100%'} pos={'relative'}> <Group w={'200%'} gap={0}>
{selectedDay && ( <Box
<Overlay w={'50%'}
h={'100%'} style={{
opacity={1} transition: 'all 0.3s ease-in-out',
blur={10} transform: selectedDay ? 'translateX(-110%)' : 'translateX(0%)',
onClick={() => setSelectedDay(undefined)} }}
style={{ >
display: 'flex', <CurrentWeather current={current} />
justifyContent: 'center', <WeatherGraphs hourly={current?.hourly} />
alignItems: 'center', <ExtendedForecastCard
backgroundColor: 'color-mix(in srgb, black, transparent 50%)', daily={current?.daily}
}} setSelectedDay={setSelectedDay}
> />
<Stack </Box>
w={'65%'} <Box
align={'center'} w={'50%'}
justify={'center'} style={{
style={{ cursor: 'pointer' }} transition: 'all 0.3s ease-in-out',
> transform: selectedDay ? 'translateX(-100%)' : 'translateX(10%)',
<ExtendedForecastDetails }}
selectedDay={selectedDay} >
setSelectedDay={setSelectedDay} <ExtendedForecastDetails
selectedHourly={selectedHourly} selectedDay={selectedDay}
setSelectedHourly={setSelectedHourly} setSelectedDay={setSelectedDay}
/> selectedHourly={selectedHourly}
</Stack> setSelectedHourly={setSelectedHourly}
</Overlay> />
)} </Box>
<CurrentWeather current={current} /> </Group>
<WeatherGraphs hourly={current?.hourly} />
<ExtendedForecastCard
daily={current?.daily}
setSelectedDay={setSelectedDay}
/>
</Stack>
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
</Modal.Root> </Modal.Root>

View File

@@ -306,23 +306,53 @@ export function useMutationAPI() {
body: object | undefined | null; body: object | undefined | null;
}) => callEndpoint(method, endpoint, body), }) => callEndpoint(method, endpoint, body),
onSuccess: (_, { endpoint }) => { onSuccess: (_, { endpoint }) => {
queryClient.refetchQueries({ queryClient
queryKey: ['GET', endpoint], .refetchQueries({
}); queryKey: ['GET', endpoint],
})
.catch((error) => {
console.error(error);
});
endpoint === 'api/bookmarkOrder' && endpoint === 'api/bookmarkOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/bookmarks'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/bookmarks'] })
.catch((error) => {
console.error(error);
});
endpoint === 'api/bookmarkCategoryOrder' && endpoint === 'api/bookmarkCategoryOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/bookmarkCategories'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/bookmarkCategories'] })
.catch((error) => {
console.error(error);
});
endpoint === 'api/applicationOrder' && endpoint === 'api/applicationOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/applications'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/applications'] })
.catch((error) => {
console.error(error);
});
endpoint === 'api/applications' && endpoint === 'api/applications' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/enabledApplications'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/enabledApplications'] })
.catch((error) => {
console.error(error);
});
endpoint === 'api/bookmarks' && endpoint === 'api/bookmarks' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/themes'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/themes'] }).catch((error) => {
console.error(error);
});
endpoint === 'api/providers' && endpoint === 'api/providers' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/providers/default'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/providers/default'] })
.catch((error) => {
console.error(error);
});
endpoint === 'api/personalData' && endpoint === 'api/personalData' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/personalData'] }); queryClient
.refetchQueries({ queryKey: ['GET', 'api/personalData'] })
.catch((error) => {
console.error(error);
});
}, },
}); });
} }
@@ -426,9 +456,21 @@ export function useFileUploadAPI() {
}) => callEndpoint(method, endpoint, body), }) => callEndpoint(method, endpoint, body),
onMutate: () => {}, onMutate: () => {},
onSuccess: (_variables) => { onSuccess: (_variables) => {
queryClient.invalidateQueries({ queryKey: ['GET', 'api/backgrounds'] }); queryClient
queryClient.invalidateQueries({ queryKey: ['GET', 'api/themes'] }); .invalidateQueries({ queryKey: ['GET', 'api/backgrounds'] })
queryClient.invalidateQueries({ queryKey: ['GET', 'api/themePlaylists'] }); .catch((error) => {
console.error(error);
});
queryClient
.invalidateQueries({ queryKey: ['GET', 'api/themes'] })
.catch((error) => {
console.error(error);
});
queryClient
.invalidateQueries({ queryKey: ['GET', 'api/themePlaylists'] })
.catch((error) => {
console.error(error);
});
}, },
}); });
} }

View File

@@ -2,126 +2,80 @@ import { useLocalStorage } from '@mantine/hooks';
import { appTheme } from 'contexts'; import { appTheme } from 'contexts';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { Location, Theme, User } from 'types'; import type { Settings, Theme } from 'types';
import { useMutationAPI } from './useAPI'; import { useMutationAPI } from './useAPI';
const initialSettings: Settings = {
theme: 'random',
appFilter: ['All'],
appSort: 'A>Z',
appDensity: 6,
appScale: 1,
bookmarkSort: 'A>Z',
bookmarkFilter: ['All'],
bookmarkDensity: 6,
bookmarkScale: 1,
currentThemePlaylist: 'All',
defaultProviderLabel: 'Google Search',
defaultProviderURL: 'https://www.google.com/search?q=',
defaultProviderPrefix: '/g',
defaultProviderIcon: 'FaGoogle',
timeFormat: '12h',
firstName: 'First',
lastName: 'Last',
nickname: 'Boss',
email: 'triggerUser@default.com',
physicalAddress: '1510 Polk St, Houston, TX 77002',
defaultFont: 'Jost',
enableWol: 0,
enableGuacamole: 0,
guacamoleHost: 'https://domain.example.com/guacamole',
lat: 29.750950134386066,
lon: -95.36220654860034,
messageSettingsSort: 'Oldest',
appSettingsSort: 'Custom',
bookmarkSettingsSort: 'Custom',
themesSettingsSort: 'Oldest',
providerSettingsSort: 'Oldest',
wolClientsSettingsSort: 'A>Z',
guacamoleClientsSettingsSort: 'A>Z',
};
export function useCommonHooks() { export function useCommonHooks() {
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const isMobile = globalThis.matchMedia('(max-width: 600px)').matches; const isMobile = globalThis.matchMedia('(max-width: 600px)').matches;
const [theme, setTheme] = useAtom<Theme>(appTheme); const [theme, setTheme] = useAtom<Theme>(appTheme);
const [themeName, setThemeName] = useLocalStorage<string>({
key: 'theme', const [settings, setSettings] = useLocalStorage<Settings>({
defaultValue: 'random', key: 'trigger_settings',
}); defaultValue: initialSettings,
const [appFilter, setAppFilter] = useLocalStorage<string[]>({
key: 'appFilter',
defaultValue: ['All'],
});
const [appSort, setAppSort] = useLocalStorage<string>({
key: 'appSort',
defaultValue: 'A>Z',
});
const [appDensity, setAppDensity] = useLocalStorage<number>({
key: 'appDensity',
defaultValue: 6,
});
const [appScale, setAppScale] = useLocalStorage<number>({
key: 'appScale',
defaultValue: 1,
});
const [bookmarkSort, setBookmarkSort] = useLocalStorage<string>({
key: 'bookmarkSort',
defaultValue: 'A>Z',
});
const [bookmarkFilter, setBookmarkFilter] = useLocalStorage<string[]>({
key: 'bookmarkFilter',
defaultValue: ['All'],
});
const [bookmarkDensity, setBookmarkDensity] = useLocalStorage<number>({
key: 'bookmarkDensity',
defaultValue: 6,
});
const [bookmarkScale, setBookmarkScale] = useLocalStorage<number>({
key: 'bookmarkScale',
defaultValue: 1,
});
const [currentTheme, setCurrentTheme] = useLocalStorage<string>({
key: 'theme',
defaultValue: 'random',
});
const [currentThemePlaylist, setCurrentThemePlaylist] = useLocalStorage<string>({
key: 'themePlaylist',
defaultValue: 'All',
});
const [defaultProviderLabel, setDefaultProviderLabel] = useLocalStorage<string>({
key: 'providerLabel',
defaultValue: 'Google Search',
});
const [defaultProviderURL, setDefaultProviderURL] = useLocalStorage<string>({
key: 'providerURL',
defaultValue: 'https://www.google.com/search?q=',
});
const [defaultProviderPrefix, setDefaultProviderPrefix] = useLocalStorage<string>({
key: 'providerPrefix',
defaultValue: '/g',
});
const [defaultProviderIcon, setDefaultProviderIcon] = useLocalStorage<string>({
key: 'providerIcon',
defaultValue: 'FaGoogle',
});
const [timeFormat, setTimeFormat] = useLocalStorage<string>({
key: 'timeFormat',
defaultValue: '12h',
}); });
// Helper to update specific keys without wiping the rest of the object
const updateSetting = <K extends keyof Settings>(key: K, value: Settings[K]) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [providerLabel, setProviderLabel] = useState(defaultProviderLabel); const [providerLabel, setProviderLabel] = useState(settings.defaultProviderLabel);
const [providerURL, setProviderURL] = useState(defaultProviderURL); const [providerURL, setProviderURL] = useState(settings.defaultProviderURL);
const [providerPrefix, setProviderPrefix] = useState(defaultProviderPrefix); const [providerPrefix, setProviderPrefix] = useState(settings.defaultProviderPrefix);
const [providerIcon, setProviderIcon] = useState(defaultProviderIcon); const [providerIcon, setProviderIcon] = useState(settings.defaultProviderIcon);
useEffect(() => { useEffect(() => {
setProviderLabel(defaultProviderLabel); setProviderLabel(settings.defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(settings.defaultProviderURL);
setProviderPrefix(defaultProviderPrefix); setProviderPrefix(settings.defaultProviderPrefix);
setProviderIcon(defaultProviderIcon); setProviderIcon(settings.defaultProviderIcon);
}, [ }, [
defaultProviderLabel, settings.defaultProviderLabel,
defaultProviderURL, settings.defaultProviderURL,
defaultProviderPrefix, settings.defaultProviderPrefix,
defaultProviderIcon, settings.defaultProviderIcon,
]); ]);
const [currentUser, setCurrentUser] = useLocalStorage<User>({ const urlPattern =
key: 'user', /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(#[-a-z\d_]*)?$/i;
defaultValue: {
firstName: 'First',
lastName: 'Last',
nickname: 'Boss',
email: 'triggerUser@default.com',
physicalAddress: '1510 Polk St, Houston, TX 77002',
defaultFont: 'Jost',
enableWol: 0,
enableGuacamole: 0,
guacamoleHost: 'https://domain.example.com/guacamole',
},
});
const [currentLocation, setCurrentLocation] = useLocalStorage<Location>({
key: 'location',
defaultValue: {
lat: 29.750950134386066,
lon: -95.36220654860034,
},
});
const urlPattern = new RegExp(
String.raw`^(https?:\/\/)?` +
String.raw`((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|` +
String.raw`((\d{1,3}\.){3}\d{1,3}))` +
String.raw`(\:\d+)?(\/[-a-z\d%_.~+]*)*` +
String.raw`(\?[;&a-z\d%_.~+=-]*)?` +
String.raw`(\#[-a-z\d_]*)?$`,
'i',
);
const textShadowStyle = const textShadowStyle =
'1px 1px 1px color-mix(in srgb, var(--mantine-color-accent-0), transparent 50%)'; '1px 1px 1px color-mix(in srgb, var(--mantine-color-accent-0), transparent 50%)';
@@ -139,22 +93,24 @@ export function useCommonHooks() {
const enabled = themes const enabled = themes
.filter((t: Theme) => t.randomEnabled === 1) .filter((t: Theme) => t.randomEnabled === 1)
.filter((t: Theme) => t.name !== theme.name); .filter((t: Theme) => t.name !== theme.name);
if (enabled.length === 0 && currentThemePlaylist === 'All') { if (enabled.length === 0 && settings.currentThemePlaylist === 'All') {
setTheme(themes[0]); setTheme(themes[0]);
save && setThemeName(themes[0].name); if (save) updateSetting('theme', themes[0].name);
} else if (currentThemePlaylist === 'All') { } else if (settings.currentThemePlaylist === 'All') {
const index = Math.floor(Math.random() * enabled.length); const index = Math.floor(Math.random() * enabled.length);
setTheme(enabled[index]); setTheme(enabled[index]);
save && setThemeName(enabled[index].name); if (save) updateSetting('theme', enabled[index].name);
} else { } else {
const index = Math.floor(Math.random() * themes.length); const index = Math.floor(Math.random() * themes.length);
setTheme(themes[index]); setTheme(themes[index]);
save && setThemeName(themes[index].name); if (save) updateSetting('theme', themes[index].name);
} }
}; };
const handleDelete = (item: any, endpoint: string) => { const handleDelete = (item: any, endpoint: string) => {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item }); mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item }).catch((error) => {
console.error(error);
});
}; };
const iconFilter = (selectedTheme?: Theme) => { const iconFilter = (selectedTheme?: Theme) => {
@@ -178,7 +134,6 @@ export function useCommonHooks() {
const getObjectDiff = (original: any, current: any) => { const getObjectDiff = (original: any, current: any) => {
const changes: any = {}; const changes: any = {};
// Check current object's properties
for (const [key, value] of Object.entries(current)) { for (const [key, value] of Object.entries(current)) {
if (!(key in original)) { if (!(key in original)) {
changes[key] = { changes[key] = {
@@ -191,7 +146,6 @@ export function useCommonHooks() {
const originalValue = original[key]; const originalValue = original[key];
const currentValue = value; const currentValue = value;
// Handle different types of comparisons
if ( if (
originalValue !== currentValue && originalValue !== currentValue &&
String(originalValue) !== String(currentValue) && String(originalValue) !== String(currentValue) &&
@@ -204,7 +158,6 @@ export function useCommonHooks() {
} }
} }
// Check for removed properties
for (const key of Object.keys(original)) { for (const key of Object.keys(original)) {
if (!(key in current)) { if (!(key in current)) {
changes[key] = { changes[key] = {
@@ -218,6 +171,8 @@ export function useCommonHooks() {
}; };
return { return {
settings,
updateSetting,
isMobile, isMobile,
theme, theme,
setTheme, setTheme,
@@ -228,28 +183,6 @@ export function useCommonHooks() {
getObjectDiff, getObjectDiff,
handleThemeChange, handleThemeChange,
handleDelete, handleDelete,
themeName,
setThemeName,
appFilter,
setAppFilter,
appSort,
setAppSort,
appDensity,
setAppDensity,
appScale,
setAppScale,
bookmarkSort,
setBookmarkSort,
bookmarkFilter,
setBookmarkFilter,
bookmarkDensity,
setBookmarkDensity,
bookmarkScale,
setBookmarkScale,
currentTheme,
setCurrentTheme,
currentThemePlaylist,
setCurrentThemePlaylist,
searchValue, searchValue,
setSearchValue, setSearchValue,
providerLabel, providerLabel,
@@ -260,20 +193,6 @@ export function useCommonHooks() {
setProviderPrefix, setProviderPrefix,
providerIcon, providerIcon,
setProviderIcon, setProviderIcon,
defaultProviderLabel,
setDefaultProviderLabel,
defaultProviderURL,
setDefaultProviderURL,
defaultProviderPrefix,
setDefaultProviderPrefix,
defaultProviderIcon,
setDefaultProviderIcon,
timeFormat,
setTimeFormat,
currentUser,
setCurrentUser,
currentLocation,
setCurrentLocation,
textShadowStyle, textShadowStyle,
largeTextShadowStyle, largeTextShadowStyle,
}; };

View File

@@ -1,4 +1,4 @@
import { Box, Grid, Stack } from '@mantine/core'; import { Box, Stack } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useOpenMeteoAPI, useOSMAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useOpenMeteoAPI, useOSMAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -15,14 +15,7 @@ import {
} from './components'; } from './components';
export function Homepage() { export function Homepage() {
const { const { isMobile, settings, updateSetting } = useCommonHooks();
appFilter,
bookmarkFilter,
isMobile,
setCurrentUser,
currentLocation,
setCurrentLocation,
} = useCommonHooks();
const dateOptions: Intl.DateTimeFormatOptions = isMobile const dateOptions: Intl.DateTimeFormatOptions = isMobile
? {} ? {}
: { : {
@@ -34,7 +27,7 @@ export function Homepage() {
const date = new Date().toLocaleDateString('en-US', dateOptions); const date = new Date().toLocaleDateString('en-US', dateOptions);
const { data: personalData, isFetched: personalIsFetched } = useQueryAPI({ const { data: personalData } = useQueryAPI({
endpoint: 'api/personalData', endpoint: 'api/personalData',
}); });
const { data: messageData } = useQueryAPI({ const { data: messageData } = useQueryAPI({
@@ -44,8 +37,8 @@ export function Homepage() {
address: personalData?.[0]?.physicalAddress, address: personalData?.[0]?.physicalAddress,
}); });
const { data: meteoWeather } = useOpenMeteoAPI({ const { data: meteoWeather } = useOpenMeteoAPI({
lat: weatherMetadata?.[0].lat ?? currentLocation.lat, lat: weatherMetadata?.[0].lat ?? settings.lat,
lon: weatherMetadata?.[0].lon ?? currentLocation.lon, lon: weatherMetadata?.[0].lon ?? settings.lon,
}); });
const personal: PersonalData = personalData?.[0]; const personal: PersonalData = personalData?.[0];
@@ -59,11 +52,6 @@ export function Homepage() {
const [weatherModalOpened, { open: openWeatherModal, close: closeWeatherModal }] = const [weatherModalOpened, { open: openWeatherModal, close: closeWeatherModal }] =
useDisclosure(false); useDisclosure(false);
useEffect(() => {
if (personalData && personalIsFetched) {
setCurrentUser(personalData[0]);
}
}, [personalData, personalIsFetched]);
useEffect(() => { useEffect(() => {
if (personal) { if (personal) {
setDisplayedGreeting(() => { setDisplayedGreeting(() => {
@@ -87,21 +75,23 @@ export function Homepage() {
useEffect(() => { useEffect(() => {
if (meteoWeather) { if (meteoWeather) {
setMeteoCurrent(meteoWeather); setMeteoCurrent(meteoWeather);
setCurrentLocation({ updateSetting('lat', weatherMetadata?.[0].lat);
lat: weatherMetadata?.[0].lat, updateSetting('lon', weatherMetadata?.[0].lon);
lon: weatherMetadata?.[0].lon,
});
} }
}, [meteoWeather]); }, [meteoWeather]);
return ( return (
<Stack <Stack
gap={0} gap={0}
h={appFilter.length > 0 || bookmarkFilter.length > 0 ? undefined : '100%'} h={
settings.appFilter.length > 0 || settings.bookmarkFilter.length > 0
? undefined
: '100%'
}
className={stackClasses.containerStack} className={stackClasses.containerStack}
justify='center' justify='center'
> >
{appFilter.length > 0 || bookmarkFilter.length > 0 ? ( {settings.appFilter.length > 0 || settings.bookmarkFilter.length > 0 ? (
<> <>
<Search <Search
current={meteoCurrent} current={meteoCurrent}
@@ -120,16 +110,16 @@ export function Homepage() {
current={meteoCurrent} current={meteoCurrent}
openWeatherModal={openWeatherModal} openWeatherModal={openWeatherModal}
/> />
<Applications contentType={appFilter} /> <Applications contentType={settings.appFilter} />
<Bookmarks contentType={bookmarkFilter} /> <Bookmarks contentType={settings.bookmarkFilter} />
</> </>
) : ( ) : (
<Box h={'100%'} w={'100%'}> <Box h={'100%'} w={'100%'}>
<Stack align='center'> <Stack align='center'>
<LargeHeader greet={displayedGreeting} date={date} /> <LargeHeader greet={displayedGreeting} date={date} />
<Grid grow columns={24} pos={'absolute'} bottom={'1rem'}> <Box w={'860px'} pos={'absolute'} bottom={'1rem'}>
{meteoCurrent && <CurrentWeather current={meteoCurrent} />} {meteoCurrent && <CurrentWeather current={meteoCurrent} />}
</Grid> </Box>
</Stack> </Stack>
<Search <Search
current={meteoCurrent} current={meteoCurrent}

View File

@@ -8,10 +8,10 @@ export function Applications({ contentType }: { contentType: string[] }) {
const { data: applicationData } = useQueryAPI({ const { data: applicationData } = useQueryAPI({
endpoint: 'api/enabledApplications', endpoint: 'api/enabledApplications',
}); });
const { appSort, appDensity, appScale, theme, textShadowStyle } = useCommonHooks(); const { settings, theme, textShadowStyle } = useCommonHooks();
const applications: Application[] = applicationData const applications: Application[] = applicationData
?.sort((a: Application, b: Application) => { ?.sort((a: Application, b: Application) => {
switch (appSort) { switch (settings.appSort) {
case 'A>Z': case 'A>Z':
return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1; return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1;
case 'Z>A': case 'Z>A':
@@ -54,7 +54,7 @@ export function Applications({ contentType }: { contentType: string[] }) {
style={{ style={{
gap: '.25rem', gap: '.25rem',
display: 'grid', display: 'grid',
gridTemplateColumns: `repeat(${appDensity}, 1fr)`, gridTemplateColumns: `repeat(${settings.appDensity}, 1fr)`,
}} }}
> >
{applications?.map((application: Application) => { {applications?.map((application: Application) => {
@@ -65,7 +65,7 @@ export function Applications({ contentType }: { contentType: string[] }) {
url={application.url} url={application.url}
icon={application.icon} icon={application.icon}
enabled={application.enabled} enabled={application.enabled}
scale={appScale} scale={settings.appScale}
linksActive linksActive
/> />
); );

View File

@@ -11,15 +11,14 @@ export function Bookmarks({ contentType }: { contentType: string[] }) {
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({ const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: 'api/bookmarkCategories', endpoint: 'api/bookmarkCategories',
}); });
const { bookmarkSort, bookmarkDensity, bookmarkScale, theme, textShadowStyle } = const { settings, theme, textShadowStyle } = useCommonHooks();
useCommonHooks();
const groupList: BookmarkGroup[] = groupData?.map((category: BookmarkGroup) => { const groupList: BookmarkGroup[] = groupData?.map((category: BookmarkGroup) => {
return category; return category;
}); });
const bookmarks: Bookmark[] = bookmarkData || []; const bookmarks: Bookmark[] = bookmarkData || [];
const groups: BookmarkGroup[] = groupList const groups: BookmarkGroup[] = groupList
?.toSorted((a: BookmarkGroup, b: BookmarkGroup) => { ?.toSorted((a: BookmarkGroup, b: BookmarkGroup) => {
switch (bookmarkSort) { switch (settings.bookmarkSort) {
case 'A>Z': case 'A>Z':
return a.categoryName.toLocaleLowerCase() > b.categoryName.toLocaleLowerCase() return a.categoryName.toLocaleLowerCase() > b.categoryName.toLocaleLowerCase()
? 1 ? 1
@@ -71,7 +70,7 @@ export function Bookmarks({ contentType }: { contentType: string[] }) {
style={{ style={{
display: 'grid', display: 'grid',
gridRowGap: '0px', gridRowGap: '0px',
gridTemplateColumns: `repeat(${bookmarkDensity}, 1fr)`, gridTemplateColumns: `repeat(${settings.bookmarkDensity}, 1fr)`,
}} }}
> >
{groups {groups
@@ -88,7 +87,7 @@ export function Bookmarks({ contentType }: { contentType: string[] }) {
(bookmark: Bookmark) => (bookmark: Bookmark) =>
bookmark.category === category.categoryName, bookmark.category === category.categoryName,
)} )}
bookmarkScale={bookmarkScale} bookmarkScale={settings.bookmarkScale}
/> />
); );
})} })}

View File

@@ -18,13 +18,12 @@ export function Header({
const [time, setTime] = useState(new Date()); const [time, setTime] = useState(new Date());
const { const {
isMobile, isMobile,
currentUser,
fixDigits, fixDigits,
theme, theme,
textShadowStyle, textShadowStyle,
largeTextShadowStyle, largeTextShadowStyle,
timeFormat, settings,
setTimeFormat, updateSetting,
} = useCommonHooks(); } = useCommonHooks();
useEffect(() => { useEffect(() => {
@@ -51,12 +50,12 @@ export function Header({
}} }}
> >
{greet {greet
?.replace('$firstName', currentUser.firstName) ?.replace('$firstName', settings.firstName)
.replace('$lastName', currentUser.lastName) .replace('$lastName', settings.lastName)
.replace('$nickname', currentUser.nickname) .replace('$nickname', settings.nickname)
.replace('$^firstName', currentUser.firstName.toUpperCase()) .replace('$^firstName', settings.firstName.toUpperCase())
.replace('$^lastName', currentUser.lastName.toUpperCase()) .replace('$^lastName', settings.lastName.toUpperCase())
.replace('$^nickname', currentUser.nickname?.toUpperCase())} .replace('$^nickname', settings.nickname?.toUpperCase())}
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
@@ -123,10 +122,12 @@ export function Header({
textShadow: theme.textShadowsEnabled ? textShadowStyle : undefined, textShadow: theme.textShadowsEnabled ? textShadowStyle : undefined,
}} }}
onClick={() => onClick={() =>
timeFormat === '12h' ? setTimeFormat('24h') : setTimeFormat('12h') settings.timeFormat === '12h'
? updateSetting('timeFormat', '24h')
: updateSetting('timeFormat', '12h')
} }
> >
{timeFormat === '12h' {settings.timeFormat === '12h'
? `${ ? `${
time.getHours() === 0 time.getHours() === 0
? 12 ? 12

View File

@@ -13,13 +13,12 @@ export function LargeHeader({
const [time, setTime] = useState(new Date()); const [time, setTime] = useState(new Date());
const { const {
isMobile, isMobile,
currentUser,
fixDigits, fixDigits,
theme, theme,
textShadowStyle, textShadowStyle,
largeTextShadowStyle, largeTextShadowStyle,
timeFormat, settings,
setTimeFormat, updateSetting,
} = useCommonHooks(); } = useCommonHooks();
useEffect(() => { useEffect(() => {
@@ -40,12 +39,12 @@ export function LargeHeader({
}} }}
> >
{greet {greet
?.replace('$firstName', currentUser.firstName) ?.replace('$firstName', settings.firstName)
.replace('$lastName', currentUser.lastName) .replace('$lastName', settings.lastName)
.replace('$nickname', currentUser.nickname) .replace('$nickname', settings.nickname)
.replace('$^firstName', currentUser.firstName.toUpperCase()) .replace('$^firstName', settings.firstName.toUpperCase())
.replace('$^lastName', currentUser.lastName.toUpperCase()) .replace('$^lastName', settings.lastName.toUpperCase())
.replace('$^nickname', currentUser.nickname?.toUpperCase())} .replace('$^nickname', settings.nickname?.toUpperCase())}
</Text> </Text>
<Text <Text
fz={'2.25rem'} fz={'2.25rem'}
@@ -69,10 +68,12 @@ export function LargeHeader({
zIndex: 1, zIndex: 1,
}} }}
onClick={() => onClick={() =>
timeFormat === '12h' ? setTimeFormat('24h') : setTimeFormat('12h') settings.timeFormat === '12h'
? updateSetting('timeFormat', '24h')
: updateSetting('timeFormat', '12h')
} }
> >
{timeFormat === '12h' {settings.timeFormat === '12h'
? `${ ? `${
time.getHours() === 0 time.getHours() === 0
? 12 ? 12

View File

@@ -63,14 +63,8 @@ export function Search({
setProviderIcon, setProviderIcon,
searchValue, searchValue,
setSearchValue, setSearchValue,
defaultProviderURL, settings,
setDefaultProviderURL, updateSetting,
defaultProviderLabel,
setDefaultProviderLabel,
defaultProviderPrefix,
setDefaultProviderPrefix,
defaultProviderIcon,
setDefaultProviderIcon,
} = useCommonHooks(); } = useCommonHooks();
const { ref, height } = useElementSize(); const { ref, height } = useElementSize();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
@@ -146,8 +140,8 @@ export function Search({
setSearchValue(''); setSearchValue('');
setListIndex(-1); setListIndex(-1);
setHistoryMultiplier(1); setHistoryMultiplier(1);
setProviderLabel(defaultProviderLabel); setProviderLabel(settings.defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(settings.defaultProviderURL);
}; };
const handleGuacamoleClientLaunchItem = ({ option }: { option: any }) => { const handleGuacamoleClientLaunchItem = ({ option }: { option: any }) => {
@@ -176,9 +170,13 @@ export function Search({
method: 'POST', method: 'POST',
endpoint: searchEndpoint, endpoint: searchEndpoint,
body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext }, body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext },
}).then(() => { })
open(`${providerURL}${subtext}`, '_self'); .then(() => {
}); open(`${providerURL}${subtext}`, '_self');
})
.catch((error) => {
console.error(error);
});
} else if (providerLabel === 'Show weather') { } else if (providerLabel === 'Show weather') {
setSearchValue(''); setSearchValue('');
closeDropdown(); closeDropdown();
@@ -211,9 +209,13 @@ export function Search({
method: 'POST', method: 'POST',
endpoint: searchEndpoint, endpoint: searchEndpoint,
body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext }, body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext },
}).then(() => { })
open(`${providerURL}${subtext}`, '_self'); .then(() => {
}); open(`${providerURL}${subtext}`, '_self');
})
.catch((error) => {
console.error(error);
});
} else { } else {
open(`${providerURL}${subtext}`, '_self'); open(`${providerURL}${subtext}`, '_self');
} }
@@ -372,10 +374,10 @@ export function Search({
setProviderIcon(''); setProviderIcon('');
} }
} else { } else {
setProviderLabel(defaultProviderLabel); setProviderLabel(settings.defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(settings.defaultProviderURL);
setProviderPrefix(defaultProviderPrefix); setProviderPrefix(settings.defaultProviderPrefix);
setProviderIcon(defaultProviderIcon); setProviderIcon(settings.defaultProviderIcon);
} }
}; };
@@ -460,12 +462,16 @@ export function Search({
useEffect(() => { useEffect(() => {
if (defaultProviderData && defaultProviderDataIsFetched) { if (defaultProviderData && defaultProviderDataIsFetched) {
setDefaultProviderURL( updateSetting(
'defaultProviderURL',
defaultProviderData[0]?.url ?? 'https://www.google.com/search?q=', defaultProviderData[0]?.url ?? 'https://www.google.com/search?q=',
); );
setDefaultProviderPrefix(defaultProviderData[0]?.prefix ?? '/g'); updateSetting('defaultProviderPrefix', defaultProviderData[0]?.prefix ?? '/g');
setDefaultProviderLabel(defaultProviderData[0]?.name ?? 'Google Search'); updateSetting('defaultProviderIcon', defaultProviderData[0]?.icon ?? 'FaGoogle');
setDefaultProviderIcon(defaultProviderData[0]?.icon ?? 'FaGoogle'); updateSetting(
'defaultProviderLabel',
defaultProviderData[0]?.name ?? 'Google Search',
);
} }
}, [defaultProviderData, defaultProviderDataIsFetched]); }, [defaultProviderData, defaultProviderDataIsFetched]);

View File

@@ -24,11 +24,11 @@ export default function RenderItem({
setHistoryMultiplier: React.Dispatch<React.SetStateAction<number>>; setHistoryMultiplier: React.Dispatch<React.SetStateAction<number>>;
searchEndpoint: string; searchEndpoint: string;
}) { }) {
const { defaultProviderURL } = useCommonHooks(); const { settings } = useCommonHooks();
const handleSearchHistoryItem = ({ option }: { option: SearchHistoryItem }) => { const handleSearchHistoryItem = ({ option }: { option: SearchHistoryItem }) => {
const url = const url =
providers.find((provider: Provider) => provider.prefix === option.prefix)?.url ?? providers.find((provider: Provider) => provider.prefix === option.prefix)?.url ??
defaultProviderURL; settings.defaultProviderURL;
open(`${url}${option.searchTerm}`, '_self'); open(`${url}${option.searchTerm}`, '_self');
}; };

View File

@@ -130,6 +130,8 @@ function SearchSelectItem({
method: 'DELETE', method: 'DELETE',
endpoint: searchEndpoint, endpoint: searchEndpoint,
body: { id: item.option.id }, body: { id: item.option.id },
}).catch((error) => {
console.error(error);
}); });
}} }}
> >

View File

@@ -33,26 +33,7 @@ export function SettingsButton({ openProviderModal }: { openProviderModal: () =>
const [selectedClient, setSelectedClient] = useState<WolClient>(wolClientData?.[0]); const [selectedClient, setSelectedClient] = useState<WolClient>(wolClientData?.[0]);
const navigate = useNavigate(); const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { const { theme, isMobile, settings } = useCommonHooks();
theme,
isMobile,
appFilter,
setAppFilter,
appSort,
setAppSort,
appDensity,
setAppDensity,
appScale,
setAppScale,
bookmarkSort,
setBookmarkSort,
bookmarkFilter,
setBookmarkFilter,
bookmarkDensity,
setBookmarkDensity,
bookmarkScale,
setBookmarkScale,
} = useCommonHooks();
const handleWakeRequest = () => { const handleWakeRequest = () => {
mutateAsync({ mutateAsync({
@@ -123,26 +104,18 @@ export function SettingsButton({ openProviderModal }: { openProviderModal: () =>
<SettingsCard <SettingsCard
categories={categories} categories={categories}
sectionTitle={'Applications'} sectionTitle={'Applications'}
filter={appFilter} filter={settings.appFilter}
setFilter={setAppFilter} sort={settings.appSort}
sort={appSort} density={settings.appDensity}
setSort={setAppSort} scale={settings.appScale}
density={appDensity}
setDensity={setAppDensity}
scale={appScale}
setScale={setAppScale}
/> />
<SettingsCard <SettingsCard
categories={categories} categories={categories}
sectionTitle={'Bookmarks'} sectionTitle={'Bookmarks'}
filter={bookmarkFilter} filter={settings.bookmarkFilter}
setFilter={setBookmarkFilter} sort={settings.bookmarkSort}
sort={bookmarkSort} density={settings.bookmarkDensity}
setSort={setBookmarkSort} scale={settings.bookmarkScale}
density={bookmarkDensity}
setDensity={setBookmarkDensity}
scale={bookmarkScale}
setScale={setBookmarkScale}
/> />
</Stack> </Stack>
</Group> </Group>

View File

@@ -14,26 +14,18 @@ function SettingsCard({
categories, categories,
sectionTitle, sectionTitle,
filter, filter,
setFilter,
sort, sort,
setSort,
density, density,
setDensity,
scale, scale,
setScale,
}: { }: {
categories: ComboboxItem[]; categories: ComboboxItem[];
sectionTitle: string; sectionTitle: string;
filter: any; filter: any;
setFilter: any;
sort: any; sort: any;
setSort: any;
density: any; density: any;
setDensity: any;
scale: any; scale: any;
setScale: any;
}) { }) {
const { theme } = useCommonHooks(); const { theme, updateSetting } = useCommonHooks();
return ( return (
<Stack <Stack
gap={0} gap={0}
@@ -70,11 +62,23 @@ function SettingsCard({
className={sliderClasses.default} className={sliderClasses.default}
classNames={sliderClasses} classNames={sliderClasses}
value={density} value={density}
onChange={(value) => setDensity(value)} onChange={(value) => {
if (sectionTitle === 'Applications') {
updateSetting('appDensity', value);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkDensity', value);
}
}}
min={1} min={1}
max={16} max={16}
marks={[{ value: 6, label: 'Default' }]} marks={[{ value: 6, label: 'Default' }]}
onDoubleClick={() => setDensity(6)} onDoubleClick={() => {
if (sectionTitle === 'Applications') {
updateSetting('appDensity', 6);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkDensity', 6);
}
}}
/> />
</Stack> </Stack>
{scale !== undefined && ( {scale !== undefined && (
@@ -86,12 +90,24 @@ function SettingsCard({
className={sliderClasses.default} className={sliderClasses.default}
classNames={sliderClasses} classNames={sliderClasses}
value={scale} value={scale}
onChange={(value) => setScale(value)} onChange={(value) => {
if (sectionTitle === 'Applications') {
updateSetting('appScale', value);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkScale', value);
}
}}
step={0.1} step={0.1}
min={0.2} min={0.2}
max={3} max={3}
marks={[{ value: 1, label: 'Default' }]} marks={[{ value: 1, label: 'Default' }]}
onDoubleClick={() => setScale(1)} onDoubleClick={() => {
if (sectionTitle === 'Applications') {
updateSetting('appScale', 1);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkScale', 1);
}
}}
/> />
</Stack> </Stack>
)} )}
@@ -113,7 +129,11 @@ function SettingsCard({
} }
defaultValue={filter} defaultValue={filter}
onChange={(data: string[]) => { onChange={(data: string[]) => {
setFilter(data); if (sectionTitle === 'Applications') {
updateSetting('appFilter', data);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkFilter', data);
}
}} }}
classNames={multiSelectClasses} classNames={multiSelectClasses}
comboboxProps={{ comboboxProps={{
@@ -147,7 +167,11 @@ function SettingsCard({
position: 'top', position: 'top',
}} }}
onChange={(_value, option: ComboboxItem) => { onChange={(_value, option: ComboboxItem) => {
setSort(option.value); if (sectionTitle === 'Applications') {
updateSetting('appSort', option.value);
} else if (sectionTitle === 'Bookmarks') {
updateSetting('bookmarkSort', option.value);
}
}} }}
labelProps={{ top: 8 }} labelProps={{ top: 8 }}
styles={ styles={

View File

@@ -31,16 +31,8 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
}); });
const allThemes: Theme[] = allThemeData || []; const allThemes: Theme[] = allThemeData || [];
const { const { isMobile, handleThemeChange, theme, settings, updateSetting } =
isMobile, useCommonHooks();
currentTheme,
setCurrentTheme,
setThemeName,
handleThemeChange,
theme,
currentThemePlaylist,
setCurrentThemePlaylist,
} = useCommonHooks();
const [current, setCurrent] = useState<string>(theme.name); const [current, setCurrent] = useState<string>(theme.name);
@@ -128,11 +120,11 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
borderRadius: '50%', borderRadius: '50%',
}, },
}} }}
disabled={currentThemePlaylist === 'Time of Day' || isAnimating} disabled={settings.currentThemePlaylist === 'Time of Day' || isAnimating}
onClick={() => { onClick={() => {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylistData?.find( themePlaylistData?.find(
(t: ThemePlaylist) => t?.name === currentThemePlaylist, (t: ThemePlaylist) => t?.name === settings.currentThemePlaylist,
)?.themeList ?? '[]', )?.themeList ?? '[]',
); );
if (themeList.length > 0) { if (themeList.length > 0) {
@@ -140,16 +132,16 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
themeList.includes(t.id), themeList.includes(t.id),
); );
handleThemeChange(filteredThemes, false); handleThemeChange(filteredThemes, false);
} else if (currentThemePlaylist === 'All') { } else if (settings.currentThemePlaylist === 'All') {
handleThemeChange(allThemes, true); handleThemeChange(allThemes, true);
} else if (currentThemePlaylist === 'Backgrounds') { } else if (settings.currentThemePlaylist === 'Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter( allThemes.filter(
(a: any) => !a.backgroundImage.toLowerCase().includes('none'), (a: any) => !a.backgroundImage.toLowerCase().includes('none'),
), ),
false, false,
); );
} else if (currentThemePlaylist === 'No Backgrounds') { } else if (settings.currentThemePlaylist === 'No Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter((a: any) => allThemes.filter((a: any) =>
a.backgroundImage.toLowerCase().includes('none'), a.backgroundImage.toLowerCase().includes('none'),
@@ -169,7 +161,7 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
placeholder='Pick a playlist...' placeholder='Pick a playlist...'
mb={'.25rem'} mb={'.25rem'}
ml={'.25rem'} ml={'.25rem'}
value={currentThemePlaylist} value={settings.currentThemePlaylist}
data={[ data={[
{ value: 'All', label: 'All' }, { value: 'All', label: 'All' },
{ {
@@ -226,20 +218,21 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
: undefined : undefined
} }
onChange={(_value, option: ComboboxItem) => { onChange={(_value, option: ComboboxItem) => {
setCurrentThemePlaylist(option.value); updateSetting('currentThemePlaylist', option.value);
if (option.value === 'Time of Day') { if (option.value === 'Time of Day') {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylistData.find((t: ThemePlaylist) => t.name === 'Time of Day') themePlaylistData.find((t: ThemePlaylist) => t.name === 'Time of Day')
.themeList, .themeList,
); );
setThemeName( updateSetting(
'theme',
allThemeData.find( allThemeData.find(
(t: Theme) => (t: Theme) =>
t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]), t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]),
).name, ).name,
); );
} else { } else {
setThemeName('random'); updateSetting('theme', 'random');
} }
}} }}
/> />
@@ -252,12 +245,12 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
<ActionIcon <ActionIcon
className={actionIconClasses.menuActions} className={actionIconClasses.menuActions}
classNames={actionIconClasses} classNames={actionIconClasses}
disabled={currentThemePlaylist === 'Time of Day' || isAnimating} disabled={settings.currentThemePlaylist === 'Time of Day' || isAnimating}
h={40} h={40}
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
color: color:
currentTheme === 'random' settings.theme === 'random'
? 'var(--mantine-color-accent-0)' ? 'var(--mantine-color-accent-0)'
: 'var(--mantine-color-primary-0)', : 'var(--mantine-color-primary-0)',
borderTopRightRadius: 0, borderTopRightRadius: 0,
@@ -271,7 +264,7 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
styles={{ styles={{
icon: { icon: {
backgroundColor: backgroundColor:
currentTheme === 'random' settings.theme === 'random'
? `${theme?.searchColor === theme?.primaryColor ? theme.searchColor?.substring(0, 7) : theme.primaryColor?.substring(0, 7)}88` ? `${theme?.searchColor === theme?.primaryColor ? theme.searchColor?.substring(0, 7) : theme.primaryColor?.substring(0, 7)}88`
: 'transparent', : 'transparent',
width: 'fit-content', width: 'fit-content',
@@ -284,8 +277,8 @@ function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
}, },
}} }}
onClick={() => { onClick={() => {
setCurrentTheme(currentTheme === 'random' ? theme.name : 'random'); updateSetting('theme', 'random');
setCurrentThemePlaylist('All'); updateSetting('currentThemePlaylist', 'All');
}} }}
> >
<FiShuffle size={20} /> <FiShuffle size={20} />

View File

@@ -28,6 +28,7 @@ import DebouncedApplication from './DebouncedApplication';
export function Applications() { export function Applications() {
const allIcons: any = { ...fa6, ...vsc, ...si }; const allIcons: any = { ...fa6, ...vsc, ...si };
const { isMobile, reorder, theme, settings, updateSetting } = useCommonHooks();
const defaultApplication: Application = { const defaultApplication: Application = {
id: null, id: null,
name: '', name: '',
@@ -38,6 +39,7 @@ export function Applications() {
index: 0, index: 0,
enabled: 1, enabled: 1,
}; };
const [dragging, setDragging] = useState(false);
const [searchValue, setSearchValue] = useState<string | number>(''); const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const form = useForm({ const form = useForm({
@@ -54,19 +56,40 @@ export function Applications() {
const { data: applicationData, isFetching: applicationDataIsFetching } = useQueryAPI({ const { data: applicationData, isFetching: applicationDataIsFetching } = useQueryAPI({
endpoint: appsEndpoint, endpoint: appsEndpoint,
}); });
const applications: Application[] = const applications: Application[] = applicationData
applicationData ?.filter(
?.filter( (a: Application) =>
(a: Application) => a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.baseUrl.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
a.baseUrl.toLowerCase().includes((debouncedSearch as string).toLowerCase()), )
) .sort((a: Application, b: Application) => {
.sort((a: Application, b: Application) => (a.index > b.index ? 1 : -1)) || []; if (!dragging) {
switch (settings.appSettingsSort) {
case 'A>Z':
return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1;
case 'Z>A':
return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? 1 : -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
case 'Custom':
return a.index > b.index ? 1 : -1;
default:
return 0;
}
}
return a.index > b.index ? 1 : -1;
});
const [applicationList, setApplicationList] = useState<Application[]>([]); const [applicationList, setApplicationList] = useState<Application[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [dragging, setDragging] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -108,13 +131,15 @@ export function Applications() {
method: 'PATCH', method: 'PATCH',
endpoint: appsOrderEndpoint, endpoint: appsOrderEndpoint,
body: applicationList, body: applicationList,
}).catch((error) => {
console.error(error);
}); });
} }
}, [applicationList]); }, [applicationList]);
return ( return (
<Stack> <Stack>
<SettingsHeader <SettingsHeader
setDragging={applications.length > 0 ? setDragging : undefined} setDragging={applications?.length > 0 ? setDragging : undefined}
openModal={openModal} openModal={openModal}
title={'Applications'} title={'Applications'}
tooltip={{ tooltip={{
@@ -140,6 +165,11 @@ export function Applications() {
searchPlaceholder={'Search Applications...'} searchPlaceholder={'Search Applications...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.appSettingsSort}
setSortBy={(value) => {
updateSetting('appSettingsSort', value);
}}
customSearchEnabled
/> />
{modalOpened && ( {modalOpened && (
<AddItemModal <AddItemModal
@@ -149,12 +179,12 @@ export function Applications() {
body={ body={
<form <form
onSubmit={form.onSubmit((values) => { onSubmit={form.onSubmit((values) => {
mutateAsync({ void mutateAsync({
method: 'POST', method: 'POST',
endpoint: appsEndpoint, endpoint: appsEndpoint,
body: { body: {
...values, ...values,
index: applications.length, index: applications?.length,
}, },
}); });
handleClose(); handleClose();
@@ -318,7 +348,7 @@ export function Applications() {
size: 'xl', size: 'xl',
}} }}
/> />
{applications.length > 0 ? ( {applications?.length > 0 ? (
<Droppable droppableId={'groupList'}> <Droppable droppableId={'groupList'}>
{(provided) => ( {(provided) => (
<Grid <Grid

View File

@@ -38,7 +38,13 @@ function DebouncedApplication({
}; };
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length > 0) { if (debounced && Object.keys(debounced).length > 0) {
mutateAsync({ method: 'PATCH', endpoint: 'api/applications', body: debounced }); mutateAsync({
method: 'PATCH',
endpoint: 'api/applications',
body: debounced,
}).catch((error) => {
console.error(error);
});
} }
}, [debounced]); }, [debounced]);

View File

@@ -19,6 +19,7 @@ import type { Bookmark, BookmarkGroup } from 'types';
import DebouncedGroupSettingsItem from './DebouncedGroupSettingsItem'; import DebouncedGroupSettingsItem from './DebouncedGroupSettingsItem';
export function Bookmarks() { export function Bookmarks() {
const { isMobile, reorder, theme, settings, updateSetting } = useCommonHooks();
const defaultGroup = { const defaultGroup = {
id: null, id: null,
categoryName: '', categoryName: '',
@@ -32,6 +33,7 @@ export function Bookmarks() {
filterName: isNotEmpty('Filter name is required'), filterName: isNotEmpty('Filter name is required'),
}, },
}); });
const [dragging, setDragging] = useState(false);
const bookmarksEndpoint = 'api/bookmarks'; const bookmarksEndpoint = 'api/bookmarks';
const { data: bookmarkData, isFetching: bookmarkDataIsFetching } = useQueryAPI({ const { data: bookmarkData, isFetching: bookmarkDataIsFetching } = useQueryAPI({
endpoint: bookmarksEndpoint, endpoint: bookmarksEndpoint,
@@ -41,10 +43,8 @@ export function Bookmarks() {
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({ const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: categoriesEndpoint, endpoint: categoriesEndpoint,
}); });
const [dragging, setDragging] = useState(false);
const [groups, setGroups] = useState<BookmarkGroup[]>([]); const [groups, setGroups] = useState<BookmarkGroup[]>([]);
const [groupList, setGroupList] = useState<BookmarkGroup[]>([]); const [groupList, setGroupList] = useState<BookmarkGroup[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState<string | number>(''); const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500); const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500);
@@ -71,6 +71,7 @@ export function Bookmarks() {
const foundGroups: BookmarkGroup[] = groupData?.filter((a: BookmarkGroup) => const foundGroups: BookmarkGroup[] = groupData?.filter((a: BookmarkGroup) =>
a.categoryName.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()), a.categoryName.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()),
); );
const handleClose = () => { const handleClose = () => {
closeModal(); closeModal();
form.reset(); form.reset();
@@ -99,15 +100,50 @@ export function Bookmarks() {
useEffect(() => { useEffect(() => {
if (groupData && groupsIsFetched) { if (groupData && groupsIsFetched) {
const groupList = [...new Set([...foundBookmarkGroups, ...foundGroups])].sort( const groupList = [...new Set([...foundBookmarkGroups, ...foundGroups])].sort(
(a: BookmarkGroup, b: BookmarkGroup) => (a: BookmarkGroup, b: BookmarkGroup) => {
a.categoryIndex > b.categoryIndex ? 1 : -1, if (!dragging) {
switch (settings.bookmarkSettingsSort) {
case 'A>Z':
return a.categoryName.toLocaleLowerCase() >
b.categoryName.toLocaleLowerCase()
? 1
: -1;
case 'Z>A':
return a.categoryName.toLocaleLowerCase() <
b.categoryName.toLocaleLowerCase()
? 1
: -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
case 'Custom':
return a.categoryIndex > b.categoryIndex ? 1 : -1;
default:
return 0;
}
}
return a.categoryIndex > b.categoryIndex ? 1 : -1;
},
); );
setGroups(groupList); setGroups(groupList);
} }
if (debouncedGroupSearch === searchValue) { if (debouncedGroupSearch === searchValue) {
setIsLoading(false); setIsLoading(false);
} }
}, [groupData, groupsIsFetched, debouncedGroupSearch]); }, [
groupData,
groupsIsFetched,
debouncedGroupSearch,
settings.bookmarkSettingsSort,
dragging,
]);
useEffect(() => { useEffect(() => {
if (groupList.length > 0 && groupList !== groups) { if (groupList.length > 0 && groupList !== groups) {
@@ -115,6 +151,8 @@ export function Bookmarks() {
method: 'PATCH', method: 'PATCH',
endpoint: 'api/bookmarkCategoryOrder', endpoint: 'api/bookmarkCategoryOrder',
body: groupList, body: groupList,
}).catch((error) => {
console.error(error);
}); });
} }
}, [groupList]); }, [groupList]);
@@ -136,6 +174,8 @@ export function Bookmarks() {
...values, ...values,
categoryIndex: groups.length, categoryIndex: groups.length,
}, },
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
})} })}
@@ -221,6 +261,11 @@ export function Bookmarks() {
searchPlaceholder={'Search Bookmarks...'} searchPlaceholder={'Search Bookmarks...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.bookmarkSettingsSort}
setSortBy={(value) => {
updateSetting('bookmarkSettingsSort', value);
}}
customSearchEnabled
/> />
{dragging ? ( {dragging ? (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>

View File

@@ -33,7 +33,11 @@ function DebouncedBookmarkItem({
}; };
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length) { if (debounced && Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/bookmarks', body: debounced }); mutateAsync({ method: 'PATCH', endpoint: 'api/bookmarks', body: debounced }).catch(
(error) => {
console.error(error);
},
);
} }
}, [debounced]); }, [debounced]);

View File

@@ -94,6 +94,8 @@ function DebouncedGroupSettingsItem({
method: 'PATCH', method: 'PATCH',
endpoint: 'api/bookmarkCategories', endpoint: 'api/bookmarkCategories',
body: debounced, body: debounced,
}).catch((error) => {
console.error(error);
}); });
setUpdate(undefined); setUpdate(undefined);
} }
@@ -107,7 +109,13 @@ function DebouncedGroupSettingsItem({
useEffect(() => { useEffect(() => {
if (bookmarkList && bookmarkList !== bookmarks) { if (bookmarkList && bookmarkList !== bookmarks) {
mutateAsync({ method: 'PATCH', endpoint: 'api/bookmarkOrder', body: bookmarkList }); mutateAsync({
method: 'PATCH',
endpoint: 'api/bookmarkOrder',
body: bookmarkList,
}).catch((error) => {
console.error(error);
});
} }
}, [bookmarkList]); }, [bookmarkList]);
@@ -351,6 +359,8 @@ function DebouncedGroupSettingsItem({
method: 'POST', method: 'POST',
endpoint: 'api/bookmarks', endpoint: 'api/bookmarks',
body: { ...values, index: bookmarks.length }, body: { ...values, index: bookmarks.length },
}).catch((error) => {
console.error(error);
}); });
setPopoverOpened(false); setPopoverOpened(false);
form.reset(); form.reset();

View File

@@ -47,7 +47,13 @@ function DebouncedGuacamoleClient({
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length) { if (debounced && Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/guacamoleClients', body: debounced }); mutateAsync({
method: 'PATCH',
endpoint: 'api/guacamoleClients',
body: debounced,
}).catch((error) => {
console.error(error);
});
} }
}, [debounced]); }, [debounced]);
@@ -92,6 +98,7 @@ function DebouncedGuacamoleClient({
onClick={() => { onClick={() => {
openModal(); openModal();
}} }}
style={{ zIndex: 250 }}
> >
<FiTrash color='black' /> <FiTrash color='black' />
</ActionIcon> </ActionIcon>

View File

@@ -23,6 +23,7 @@ import DebouncedGuacamoleClient from './DebouncedGuacamoleClient';
import GuacamoleClientSettingsItem from './GuacamoleClientSettingsItem'; import GuacamoleClientSettingsItem from './GuacamoleClientSettingsItem';
export function GuacamoleClients() { export function GuacamoleClients() {
const { isMobile, theme, settings, updateSetting } = useCommonHooks();
const defaultGuacamoleClient: GuacamoleClient = { const defaultGuacamoleClient: GuacamoleClient = {
id: undefined, id: undefined,
clientName: '', clientName: '',
@@ -46,15 +47,41 @@ export function GuacamoleClients() {
endpoint: endpoint, endpoint: endpoint,
}); });
const guacamoleClients: GuacamoleClient[] = const guacamoleClients: GuacamoleClient[] =
guacamoleClientsData.filter( guacamoleClientsData
(a: GuacamoleClient) => ?.filter(
a.clientName.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || (a: GuacamoleClient) =>
a.clientIdentifier a.clientName
.toLowerCase() .toLowerCase()
.includes((debouncedSearch as string).toLowerCase()), .includes((debouncedSearch as string).toLowerCase()) ||
) || []; a.clientIdentifier
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()),
)
?.sort((a: GuacamoleClient, b: GuacamoleClient) => {
switch (settings.guacamoleClientsSettingsSort) {
case 'A>Z':
return a.clientName.toLocaleLowerCase() > b.clientName.toLocaleLowerCase()
? 1
: -1;
case 'Z>A':
return a.clientName.toLocaleLowerCase() < b.clientName.toLocaleLowerCase()
? 1
: -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
}) || [];
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const { isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -84,6 +111,8 @@ export function GuacamoleClients() {
method: 'POST', method: 'POST',
endpoint: endpoint, endpoint: endpoint,
body: { ...values, enabled: 1 }, body: { ...values, enabled: 1 },
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
})} })}
@@ -210,6 +239,11 @@ export function GuacamoleClients() {
searchPlaceholder={'Search Clients...'} searchPlaceholder={'Search Clients...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.guacamoleClientsSettingsSort}
setSortBy={(value) => {
updateSetting('guacamoleClientsSettingsSort', value);
}}
customSearchEnabled={false}
/> />
<Stack <Stack
mt={isMobile ? '10rem' : '6rem'} mt={isMobile ? '10rem' : '6rem'}

View File

@@ -17,6 +17,7 @@ import type { Message } from 'types';
import DebouncedMessage from './DebouncedMessage'; import DebouncedMessage from './DebouncedMessage';
export function Messages() { export function Messages() {
const { isMobile, theme, settings, updateSetting } = useCommonHooks();
const endpoint = 'api/messages'; const endpoint = 'api/messages';
const { data: messageData, isFetching: messageDataIsFetching } = useQueryAPI({ const { data: messageData, isFetching: messageDataIsFetching } = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
@@ -31,12 +32,32 @@ export function Messages() {
}); });
const [searchValue, setSearchValue] = useState<string | number>(''); const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const { isMobile, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const messages: Message[] = const messages: Message[] =
messageData?.filter((m: Message) => messageData
m.message.toLowerCase().includes((debouncedSearch as string).toLowerCase()), ?.filter((m: Message) =>
) || []; m.message.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Message, b: Message) => {
switch (settings.messageSettingsSort) {
case 'A>Z':
return a.message.toLocaleLowerCase() > b.message.toLocaleLowerCase() ? 1 : -1;
case 'Z>A':
return a.message.toLocaleLowerCase() < b.message.toLocaleLowerCase() ? 1 : -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
}) || [];
const [message, setMessage] = useState({}); const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000); const [debounced] = useDebouncedValue(message, 1000);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
@@ -49,7 +70,11 @@ export function Messages() {
useEffect(() => { useEffect(() => {
if (Object.keys(debounced).length) { if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced }); mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced }).catch(
(error) => {
console.error(error);
},
);
} }
}, [debounced]); }, [debounced]);
@@ -82,6 +107,11 @@ export function Messages() {
searchPlaceholder={'Search Messages...'} searchPlaceholder={'Search Messages...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.messageSettingsSort}
setSortBy={(value) => {
updateSetting('messageSettingsSort', value);
}}
customSearchEnabled={false}
/> />
{modalOpened && ( {modalOpened && (
<AddItemModal <AddItemModal
@@ -95,6 +125,8 @@ export function Messages() {
method: 'POST', method: 'POST',
endpoint: endpoint, endpoint: endpoint,
body: { message: values.message }, body: { message: values.message },
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
})} })}

View File

@@ -37,7 +37,11 @@ export function Personalization() {
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length) { if (debounced && Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced }); mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced }).catch(
(error) => {
console.error(error);
},
);
setUpdate(undefined); setUpdate(undefined);
} }
}, [debounced]); }, [debounced]);
@@ -87,6 +91,9 @@ export function Personalization() {
searchPlaceholder={undefined} searchPlaceholder={undefined}
isLoading={false} isLoading={false}
setIsLoading={() => {}} setIsLoading={() => {}}
sortBy={''}
setSortBy={undefined}
customSearchEnabled={false}
/> />
<Stack align='center' mt={'6rem'} pos={'relative'}> <Stack align='center' mt={'6rem'} pos={'relative'}>
<LoadingOverlay <LoadingOverlay

View File

@@ -40,7 +40,11 @@ function DebouncedSearchProvider({
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length) { if (debounced && Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/providers', body: debounced }); mutateAsync({ method: 'PATCH', endpoint: 'api/providers', body: debounced }).catch(
(error) => {
console.error(error);
},
);
} }
}, [debounced]); }, [debounced]);

View File

@@ -86,6 +86,9 @@ export function SearchHistory() {
searchPlaceholder={'Search your Search History...'} searchPlaceholder={'Search your Search History...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={''}
setSortBy={undefined}
customSearchEnabled={false}
/> />
<Stack <Stack
mt={isMobile ? '10rem' : '6rem'} mt={isMobile ? '10rem' : '6rem'}

View File

@@ -36,6 +36,7 @@ import DebouncedSearchProvider from './DebouncedSearchProvider';
import SearchProviderSettingsItem from './SearchProviderSettingsItem'; import SearchProviderSettingsItem from './SearchProviderSettingsItem';
export function SearchProviders() { export function SearchProviders() {
const { isMobile, theme, iconFilter, settings, updateSetting } = useCommonHooks();
const allIcons: any = { ...fa6, ...vsc, ...si }; const allIcons: any = { ...fa6, ...vsc, ...si };
const defaultProvider: Provider = { const defaultProvider: Provider = {
id: undefined, id: undefined,
@@ -62,14 +63,32 @@ export function SearchProviders() {
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const providers: Provider[] = const providers: Provider[] =
providerData providerData
.filter( ?.filter(
(a: Provider) => (a: Provider) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.prefix.toLowerCase().includes((debouncedSearch as string).toLowerCase()), a.prefix.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
) )
.sort((a: Provider, b: Provider) => (a.prefix > b.prefix ? 1 : -1)) ?? []; ?.sort((a: Provider, b: Provider) => {
const { isMobile, theme, iconFilter } = useCommonHooks(); switch (settings.providerSettingsSort) {
case 'A>Z':
return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1;
case 'Z>A':
return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? 1 : -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
}) ?? [];
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -100,6 +119,8 @@ export function SearchProviders() {
method: 'POST', method: 'POST',
endpoint: endpoint, endpoint: endpoint,
body: values, body: values,
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
})} })}
@@ -281,6 +302,11 @@ export function SearchProviders() {
searchPlaceholder={'Search Providers...'} searchPlaceholder={'Search Providers...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.providerSettingsSort}
setSortBy={(value) => {
updateSetting('providerSettingsSort', value);
}}
customSearchEnabled={false}
/> />
<Stack <Stack
mt={isMobile ? '10rem' : '6rem'} mt={isMobile ? '10rem' : '6rem'}

View File

@@ -59,6 +59,8 @@ export function DeleteBackground({
method: 'DELETE', method: 'DELETE',
endpoint: 'api/backgrounds', endpoint: 'api/backgrounds',
body: { fileName }, body: { fileName },
}).catch((error) => {
console.error(error);
}); });
}} }}
> >

View File

@@ -72,6 +72,8 @@ export function EditBackground({
fileName.length, fileName.length,
)}`, )}`,
}, },
}).catch((error) => {
console.error(error);
}); });
} else { } else {
mutateAsync({ mutateAsync({
@@ -81,6 +83,8 @@ export function EditBackground({
fileName, fileName,
newFileName: rename, newFileName: rename,
}, },
}).catch((error) => {
console.error(error);
}); });
} }
handleEditBackgroundModalClose(); handleEditBackgroundModalClose();

View File

@@ -41,7 +41,7 @@ export function ThemePlaylists() {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [selectedPlaylist, setSelectedPlaylist] = const [selectedPlaylist, setSelectedPlaylist] =
useState<ThemePlaylist>(defaultPlaylist); useState<ThemePlaylist>(defaultPlaylist);
const { currentThemePlaylist, theme, isMobile } = useCommonHooks(); const { settings, theme, isMobile } = useCommonHooks();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleOpen = useCallback( const handleOpen = useCallback(
@@ -62,6 +62,8 @@ export function ThemePlaylists() {
method: selectedPlaylist?.id === undefined ? 'POST' : 'PATCH', method: selectedPlaylist?.id === undefined ? 'POST' : 'PATCH',
endpoint: themePlaylistsEndpoint, endpoint: themePlaylistsEndpoint,
body: selectedPlaylist ?? {}, body: selectedPlaylist ?? {},
}).catch((error) => {
console.error(error);
}); });
close(); close();
}, [selectedPlaylist, mutateAsync, themePlaylistsEndpoint, close]); }, [selectedPlaylist, mutateAsync, themePlaylistsEndpoint, close]);
@@ -162,8 +164,11 @@ export function ThemePlaylists() {
searchPlaceholder={'Search Theme Playlists...'} searchPlaceholder={'Search Theme Playlists...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={''}
setSortBy={undefined}
customSearchEnabled={false}
/> />
<Stack mt={isMobile ? '10rem' : '6rem'}> <Stack mt={isMobile ? '10rem' : '6rem'} gap={'2rem'}>
<LoadingOverlay <LoadingOverlay
visible={themePlaylistDataIsFetching} visible={themePlaylistDataIsFetching}
zIndex={180} zIndex={180}
@@ -186,7 +191,7 @@ export function ThemePlaylists() {
</Text> </Text>
<Group> <Group>
<Group pos={'relative'}> <Group pos={'relative'}>
{'All' === currentThemePlaylist && ( {'All' === settings.currentThemePlaylist && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -200,16 +205,14 @@ export function ThemePlaylists() {
}} }}
/> />
)} )}
<Box> <ThemePlaylistStack
<ThemePlaylistStack cardPlaylist={allStack}
cardPlaylist={allStack} handleOpen={handleOpen}
handleOpen={handleOpen} allThemes={allThemeData}
allThemes={allThemeData} />
/>
</Box>
</Group> </Group>
<Group pos={'relative'}> <Group pos={'relative'}>
{'Backgrounds' === currentThemePlaylist && ( {'Backgrounds' === settings.currentThemePlaylist && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -226,16 +229,14 @@ export function ThemePlaylists() {
}} }}
/> />
)} )}
<Box> <ThemePlaylistStack
<ThemePlaylistStack cardPlaylist={backgroundsStack}
cardPlaylist={backgroundsStack} handleOpen={handleOpen}
handleOpen={handleOpen} allThemes={allThemeData}
allThemes={allThemeData} />
/>
</Box>
</Group> </Group>
<Group pos={'relative'}> <Group pos={'relative'}>
{'No Backgrounds' === currentThemePlaylist && ( {'No Backgrounds' === settings.currentThemePlaylist && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -252,16 +253,14 @@ export function ThemePlaylists() {
}} }}
/> />
)} )}
<Box> <ThemePlaylistStack
<ThemePlaylistStack cardPlaylist={noBackgroundsStack}
cardPlaylist={noBackgroundsStack} handleOpen={handleOpen}
handleOpen={handleOpen} allThemes={allThemeData}
allThemes={allThemeData} />
/>
</Box>
</Group> </Group>
<Group pos={'relative'}> <Group pos={'relative'}>
{'Time of Day' === currentThemePlaylist && ( {'Time of Day' === settings.currentThemePlaylist && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -314,7 +313,7 @@ export function ThemePlaylists() {
.map((cardPlaylist: ThemePlaylist) => { .map((cardPlaylist: ThemePlaylist) => {
return ( return (
<Group key={cardPlaylist.name} pos={'relative'}> <Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && ( {cardPlaylist.name === settings.currentThemePlaylist && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',

View File

@@ -18,7 +18,7 @@ export function ThemePlaylistCard({
allThemes: Theme[]; allThemes: Theme[];
}) { }) {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { setThemeName, setCurrentThemePlaylist, handleDelete } = useCommonHooks(); const { updateSetting, handleDelete } = useCommonHooks();
const defaultLists = ['All', 'Backgrounds', 'No Backgrounds']; const defaultLists = ['All', 'Backgrounds', 'No Backgrounds'];
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const themeList = JSON.parse( const themeList = JSON.parse(
@@ -117,8 +117,8 @@ export function ThemePlaylistCard({
border: 'solid var(--mantine-color-border-0)', border: 'solid var(--mantine-color-border-0)',
}} }}
onClick={() => { onClick={() => {
setCurrentThemePlaylist(cardPlaylist.name); updateSetting('currentThemePlaylist', cardPlaylist.name);
setThemeName('random'); updateSetting('theme', 'random');
}} }}
disabled={themeList.length === 0} disabled={themeList.length === 0}
> >

View File

@@ -1,14 +1,15 @@
import { Parallax } from '@gfazioli/mantine-parallax'; import { Parallax } from '@gfazioli/mantine-parallax';
import { Reflection } from '@gfazioli/mantine-reflection'; import { Reflection } from '@gfazioli/mantine-reflection';
import { ActionIcon, Box, Group, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Group, Overlay, Popover, Stack, Text } from '@mantine/core';
import { useDebouncedCallback, useDisclosure } from '@mantine/hooks'; import { useDebouncedCallback, useDisclosure, useLongPress } from '@mantine/hooks';
import { DeleteItemModal } from 'components'; import { DeleteItemModal } from 'components';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { FiEdit, FiTrash } from 'react-icons/fi'; import { FiEdit, FiTrash } from 'react-icons/fi';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types'; import type { Theme, ThemePlaylist } from 'types';
import { ThemeCard } from '../../Themes'; import { ThemeCardPreview } from '../../Themes/ThemeCard';
import { ThemeModalContent } from '../../Themes/ThemeModal/components/ThemeModalContent';
export function ThemePlaylistStack({ export function ThemePlaylistStack({
cardPlaylist, cardPlaylist,
@@ -19,16 +20,14 @@ export function ThemePlaylistStack({
handleOpen: (cardPlaylist: ThemePlaylist) => void; handleOpen: (cardPlaylist: ThemePlaylist) => void;
allThemes: Theme[]; allThemes: Theme[];
}) { }) {
const [pressed, setPressed] = useState<boolean>(false);
const handlers = useLongPress(() => {
setPressed(true);
});
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { setThemeName, setCurrentThemePlaylist, handleDelete, currentUser } = const { settings, updateSetting, handleDelete } = useCommonHooks();
useCommonHooks();
const [indexOffset, setIndexOffset] = useState(0); const [indexOffset, setIndexOffset] = useState(0);
const defaultLists = ['All', 'Backgrounds', 'No Backgrounds']; const [movingIndex, setMovingIndex] = useState(0);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const themeList = JSON.parse(
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
);
const missingTheme = { const missingTheme = {
id: undefined, id: undefined,
name: 'default', name: 'default',
@@ -41,8 +40,14 @@ export function ThemePlaylistStack({
randomEnabled: 1, randomEnabled: 1,
textShadowsEnabled: 0, textShadowsEnabled: 0,
appShadowsEnabled: 0, appShadowsEnabled: 0,
themeFont: currentUser.defaultFont, themeFont: settings.defaultFont,
}; };
const [topTheme, setTopTheme] = useState<Theme>(missingTheme);
const defaultLists = ['All', 'Backgrounds', 'No Backgrounds'];
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const themeList = JSON.parse(
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
);
const presentationProps = { const presentationProps = {
initialRotationX: 60, initialRotationX: 60,
@@ -62,223 +67,354 @@ export function ThemePlaylistStack({
} }
}, 75); }, 75);
return ( const sortedItems = useMemo(
<Reflection reflectionStart={75} reflectionEnd={100} shadowOpacity={0.25}> () =>
<Stack (themeList.length > 0 &&
gap={0} themeList.sort((a: number, b: number) => (a > b ? 1 : -1))) ||
w={300} [],
pos={'relative'} [themeList],
onMouseEnter={() => setHover(true)} );
onMouseLeave={() => setHover(false)}
const items = sortedItems?.map((item: number, index: number) => {
let spot = index + indexOffset;
if (spot <= -1) {
spot = index + indexOffset + themeList.length;
} else if (spot >= themeList.length) {
spot = index + indexOffset - themeList.length;
} else {
spot = index + indexOffset;
}
const theme: any = allThemes.find((theme: Theme) => theme.id === item);
return (
<Box
key={theme.id}
style={{ style={{
background: hover transition: 'all 0.3s ease-in-out',
? 'radial-gradient(var(--mantine-color-search-0), transparent)' position: 'absolute',
: undefined, top: 16 * (spot + 1),
cursor: 'pointer', zIndex: themeList.length - spot,
borderRadius: '100%',
}}
onClick={() => {
setCurrentThemePlaylist(cardPlaylist.name);
setThemeName('random');
}} }}
> >
{modalOpened && ( <Parallax {...(!(hover && spot === 0) ? presentationProps : { disabled: true })}>
<DeleteItemModal <ThemeCardPreview key={theme.id} cardTheme={theme} />
modalOpened={modalOpened} </Parallax>
handleClose={closeModal} </Box>
handleDelete={() => handleDelete(cardPlaylist, 'api/themePlaylists')} );
title='Delete Theme Playlist' });
body={
<Stack gap={0} w={'100%'} h={'150px'} align='center'> useEffect(() => {
<Text>Are you sure you want to delete this Theme Playlist?</Text> if (cardPlaylist.name === 'Time of Day') {
<Text setTopTheme(
className={textClasses.deleteItem} allThemes.find(
classNames={textClasses} (t: Theme) => t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]),
>{`"${cardPlaylist.name}"`}</Text> ) || missingTheme,
<Text c='red'>This action cannot be undone!</Text> );
</Stack> } else if (indexOffset > 0 && indexOffset < themeList.length) {
} setMovingIndex(themeList.length - indexOffset);
/> setTopTheme(
)} allThemes.find(
{cardPlaylist?.name === 'Time of Day' ? ( (theme: Theme) => theme.id === themeList[themeList.length - indexOffset],
<Stack ) || missingTheme,
w='100%' );
h='200px' } else if (indexOffset < 0 && Math.abs(indexOffset) < themeList.length) {
justify='center' setMovingIndex(Math.abs(indexOffset));
pos='relative' setTopTheme(
gap={16} allThemes.find((theme: Theme) => theme.id === themeList[Math.abs(indexOffset)]) ||
style={{ overflow: 'hidden' }} missingTheme,
> );
<Parallax } else if (indexOffset === 0) {
{...presentationProps} setMovingIndex(0);
style={{ position: 'absolute', top: -60, zIndex: 3 }} setTopTheme(
allThemes.find((theme: Theme) => theme.id === themeList[0]) || missingTheme,
);
}
}, [indexOffset]);
return (
<Popover
withArrow
opened={pressed}
{...handlers}
arrowSize={16}
transitionProps={{ duration: 200, timingFunction: 'ease' }}
>
<Popover.Target>
<Box>
<Reflection reflectionStart={75} reflectionEnd={100} shadowOpacity={0.25}>
<Stack
gap={0}
w={300}
pos={'relative'}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
background: hover
? 'radial-gradient(var(--mantine-color-search-0), transparent)'
: undefined,
cursor: 'pointer',
borderRadius: '100%',
}}
onClick={() => {
updateSetting('currentThemePlaylist', cardPlaylist.name);
updateSetting('theme', 'random');
}}
> >
<ThemeCard {modalOpened && (
cardTheme={ <DeleteItemModal
allThemes.find((theme: Theme) => theme.id === Number(themeList[0])) || modalOpened={modalOpened}
missingTheme handleClose={closeModal}
} handleDelete={() => handleDelete(cardPlaylist, 'api/themePlaylists')}
allThemes={allThemes} title='Delete Theme Playlist'
themePlaylists={[]} body={
setSelectedTheme={() => {}} <Stack gap={0} w={'100%'} h={'150px'} align='center'>
openThemeModal={() => {}} <Text>Are you sure you want to delete this Theme Playlist?</Text>
enabled={false} <Text
management={false} className={textClasses.deleteItem}
/> classNames={textClasses}
</Parallax> >{`"${cardPlaylist.name}"`}</Text>
<Parallax <Text c='red'>This action cannot be undone!</Text>
{...presentationProps} </Stack>
style={{ position: 'absolute', top: -60, zIndex: 2 }} }
> />
<ThemeCard )}
cardTheme={ <Stack
allThemes.find((theme: Theme) => theme.id === Number(themeList[1])) || w='100%'
missingTheme h='200px'
} justify='start'
allThemes={allThemes} pos='relative'
themePlaylists={[]} gap={0}
setSelectedTheme={() => {}} style={{
openThemeModal={() => {}} overflow: 'hidden',
enabled={false} transition: 'all 0.5s ease-in-out',
management={false} overscrollBehavior: 'contain',
/> }}
</Parallax> onWheel={handleMouseWheel}
<Parallax >
{...presentationProps} {cardPlaylist?.name === 'Time of Day' ? (
style={{ position: 'absolute', top: -60, zIndex: 1 }} <>
> <Parallax
<ThemeCard {...(!(
cardTheme={ hover &&
allThemes.find((theme: Theme) => theme.id === Number(themeList[2])) || Number(themeList[0]) ===
missingTheme Number(themeList[Math.floor(new Date().getHours() / 6)])
} ) && presentationProps)}
allThemes={allThemes} // style={{ position: 'absolute', top: 16 * 1, zIndex: 3 }}
themePlaylists={[]} style={{
setSelectedTheme={() => {}} position: 'absolute',
openThemeModal={() => {}} top: 16,
enabled={false} zIndex: !(
management={false} hover &&
/> Number(themeList[0]) ===
</Parallax> Number(themeList[Math.floor(new Date().getHours() / 6)])
<Parallax )
{...presentationProps} ? 3
style={{ position: 'absolute', top: -60, zIndex: 0 }} : 4,
> }}
<ThemeCard >
cardTheme={ <ThemeCardPreview
allThemes.find((theme: Theme) => theme.id === Number(themeList[3])) || cardTheme={
missingTheme allThemes.find(
} (theme: Theme) => theme.id === Number(themeList[0]),
allThemes={allThemes} ) || missingTheme
themePlaylists={[]} }
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
</Stack>
) : (
<Stack
w='100%'
h='200px'
justify='start'
pos='relative'
gap={0}
style={{
overflow: 'hidden',
transition: 'all 0.5s ease-in-out',
overscrollBehavior: 'contain',
}}
onWheel={handleMouseWheel}
>
{themeList
.sort((a: number, b: number) => (a > b ? 1 : -1))
.map((item: number, index: number) => {
let spot = index + indexOffset;
if (spot <= -1) {
spot = index + indexOffset + themeList.length;
} else if (spot >= themeList.length) {
spot = index + indexOffset - themeList.length;
} else {
spot = index + indexOffset;
}
const theme: any = allThemes.find((theme: Theme) => theme.id === item);
return (
<Box
key={theme.id}
style={{
transition: 'all 0.3s ease-in-out',
position: 'absolute',
top: 16 * (spot + 1),
zIndex: themeList.length - spot,
}}
>
<Parallax {...presentationProps}>
<ThemeCard
key={theme.id}
cardTheme={theme}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/> />
</Parallax> </Parallax>
</Box> <Parallax
); {...(!(
})} hover &&
</Stack> Number(themeList[1]) ===
)} Number(themeList[Math.floor(new Date().getHours() / 6)])
) && presentationProps)}
<Group w={'100%'} pos={'absolute'} justify='space-between' bottom={0}> style={{
<Text position: 'absolute',
style={{ top: !(
paddingRight: '.25rem', hover &&
paddingLeft: '.25rem', Number(themeList[1]) ===
backgroundColor: 'var(--mantine-color-border-0)', Number(themeList[Math.floor(new Date().getHours() / 6)])
borderRadius: '4px', )
color: 'var(--mantine-color-primary-0)', ? 16 * 2
}} : 16,
zIndex: !(
hover &&
Number(themeList[1]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
)
? 2
: 4,
}}
>
<ThemeCardPreview
cardTheme={
allThemes.find(
(theme: Theme) => theme.id === Number(themeList[1]),
) || missingTheme
}
/>
</Parallax>
<Parallax
{...(!(
hover &&
Number(themeList[2]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
) && presentationProps)}
style={{
position: 'absolute',
top: !(
hover &&
Number(themeList[2]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
)
? 16 * 3
: 16,
zIndex: !(
hover &&
Number(themeList[2]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
)
? 1
: 4,
}}
>
<ThemeCardPreview
cardTheme={
allThemes.find(
(theme: Theme) => theme.id === Number(themeList[2]),
) || missingTheme
}
/>
</Parallax>
<Parallax
{...(!(
hover &&
Number(themeList[3]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
) && presentationProps)}
style={{
position: 'absolute',
top: !(
hover &&
Number(themeList[3]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
)
? 16 * 4
: 0,
zIndex: !(
hover &&
Number(themeList[3]) ===
Number(themeList[Math.floor(new Date().getHours() / 6)])
)
? 0
: 4,
}}
>
<ThemeCardPreview
cardTheme={
allThemes.find(
(theme: Theme) => theme.id === Number(themeList[3]),
) || missingTheme
}
/>
</Parallax>
</>
) : (
items.slice(movingIndex, movingIndex + 8)
)}
</Stack>
</Stack>
</Reflection>
<Group
w={'100%'}
pos={'absolute'}
justify='space-between'
bottom={0}
style={{ zIndex: 1000 }}
> >
{cardPlaylist.name} <Group gap={2}>
</Text> <Text
<Group gap={0} style={{ zIndex: 1000 }}>
{!defaultLists.includes(cardPlaylist.name) && (
<ActionIcon
style={{ style={{
paddingRight: '.25rem', paddingRight: '.25rem',
paddingLeft: '.25rem', paddingLeft: '.25rem',
backgroundColor: 'var(--mantine-color-border-0)', backgroundColor: 'var(--mantine-color-border-0)',
borderRadius: '4px', borderRadius: '16px',
color: 'var(--mantine-color-background-0)', color: 'var(--mantine-color-background-0)',
}} }}
onClick={() => {
handleOpen(cardPlaylist);
}}
> >
<FiEdit /> {cardPlaylist.name}
</ActionIcon> </Text>
)} {themeList.length > 0 && (
{cardPlaylist.default === 0 && ( <Text
<ActionIcon style={{
color='red' paddingRight: '.25rem',
style={{ paddingLeft: '.25rem',
paddingRight: '.25rem', backgroundColor: 'var(--mantine-color-background-0)',
paddingLeft: '.25rem', borderRadius: '16px',
borderRadius: '4px', color: 'var(--mantine-color-accent-0)',
}} minWidth: '1.5rem',
onClick={() => { textAlign: 'center',
openModal(); }}
}} >
> {themeList.length}
<FiTrash color='black' /> </Text>
</ActionIcon> )}
)} </Group>
<Group gap={0}>
{!defaultLists.includes(cardPlaylist.name) && (
<ActionIcon
style={{
paddingRight: '.25rem',
paddingLeft: '.25rem',
backgroundColor: 'var(--mantine-color-border-0)',
borderRadius: '4px',
color: 'var(--mantine-color-background-0)',
}}
onClick={() => {
handleOpen(cardPlaylist);
}}
>
<FiEdit />
</ActionIcon>
)}
{cardPlaylist.default === 0 && (
<ActionIcon
color='red'
style={{
paddingRight: '.25rem',
paddingLeft: '.25rem',
borderRadius: '4px',
}}
onClick={() => {
openModal();
}}
>
<FiTrash color='black' />
</ActionIcon>
)}
</Group>
</Group> </Group>
</Group> </Box>
</Stack> </Popover.Target>
</Reflection> <Popover.Dropdown
bg={'var(--mantine-color-primary-0)'}
p={0}
style={{ border: 'none' }}
onClick={() => {
pressed && setPressed(false);
}}
>
<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={topTheme} />
</Popover.Dropdown>
</Popover>
); );
} }

View File

@@ -27,8 +27,7 @@ export function ThemeCard({
}) { }) {
const { hovered, ref: hoverRef } = useHover(); const { hovered, ref: hoverRef } = useHover();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { currentTheme, setCurrentTheme, setCurrentThemePlaylist, handleDelete } = const { settings, updateSetting, handleDelete } = useCommonHooks();
useCommonHooks();
const [hoverPreview, setHoverPreview] = useDebouncedState(false, 1000); const [hoverPreview, setHoverPreview] = useDebouncedState(false, 1000);
const [transition, setTransition] = useDebouncedState(false, 200); const [transition, setTransition] = useDebouncedState(false, 200);
@@ -113,7 +112,7 @@ export function ThemeCard({
<Button <Button
w={'275px'} w={'275px'}
h={'115px'} h={'115px'}
disabled={currentTheme === 'random'} disabled={settings.theme === 'random'}
style={{ style={{
backgroundImage: backgroundImage:
cardTheme?.backgroundImage?.toLocaleLowerCase() === 'none' cardTheme?.backgroundImage?.toLocaleLowerCase() === 'none'
@@ -137,12 +136,12 @@ export function ThemeCard({
}} }}
onClick={() => { onClick={() => {
if (enabled !== false) { if (enabled !== false) {
setCurrentTheme(cardTheme.name); updateSetting('theme', cardTheme.name);
setCurrentThemePlaylist('All'); updateSetting('currentThemePlaylist', 'All');
} }
}} }}
> >
{hovered && enabled && currentTheme !== 'random' && ( {hovered && enabled && settings.theme !== 'random' && (
<Overlay <Overlay
style={{ style={{
opacity: !hoverPreview || transition ? 0.5 : 0, opacity: !hoverPreview || transition ? 0.5 : 0,

View File

@@ -0,0 +1,119 @@
import { Box, Button, Group, Stack, Text } from '@mantine/core';
import type { Theme } from 'types';
import { MiniAppItem } from './components/MiniAppItem';
import { MiniGroupItem } from './components/MiniGroupItem';
export function ThemeCardPreview({ cardTheme }: { cardTheme: Theme }) {
return (
<Group gap={0} align='center' justify='center' display={'grid'} pos={'relative'}>
<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: 275,
}}
>
{cardTheme.name}
</Text>
<Button
w={'275px'}
h={'115px'}
style={{
backgroundImage:
cardTheme?.backgroundImage?.toLocaleLowerCase() === 'none'
? undefined
: `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../trigger/api/backgroundImages/thumbs/${cardTheme?.backgroundImage})`,
backgroundSize:
cardTheme?.backgroundImage?.toLocaleLowerCase() === 'none'
? undefined
: 'cover',
backgroundColor:
cardTheme?.backgroundImage?.toLocaleLowerCase() === 'none'
? cardTheme?.backgroundColor
: undefined,
backgroundPosition: 'center',
border: `solid ${cardTheme.borderColor}`,
transition: 'all .25s ease',
}}
>
<Group w={'100%'} gap={0} display={'flex'} align='center' justify='center'>
<Stack align='center' gap={12}>
<Box
style={{
height: '8px',
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%'} mb={-4}>
<Box
style={{
height: '2px',
width: '40px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'} gap={10} justify='space-between'>
<MiniAppItem cardTheme={cardTheme} />
<MiniAppItem cardTheme={cardTheme} />
<MiniAppItem cardTheme={cardTheme} />
<MiniAppItem cardTheme={cardTheme} />
</Group>
<Group w={'100%'} mb={-4}>
<Box
style={{
height: '2px',
width: '32px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'} gap={10} justify='space-between'>
<MiniGroupItem cardTheme={cardTheme} />
<MiniGroupItem cardTheme={cardTheme} />
<MiniGroupItem cardTheme={cardTheme} />
<MiniGroupItem cardTheme={cardTheme} />
</Group>
</Stack>
</Group>
</Button>
</Group>
);
}

View File

@@ -29,14 +29,13 @@ export function ThemeManagementPill({
transition: boolean; transition: boolean;
setTransition: (transition: boolean) => void; setTransition: (transition: boolean) => void;
}) { }) {
const { currentTheme, currentThemePlaylist, handleThemeChange, theme } = const { settings, handleThemeChange, theme } = useCommonHooks();
useCommonHooks();
const [shiftEdit, setShiftEdit] = useState<number>(0); const [shiftEdit, setShiftEdit] = useState<number>(0);
const [shiftShuffle, setShiftShuffle] = useState<number>(0); const [shiftShuffle, setShiftShuffle] = useState<number>(0);
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
return ( return (
<> <>
{currentTheme === 'random' && cardTheme.name === theme.name ? ( {settings.theme === 'random' && cardTheme.name === theme.name ? (
<Group> <Group>
<Group <Group
onMouseEnter={() => { onMouseEnter={() => {
@@ -81,16 +80,16 @@ export function ThemeManagementPill({
setShiftShuffle(0); setShiftShuffle(0);
}} }}
onClick={() => { onClick={() => {
if (currentThemePlaylist === 'All') { if (settings.currentThemePlaylist === 'All') {
handleThemeChange(allThemes, false); handleThemeChange(allThemes, false);
} else if (currentThemePlaylist === 'Backgrounds') { } else if (settings.currentThemePlaylist === 'Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter( allThemes.filter(
(a: any) => !a.backgroundImage.toLowerCase().includes('none'), (a: any) => !a.backgroundImage.toLowerCase().includes('none'),
), ),
false, false,
); );
} else if (currentThemePlaylist === 'No Backgrounds') { } else if (settings.currentThemePlaylist === 'No Backgrounds') {
handleThemeChange( handleThemeChange(
allThemes.filter((a: any) => allThemes.filter((a: any) =>
a.backgroundImage.toLowerCase().includes('none'), a.backgroundImage.toLowerCase().includes('none'),
@@ -100,7 +99,7 @@ export function ThemeManagementPill({
} else { } else {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylists?.find( themePlaylists?.find(
(t: ThemePlaylist) => t?.name === currentThemePlaylist, (t: ThemePlaylist) => t?.name === settings.currentThemePlaylist,
)?.themeList ?? '[]', )?.themeList ?? '[]',
); );
const filteredThemes = allThemes.filter((t: Theme) => const filteredThemes = allThemes.filter((t: Theme) =>
@@ -203,6 +202,8 @@ export function ThemeManagementPill({
method: 'PATCH', method: 'PATCH',
endpoint: 'api/themes', endpoint: 'api/themes',
body: { ...cardTheme, randomEnabled: !cardTheme.randomEnabled }, body: { ...cardTheme, randomEnabled: !cardTheme.randomEnabled },
}).catch((error) => {
console.error(error);
}); });
}} }}
> >

View File

@@ -1 +1,2 @@
export { ThemeCard } from './ThemeCard'; export { ThemeCard } from './ThemeCard';
export { ThemeCardPreview } from './ThemeCardPreview';

View File

@@ -30,7 +30,7 @@ export function ThemeModal({
themePlaylists: ThemePlaylist[]; themePlaylists: ThemePlaylist[];
}) { }) {
const themesEndpoint = 'api/themes'; const themesEndpoint = 'api/themes';
const { currentUser, isMobile } = useCommonHooks(); const { settings, isMobile } = useCommonHooks();
const [defaultTheme, setDefaultTheme] = useState<Theme>({ ...selectedTheme }); const [defaultTheme, setDefaultTheme] = useState<Theme>({ ...selectedTheme });
const [designerTheme, setDesignerTheme] = useState<Theme>({ const [designerTheme, setDesignerTheme] = useState<Theme>({
...selectedTheme, ...selectedTheme,
@@ -68,7 +68,7 @@ export function ThemeModal({
randomEnabled: 1, randomEnabled: 1,
textShadowsEnabled: 0, textShadowsEnabled: 0,
appShadowsEnabled: 0, appShadowsEnabled: 0,
themeFont: currentUser.defaultFont, themeFont: settings.defaultFont,
}); });
}; };
@@ -77,6 +77,8 @@ export function ThemeModal({
method: designerTheme?.id === undefined ? 'POST' : 'PATCH', method: designerTheme?.id === undefined ? 'POST' : 'PATCH',
endpoint: themesEndpoint, endpoint: themesEndpoint,
body: designerTheme, body: designerTheme,
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
}; };

View File

@@ -25,7 +25,7 @@ export default function ThemeChanges({
designerTheme: Theme; designerTheme: Theme;
setDesignerTheme: (theme: Theme) => void; setDesignerTheme: (theme: Theme) => void;
}) { }) {
const { currentUser, getObjectDiff } = useCommonHooks(); const { settings, getObjectDiff } = useCommonHooks();
return ( return (
<Stack <Stack
gap={0} gap={0}
@@ -148,7 +148,7 @@ export default function ThemeChanges({
}} }}
> >
{defaultTheme[key as keyof Theme] || {defaultTheme[key as keyof Theme] ||
`Default (${currentUser.defaultFont})`} `Default (${settings.defaultFont})`}
</Text> </Text>
<Text>→</Text> <Text>→</Text>
<Text <Text
@@ -158,7 +158,7 @@ export default function ThemeChanges({
}} }}
> >
{designerTheme[key as keyof Theme] || {designerTheme[key as keyof Theme] ||
`Default (${currentUser.defaultFont})`} `Default (${settings.defaultFont})`}
</Text> </Text>
</Group> </Group>
)} )}
@@ -197,9 +197,11 @@ export default function ThemeChanges({
<ColorSwatch <ColorSwatch
h={50} h={50}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard
defaultTheme[key as keyof Theme] as string, .writeText(defaultTheme[key as keyof Theme] as string)
); .catch((error) => {
console.error(error);
});
notifications.show({ notifications.show({
title: ( title: (
<Text fw={600} c={'var(--mantine-color-accent-0)'}> <Text fw={600} c={'var(--mantine-color-accent-0)'}>
@@ -250,9 +252,11 @@ export default function ThemeChanges({
<ColorSwatch <ColorSwatch
h={50} h={50}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard
defaultTheme[key as keyof Theme] as string, .writeText(defaultTheme[key as keyof Theme] as string)
); .catch((error) => {
console.error(error);
});
notifications.show({ notifications.show({
title: ( title: (
<Text fw={600} c={'var(--mantine-color-accent-0)'}> <Text fw={600} c={'var(--mantine-color-accent-0)'}>

View File

@@ -37,7 +37,7 @@ export function ThemeEditor({
duplicate: boolean; duplicate: boolean;
setDuplicate: (duplicate: boolean) => void; setDuplicate: (duplicate: boolean) => void;
}) { }) {
const { currentUser, isMobile } = useCommonHooks(); const { settings, isMobile } = useCommonHooks();
const [colorId, setColorId] = useState<string>(''); const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000'); const [colorValue, setColorValue] = useState('#000000');
const [rgba, setRgba] = useState<{ r: number; g: number; b: number; a?: number }>({ const [rgba, setRgba] = useState<{ r: number; g: number; b: number; a?: number }>({
@@ -80,7 +80,7 @@ export function ThemeEditor({
} else if (font && !customFontEnabled) { } else if (font && !customFontEnabled) {
setDesignerTheme({ setDesignerTheme({
...designerTheme, ...designerTheme,
themeFont: currentUser.defaultFont || 'Jost', themeFont: settings.defaultFont || 'Jost',
}); });
} }
}, [font]); }, [font]);

View File

@@ -5,7 +5,7 @@ import { selectClasses, textClasses } from 'styles';
import type { Theme } from 'types'; import type { Theme } from 'types';
export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) { export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
const { currentUser, textShadowStyle, largeTextShadowStyle } = useCommonHooks(); const { settings, textShadowStyle, largeTextShadowStyle } = useCommonHooks();
return ( return (
<Stack <Stack
style={{ style={{
@@ -31,10 +31,11 @@ export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
'--mantine-color-border-0': selectedTheme?.borderColor, '--mantine-color-border-0': selectedTheme?.borderColor,
'--mantine-color-search-0': selectedTheme?.searchColor, '--mantine-color-search-0': selectedTheme?.searchColor,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
fontFamily: selectedTheme.themeFont ?? currentUser.defaultFont, fontFamily: selectedTheme.themeFont ?? settings.defaultFont,
}} }}
align='center' align='center'
gap={0} gap={0}
miw={600}
> >
<Select <Select
disabled disabled
@@ -46,7 +47,7 @@ export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
color: selectedTheme?.primaryColor, color: selectedTheme?.primaryColor,
borderBottom: `thin solid ${selectedTheme?.borderColor}`, borderBottom: `thin solid ${selectedTheme?.borderColor}`,
backgroundColor: `color-mix(in srgb, ${selectedTheme?.searchColor}, transparent 50%)`, backgroundColor: `color-mix(in srgb, ${selectedTheme?.searchColor}, transparent 50%)`,
fontFamily: selectedTheme.themeFont ?? currentUser.defaultFont, fontFamily: selectedTheme.themeFont ?? settings.defaultFont,
}, },
}} }}
data={[ data={[
@@ -86,60 +87,43 @@ export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
> >
Applications Applications
</Text> </Text>
<Stack <Group
style={{ style={{
gap: '.25rem', display: 'grid',
backgroundSize: 'cover',
borderBottom: '0px solid',
display: 'flex',
gridColumnGap: '5px',
gridRowGap: '5px',
gridTemplateColumns: '1fr 1fr 1fr', gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: '64px', transition: 'all 0.2s ease',
padding: 0,
justifyContent: 'center',
alignContent: 'center',
}} }}
w={'100%'} w={'100%'}
display={'grid'}
> >
<Group <AppItem
style={{ url='https://google.com'
display: 'grid', icon='FaGoogle'
gridTemplateColumns: '1fr 1fr 1fr', name='Google'
transition: 'all 0.2s ease', enabled={1}
}} scale={1}
w={'100%'} linksActive={false}
display={'grid'} selectedTheme={selectedTheme}
> />
<AppItem <AppItem
url='https://google.com' url='https://google.com'
icon='FaGoogle' icon='FaGoogle'
name='Google' name='Google'
enabled={1} enabled={1}
scale={1} scale={1}
linksActive={false} linksActive={false}
selectedTheme={selectedTheme} selectedTheme={selectedTheme}
/> />
<AppItem <AppItem
url='https://google.com' url='https://google.com'
icon='FaGoogle' icon='FaGoogle'
name='Google' name='Google'
enabled={1} enabled={1}
scale={1} scale={1}
linksActive={false} linksActive={false}
selectedTheme={selectedTheme} selectedTheme={selectedTheme}
/> />
<AppItem </Group>
url='https://google.com'
icon='FaGoogle'
name='Google'
enabled={1}
scale={1}
linksActive={false}
selectedTheme={selectedTheme}
/>
</Group>
</Stack>
</Stack> </Stack>
<Stack gap={0} w={'100%'}> <Stack gap={0} w={'100%'}>
<Text <Text

View File

@@ -35,7 +35,7 @@ export default function ThemeOptions({
duplicate: boolean; duplicate: boolean;
randomColor: (withAlpha?: boolean) => string; randomColor: (withAlpha?: boolean) => string;
}) { }) {
const { currentUser } = useCommonHooks(); const { settings } = useCommonHooks();
return ( return (
<Stack h={'100%'} justify='space-between' align='center' gap={4}> <Stack h={'100%'} justify='space-between' align='center' gap={4}>
<Stack w={'100%'} align='center' gap={0} h={'min-content'}> <Stack w={'100%'} align='center' gap={0} h={'min-content'}>
@@ -137,7 +137,7 @@ export default function ThemeOptions({
onClick={() => { onClick={() => {
setCustomFontEnabled(!customFontEnabled); setCustomFontEnabled(!customFontEnabled);
setFont( setFont(
customFontEnabled ? currentUser.defaultFont : defaultTheme.themeFont, customFontEnabled ? settings.defaultFont : defaultTheme.themeFont,
); );
}} }}
style={{ style={{
@@ -163,7 +163,7 @@ export default function ThemeOptions({
autoLoad autoLoad
defaultValue={ defaultValue={
designerTheme.themeFont === null designerTheme.themeFont === null
? currentUser.defaultFont ? settings.defaultFont
: designerTheme.themeFont : designerTheme.themeFont
} }
value={(pickerFont: string) => { value={(pickerFont: string) => {
@@ -183,7 +183,7 @@ export default function ThemeOptions({
color={'red'} color={'red'}
size={32} size={32}
onClick={() => { onClick={() => {
setFont(currentUser.defaultFont); setFont(settings.defaultFont);
setCustomFontEnabled(false); setCustomFontEnabled(false);
}} }}
style={{ style={{

View File

@@ -61,6 +61,9 @@ export function ThemePreviewMiddleware({
bg={'var(--mantine-color-primary-0)'} bg={'var(--mantine-color-primary-0)'}
p={0} p={0}
style={{ border: 'none' }} style={{ border: 'none' }}
onClick={() => {
pressed && setPressed(false);
}}
> >
<Overlay <Overlay
w={'calc(300%)'} w={'calc(300%)'}

View File

@@ -24,15 +24,37 @@ import { ThemeCard } from './ThemeCard';
import { ThemeModal } from './ThemeModal'; import { ThemeModal } from './ThemeModal';
export function Themes() { export function Themes() {
const { settings, updateSetting, theme, isMobile } = useCommonHooks();
const backgroundsEndpoint = 'api/backgrounds'; const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData, isFetching: allThemeDataIsFetching } = useQueryAPI({ const { data: allThemeData, isFetching: allThemeDataIsFetching } = useQueryAPI({
endpoint: 'api/themes', endpoint: 'api/themes',
}); });
const [searchValue, setSearchValue] = useState<string | number>(''); const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const allThemes: Theme[] = allThemeData?.filter((a: Theme) => const allThemes: Theme[] = allThemeData
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()), ?.filter((a: Theme) =>
); a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Theme, b: Theme) => {
switch (settings.themesSettingsSort) {
case 'A>Z':
return a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1;
case 'Z>A':
return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? 1 : -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
});
const { data: backgroundData, isFetching: backgroundDataIsFetching } = useQueryAPI({ const { data: backgroundData, isFetching: backgroundDataIsFetching } = useQueryAPI({
endpoint: backgroundsEndpoint, endpoint: backgroundsEndpoint,
}); });
@@ -62,16 +84,6 @@ export function Themes() {
), ),
] as string[]; ] as string[];
const {
currentTheme,
currentUser,
setCurrentTheme,
theme,
isMobile,
currentThemePlaylist,
setCurrentThemePlaylist,
} = useCommonHooks();
const [selectedTheme, setSelectedTheme] = useState<Theme>({ const [selectedTheme, setSelectedTheme] = useState<Theme>({
id: undefined, id: undefined,
name: 'default', name: 'default',
@@ -84,7 +96,7 @@ export function Themes() {
randomEnabled: 1, randomEnabled: 1,
textShadowsEnabled: 0, textShadowsEnabled: 0,
appShadowsEnabled: 0, appShadowsEnabled: 0,
themeFont: currentUser.defaultFont, themeFont: settings.defaultFont,
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false); const [isNew, setIsNew] = useState(false);
@@ -139,6 +151,11 @@ export function Themes() {
searchPlaceholder={'Search Themes...'} searchPlaceholder={'Search Themes...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.themesSettingsSort}
setSortBy={(value) => {
updateSetting('themesSettingsSort', value);
}}
customSearchEnabled={false}
/> />
<Stack mt={isMobile ? '11rem' : '7rem'} align='center'> <Stack mt={isMobile ? '11rem' : '7rem'} align='center'>
<LoadingOverlay <LoadingOverlay
@@ -159,7 +176,7 @@ export function Themes() {
> >
<Overlay <Overlay
display={ display={
currentTheme === 'random' && debouncedSearch === '' ? 'flex' : 'none' settings.theme === 'random' && debouncedSearch === '' ? 'flex' : 'none'
} }
blur={5} blur={5}
style={{ style={{
@@ -174,7 +191,7 @@ export function Themes() {
{allThemes?.length > 0 ? ( {allThemes?.length > 0 ? (
allThemes.map((cardTheme: Theme) => ( allThemes.map((cardTheme: Theme) => (
<Group key={cardTheme.name} pos={'relative'}> <Group key={cardTheme.name} pos={'relative'}>
{cardTheme.name === currentTheme && ( {cardTheme.name === settings.theme && (
<FiChevronRight <FiChevronRight
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -194,11 +211,12 @@ export function Themes() {
<Box <Box
style={{ style={{
zIndex: zIndex:
currentTheme === 'random' && settings.theme === 'random' &&
(themePlaylists (themePlaylists
.find( .find(
(t: ThemePlaylist) => (t: ThemePlaylist) =>
t.name.toLowerCase() === currentThemePlaylist.toLowerCase(), t.name.toLowerCase() ===
settings.currentThemePlaylist.toLowerCase(),
) )
?.themeList.includes(`${cardTheme.id}`) || ?.themeList.includes(`${cardTheme.id}`) ||
cardTheme.name === theme.name) cardTheme.name === theme.name)
@@ -285,7 +303,9 @@ export function Themes() {
fontSize: isMobile ? '.8rem' : '1.2rem', fontSize: isMobile ? '.8rem' : '1.2rem',
}} }}
> >
{currentThemePlaylist === 'All' ? 'All' : currentThemePlaylist} {settings.currentThemePlaylist === 'All'
? 'All'
: settings.currentThemePlaylist}
</Text> </Text>
</Group> </Group>
<Divider <Divider
@@ -295,11 +315,13 @@ export function Themes() {
/> />
<Button <Button
onClick={() => { onClick={() => {
if (currentThemePlaylist === 'All') { if (settings.currentThemePlaylist === 'All') {
navigate(`/${import.meta.env.VITE_BASE_URL}/settings/themePlaylists`); void navigate(
`/${import.meta.env.VITE_BASE_URL}/settings/themePlaylists`,
);
} else { } else {
setCurrentThemePlaylist('All'); updateSetting('currentThemePlaylist', 'All');
setCurrentTheme(theme.name); updateSetting('theme', theme.name);
} }
}} }}
style={{ style={{
@@ -307,7 +329,7 @@ export function Themes() {
padding: isMobile ? 0 : undefined, padding: isMobile ? 0 : undefined,
}} }}
> >
{currentThemePlaylist === 'All' ? ( {settings.currentThemePlaylist === 'All' ? (
isMobile ? ( isMobile ? (
<FiMousePointer /> <FiMousePointer />
) : ( ) : (
@@ -323,11 +345,11 @@ export function Themes() {
className={toolTipClasses.default} className={toolTipClasses.default}
classNames={toolTipClasses} classNames={toolTipClasses}
label='Shuffle is always on when a playlist is active.' label='Shuffle is always on when a playlist is active.'
disabled={currentThemePlaylist === 'All'} disabled={settings.currentThemePlaylist === 'All'}
> >
<Group> <Group>
<Switch <Switch
disabled={currentThemePlaylist !== 'All'} disabled={settings.currentThemePlaylist !== 'All'}
label={ label={
isMobile ? ( isMobile ? (
<FaDice size={20} color={'var(--mantine-color-primary-0)'} /> <FaDice size={20} color={'var(--mantine-color-primary-0)'} />
@@ -336,10 +358,10 @@ export function Themes() {
) )
} }
labelPosition='left' labelPosition='left'
checked={currentTheme === 'random'} checked={settings.theme === 'random'}
onChange={(e) => { onChange={(e) => {
setCurrentTheme(e.target.checked ? 'random' : theme.name); updateSetting('theme', e.target.checked ? 'random' : theme.name);
setCurrentThemePlaylist('All'); updateSetting('currentThemePlaylist', 'All');
}} }}
className={switchClasses.default} className={switchClasses.default}
classNames={switchClasses} classNames={switchClasses}

View File

@@ -28,7 +28,11 @@ function DebouncedWolClient({
useEffect(() => { useEffect(() => {
if (debounced && Object.keys(debounced).length) { if (debounced && Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/WolClients', body: debounced }); mutateAsync({ method: 'PATCH', endpoint: 'api/WolClients', body: debounced }).catch(
(error) => {
console.error(error);
},
);
} }
}, [debounced]); }, [debounced]);

View File

@@ -19,6 +19,7 @@ import DebouncedWolClient from './DebouncedWolClient';
import WolClientSettingsItem from './WolClientSettingsItem'; import WolClientSettingsItem from './WolClientSettingsItem';
export function WolClients() { export function WolClients() {
const { isMobile, theme, settings, updateSetting } = useCommonHooks();
const defaultWolClient: WolClient = { const defaultWolClient: WolClient = {
id: undefined, id: undefined,
hostname: '', hostname: '',
@@ -41,13 +42,37 @@ export function WolClients() {
const [searchValue, setSearchValue] = useState<string | number>(''); const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const wolClients: WolClient[] = const wolClients: WolClient[] =
wolClientsData.filter( wolClientsData
(a: WolClient) => ?.filter(
a.hostname.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || (a: WolClient) =>
a.macAddress.toLowerCase().includes((debouncedSearch as string).toLowerCase()), a.hostname.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
) || []; a.macAddress.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
?.sort((a: WolClient, b: WolClient) => {
switch (settings.wolClientsSettingsSort) {
case 'A>Z':
return a.hostname.toLocaleLowerCase() > b.hostname.toLocaleLowerCase()
? 1
: -1;
case 'Z>A':
return a.hostname.toLocaleLowerCase() < b.hostname.toLocaleLowerCase()
? 1
: -1;
case 'Newest':
if (a.id && b.id) {
return a.id < b.id ? 1 : -1;
}
return -1;
case 'Oldest':
if (a.id && b.id) {
return a.id > b.id ? 1 : -1;
}
return -1;
default:
return 0;
}
}) || [];
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const { isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -77,6 +102,8 @@ export function WolClients() {
method: 'POST', method: 'POST',
endpoint: endpoint, endpoint: endpoint,
body: values, body: values,
}).catch((error) => {
console.error(error);
}); });
handleClose(); handleClose();
})} })}
@@ -151,6 +178,11 @@ export function WolClients() {
searchPlaceholder={'Search Clients...'} searchPlaceholder={'Search Clients...'}
isLoading={isLoading} isLoading={isLoading}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
sortBy={settings.wolClientsSettingsSort}
setSortBy={(value) => {
updateSetting('wolClientsSettingsSort', value);
}}
customSearchEnabled={false}
/> />
<Stack <Stack
mt={isMobile ? '10rem' : '6rem'} mt={isMobile ? '10rem' : '6rem'}

View File

@@ -7,6 +7,7 @@ export type { Message } from './message';
export type { PersonalData } from './personalData'; export type { PersonalData } from './personalData';
export type { Provider } from './provider'; export type { Provider } from './provider';
export type { SearchHistoryItem, SearchShortcut } from './search'; export type { SearchHistoryItem, SearchShortcut } from './search';
export type { Settings } from './settings';
export type { Theme, ThemePlaylist } from './theme'; export type { Theme, ThemePlaylist } from './theme';
export { ThemeProperties } from './theme'; export { ThemeProperties } from './theme';
export type { User } from './user'; export type { User } from './user';

35
src/types/settings.ts Normal file
View File

@@ -0,0 +1,35 @@
export interface Settings {
theme: string;
appFilter: string[];
appSort: string;
appDensity: number;
appScale: number;
bookmarkSort: string;
bookmarkFilter: string[];
bookmarkDensity: number;
bookmarkScale: number;
currentThemePlaylist: string;
defaultProviderLabel: string;
defaultProviderURL: string;
defaultProviderPrefix: string;
defaultProviderIcon: string;
timeFormat: string;
firstName: string;
lastName: string;
email: string;
physicalAddress: string;
enableWol: number;
enableGuacamole: number;
guacamoleHost: string;
nickname: string;
defaultFont: string;
lat: number;
lon: number;
messageSettingsSort: string;
appSettingsSort: string;
bookmarkSettingsSort: string;
themesSettingsSort: string;
providerSettingsSort: string;
wolClientsSettingsSort: string;
guacamoleClientsSettingsSort: string;
}