Skip to content

Commit 01950bb

Browse files
committed
refactor: normalizeWidgetSettings to handle null and non-object values. Duplicates down to 5.7%
1 parent ab25370 commit 01950bb

9 files changed

Lines changed: 253 additions & 313 deletions

File tree

client/src/app.jsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
readLocalScreensaverSettings,
1919
readLocalAutoDarkModeSettings,
2020
} from './utils/interfaceSettings.js';
21+
import { normalizeWidgetSettings, BASE_WIDGET_SETTINGS } from './utils/widgetSettings.js';
2122
import './index.css';
2223

2324
const loadAdminPanel = () => import('./components/AdminPanel.jsx');
@@ -77,10 +78,7 @@ const isAllowedDeviceSettingsMigrationKey = (key) => DEVICE_SETTINGS_MIGRATION_K
7778
// endRegion #98
7879

7980
const DEFAULT_WIDGET_SETTINGS = {
80-
chores: { enabled: false },
81-
calendar: { enabled: false },
82-
photos: { enabled: false },
83-
weather: { enabled: false },
81+
...BASE_WIDGET_SETTINGS,
8482
lightGradientStart: '#00ddeb',
8583
lightGradientEnd: '#ff6b6b',
8684
darkGradientStart: '#2e2767',
@@ -91,15 +89,6 @@ const DEFAULT_WIDGET_SETTINGS = {
9189
darkButtonGradientEnd: '#620808',
9290
};
9391

94-
const normalizeWidgetSettings = (raw) => ({
95-
...DEFAULT_WIDGET_SETTINGS,
96-
...(raw && typeof raw === 'object' ? raw : {}),
97-
chores: { ...DEFAULT_WIDGET_SETTINGS.chores, ...(raw?.chores || {}) },
98-
calendar: { ...DEFAULT_WIDGET_SETTINGS.calendar, ...(raw?.calendar || {}) },
99-
photos: { ...DEFAULT_WIDGET_SETTINGS.photos, ...(raw?.photos || {}) },
100-
weather: { ...DEFAULT_WIDGET_SETTINGS.weather, ...(raw?.weather || {}) },
101-
});
102-
10392
const readLocalTheme = () => {
10493
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
10594
return savedTheme === 'dark' ? 'dark' : 'light';

client/src/components/AdminPanel.jsx

Lines changed: 56 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ import TabIconModal from './TabIconModal';
7575
import DeleteConfirmationDialog from './DeleteConfirmationDialog';
7676
import AdminFormSection from './AdminFormSection';
7777
import VersionInfoCard from './VersionInfoCard';
78+
import LoadingBackdrop from './LoadingBackdrop';
79+
import RefreshIntervalSelect from './RefreshIntervalSelect';
80+
import ScreensaverIntervalSlider from './ScreensaverIntervalSlider';
7881
import GoogleAccountConnection from './GoogleAccountConnection';
7982
import ClamValueModal from './ClamValueModal';
8083
import SoundPicker from './SoundPicker';
8184
import useIsMobile from '../hooks/useIsMobile.js';
85+
import { syncWidgetAssignments } from '../utils/assignmentSync.js';
8286
import { stackableTableSx } from '../utils/responsiveTable.js';
8387
import {
8488
INTERFACE_COLORS_STORAGE_KEY,
@@ -627,24 +631,7 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
627631
const currentResponse = await axios.get(`${API_DEVICE_URL}/widget-assignments`);
628632
const currentAssignments = Array.isArray(currentResponse.data) ? currentResponse.data : [];
629633

630-
for (const [widgetName, desiredTabNumbers] of Object.entries(widgetAssignments)) {
631-
const existing = currentAssignments.filter(a => a.widget_name === widgetName);
632-
const existingTabNumbers = existing.map(a => a.tab_number);
633-
634-
const toRemove = existing.filter(a => !desiredTabNumbers.includes(a.tab_number));
635-
const toAdd = desiredTabNumbers.filter(number => !existingTabNumbers.includes(number));
636-
637-
for (const assignment of toRemove) {
638-
await axios.delete(`${API_DEVICE_URL}/widget-assignments/${assignment.id}`);
639-
}
640-
641-
for (const tabNumber of toAdd) {
642-
await axios.post(`${API_DEVICE_URL}/widget-assignments`, {
643-
widget_name: widgetName,
644-
tabNumber: tabNumber,
645-
});
646-
}
647-
}
634+
await syncWidgetAssignments(API_DEVICE_URL, widgetAssignments, currentAssignments);
648635

649636
if (onTabsChanged) {
650637
await onTabsChanged();
@@ -676,24 +663,7 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
676663
const currentResponse = await axios.get(`${API_DEVICE_URL}/widget-assignments`);
677664
const currentAssignments = Array.isArray(currentResponse.data) ? currentResponse.data : [];
678665

679-
for (const [pluginWidgetName, desiredTabNumbers] of Object.entries(pluginAssignments)) {
680-
const existing = currentAssignments.filter(a => a.widget_name === pluginWidgetName);
681-
const existingTabNumbers = existing.map(a => a.tab_number);
682-
683-
const toRemove = existing.filter(a => !desiredTabNumbers.includes(a.tab_number));
684-
const toAdd = desiredTabNumbers.filter(number => !existingTabNumbers.includes(number));
685-
686-
for (const assignment of toRemove) {
687-
await axios.delete(`${API_DEVICE_URL}/widget-assignments/${assignment.id}`);
688-
}
689-
690-
for (const tabNumber of toAdd) {
691-
await axios.post(`${API_DEVICE_URL}/widget-assignments`, {
692-
widget_name: pluginWidgetName,
693-
tabNumber: tabNumber,
694-
});
695-
}
696-
}
666+
await syncWidgetAssignments(API_DEVICE_URL, pluginAssignments, currentAssignments);
697667

698668
if (onTabsChanged) {
699669
await onTabsChanged();
@@ -1806,26 +1776,12 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
18061776
</Grid>
18071777

18081778
<Grid size={{ xs: 12, sm: 6 }}>
1809-
<FormControl fullWidth size="small">
1810-
<InputLabel id={`${widget}-refresh-label`}>
1811-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
1812-
<Timer fontSize="small" />
1813-
Auto-Refresh Interval
1814-
</Box>
1815-
</InputLabel>
1816-
<Select
1817-
labelId={`${widget}-refresh-label`}
1818-
value={config.refreshInterval || 0}
1819-
onChange={(e) => handleRefreshIntervalChange(widget, e.target.value)}
1820-
label="Auto-Refresh Interval"
1821-
>
1822-
{refreshIntervalOptions.map((option) => (
1823-
<MenuItem key={option.value} value={option.value}>
1824-
{option.label}
1825-
</MenuItem>
1826-
))}
1827-
</Select>
1828-
</FormControl>
1779+
<RefreshIntervalSelect
1780+
labelId={`${widget}-refresh-label`}
1781+
value={config.refreshInterval}
1782+
onChange={(value) => handleRefreshIntervalChange(widget, value)}
1783+
options={refreshIntervalOptions}
1784+
/>
18291785
</Grid>
18301786
</Grid>
18311787

@@ -1883,26 +1839,12 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
18831839
</Grid>
18841840

18851841
<Grid size={{ xs: 12, sm: 6 }}>
1886-
<FormControl fullWidth size="small">
1887-
<InputLabel id="weather-refresh-label">
1888-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
1889-
<Timer fontSize="small" />
1890-
Auto-Refresh Interval
1891-
</Box>
1892-
</InputLabel>
1893-
<Select
1894-
labelId="weather-refresh-label"
1895-
value={widgetSettings.weather?.refreshInterval || 0}
1896-
onChange={(e) => handleRefreshIntervalChange('weather', e.target.value)}
1897-
label="Auto-Refresh Interval"
1898-
>
1899-
{refreshIntervalOptions.map((option) => (
1900-
<MenuItem key={option.value} value={option.value}>
1901-
{option.label}
1902-
</MenuItem>
1903-
))}
1904-
</Select>
1905-
</FormControl>
1842+
<RefreshIntervalSelect
1843+
labelId="weather-refresh-label"
1844+
value={widgetSettings.weather?.refreshInterval}
1845+
onChange={(value) => handleRefreshIntervalChange('weather', value)}
1846+
options={refreshIntervalOptions}
1847+
/>
19061848
</Grid>
19071849
</Grid>
19081850

@@ -2088,31 +2030,17 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
20882030
</Grid>
20892031

20902032
<Grid size={{ xs: 12, sm: 6 }}>
2091-
<FormControl fullWidth size="small">
2092-
<InputLabel id={`plugin-${plugin.filename}-refresh-label`}>
2093-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
2094-
<Timer fontSize="small" />
2095-
Auto-Refresh Interval
2096-
</Box>
2097-
</InputLabel>
2098-
<Select
2099-
labelId={`plugin-${plugin.filename}-refresh-label`}
2100-
value={pSettings.refreshInterval || 0}
2101-
onChange={(e) => {
2102-
setPluginSettings(prev => ({
2103-
...prev,
2104-
[plugin.filename]: { ...prev[plugin.filename], refreshInterval: e.target.value }
2105-
}));
2106-
}}
2107-
label="Auto-Refresh Interval"
2108-
>
2109-
{refreshIntervalOptions.map((option) => (
2110-
<MenuItem key={option.value} value={option.value}>
2111-
{option.label}
2112-
</MenuItem>
2113-
))}
2114-
</Select>
2115-
</FormControl>
2033+
<RefreshIntervalSelect
2034+
labelId={`plugin-${plugin.filename}-refresh-label`}
2035+
value={pSettings.refreshInterval}
2036+
onChange={(value) => {
2037+
setPluginSettings(prev => ({
2038+
...prev,
2039+
[plugin.filename]: { ...prev[plugin.filename], refreshInterval: value }
2040+
}));
2041+
}}
2042+
options={refreshIntervalOptions}
2043+
/>
21162044
</Grid>
21172045
</Grid>
21182046

@@ -2524,45 +2452,35 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
25242452
/>
25252453

25262454
{screensaverSettings.mode === 'photos' && (
2527-
<>
2528-
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
2529-
Photo Slideshow Interval: {screensaverSettings.slideshowInterval} second{screensaverSettings.slideshowInterval !== 1 ? 's' : ''}
2530-
</Typography>
2531-
<Slider
2532-
value={screensaverSettings.slideshowInterval}
2533-
onChange={(e, value) => setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))}
2534-
min={3}
2535-
max={60}
2536-
marks={[
2537-
{ value: 3, label: '3s' },
2538-
{ value: 10, label: '10s' },
2539-
{ value: 30, label: '30s' },
2540-
{ value: 60, label: '60s' }
2541-
]}
2542-
sx={{ mb: 4 }}
2543-
/>
2544-
</>
2455+
<ScreensaverIntervalSlider
2456+
label="Photo Slideshow Interval"
2457+
value={screensaverSettings.slideshowInterval}
2458+
onChange={(value) => setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))}
2459+
min={3}
2460+
max={60}
2461+
marks={[
2462+
{ value: 3, label: '3s' },
2463+
{ value: 10, label: '10s' },
2464+
{ value: 30, label: '30s' },
2465+
{ value: 60, label: '60s' }
2466+
]}
2467+
/>
25452468
)}
25462469

25472470
{screensaverSettings.mode === 'tabs' && (
2548-
<>
2549-
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
2550-
Tab Cycle Interval: {screensaverSettings.slideshowInterval} second{screensaverSettings.slideshowInterval !== 1 ? 's' : ''}
2551-
</Typography>
2552-
<Slider
2553-
value={screensaverSettings.slideshowInterval}
2554-
onChange={(e, value) => setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))}
2555-
min={5}
2556-
max={120}
2557-
marks={[
2558-
{ value: 5, label: '5s' },
2559-
{ value: 30, label: '30s' },
2560-
{ value: 60, label: '60s' },
2561-
{ value: 120, label: '2m' }
2562-
]}
2563-
sx={{ mb: 4 }}
2564-
/>
2565-
</>
2471+
<ScreensaverIntervalSlider
2472+
label="Tab Cycle Interval"
2473+
value={screensaverSettings.slideshowInterval}
2474+
onChange={(value) => setScreensaverSettings(prev => ({ ...prev, slideshowInterval: value }))}
2475+
min={5}
2476+
max={120}
2477+
marks={[
2478+
{ value: 5, label: '5s' },
2479+
{ value: 30, label: '30s' },
2480+
{ value: 60, label: '60s' },
2481+
{ value: 120, label: '2m' }
2482+
]}
2483+
/>
25662484
)}
25672485

25682486
<Button
@@ -3480,87 +3398,7 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
34803398
</Dialog>
34813399

34823400
{/* Loading Indicator */}
3483-
<Backdrop
3484-
sx={{
3485-
color: '#fff',
3486-
zIndex: (theme) => theme.zIndex.drawer + 1,
3487-
backdropFilter: 'blur(10px)',
3488-
backgroundColor: 'rgba(0, 0, 0, 0.3)',
3489-
}}
3490-
open={isLoading}
3491-
>
3492-
<Box
3493-
sx={{
3494-
display: 'flex',
3495-
flexDirection: 'column',
3496-
alignItems: 'center',
3497-
gap: 3,
3498-
p: 4,
3499-
borderRadius: 3,
3500-
background: 'rgba(255, 255, 255, 0.1)',
3501-
backdropFilter: 'blur(20px)',
3502-
border: '1px solid rgba(255, 255, 255, 0.2)',
3503-
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
3504-
}}
3505-
>
3506-
<Box
3507-
sx={{
3508-
position: 'relative',
3509-
width: 80,
3510-
height: 80,
3511-
display: 'flex',
3512-
alignItems: 'center',
3513-
justifyContent: 'center',
3514-
}}
3515-
>
3516-
{[0, 1, 2].map((index) => (
3517-
<Box
3518-
key={index}
3519-
sx={{
3520-
position: 'absolute',
3521-
fontSize: '2rem',
3522-
animation: `clamBounce 1.5s ease-in-out ${index * 0.2}s infinite`,
3523-
'@keyframes clamBounce': {
3524-
'0%, 80%, 100%': {
3525-
transform: 'scale(0.8) translateY(0)',
3526-
opacity: 0.6,
3527-
},
3528-
'40%': {
3529-
transform: 'scale(1.2) translateY(-20px)',
3530-
opacity: 1,
3531-
},
3532-
},
3533-
}}
3534-
>
3535-
🥟
3536-
</Box>
3537-
))}
3538-
</Box>
3539-
3540-
<Typography
3541-
variant="h6"
3542-
sx={{
3543-
color: 'white',
3544-
fontWeight: 'bold',
3545-
textAlign: 'center',
3546-
textShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
3547-
}}
3548-
>
3549-
Processing...
3550-
</Typography>
3551-
3552-
<CircularProgress
3553-
size={40}
3554-
thickness={2}
3555-
sx={{
3556-
color: 'rgba(255, 255, 255, 0.7)',
3557-
'& .MuiCircularProgress-circle': {
3558-
strokeLinecap: 'round',
3559-
},
3560-
}}
3561-
/>
3562-
</Box>
3563-
</Backdrop>
3401+
<LoadingBackdrop open={isLoading} />
35643402

35653403
{/* PIN Modal */}
35663404
<PinModal

0 commit comments

Comments
 (0)