Skip to content

Commit 97b7a3a

Browse files
committed
issue 121 implement vacation mode
1 parent e5e477e commit 97b7a3a

6 files changed

Lines changed: 453 additions & 9 deletions

File tree

client/src/app.jsx

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
readLocalInterfaceColors,
2121
readLocalScreensaverSettings,
2222
readLocalAutoDarkModeSettings,
23+
readLocalVacationModeSettings,
2324
} from './utils/interfaceSettings.js';
2425
import { normalizeWidgetSettings, BASE_WIDGET_SETTINGS } from './utils/widgetSettings.js';
2526
import { buildMobileWidgetList } from './utils/mobileWidgets.js';
@@ -32,6 +33,7 @@ const loadWeatherWidget = () => import('./components/WeatherWidget.jsx');
3233
const loadChoreWidget = () => import('./components/ChoreWidget.jsx');
3334
const loadTabIconModal = () => import('./components/TabIconModal.jsx');
3435
const loadScreenSaver = () => import('./components/ScreenSaver.jsx');
36+
const loadVacationScreensaver = () => import('./components/VacationScreensaver.jsx');
3537

3638
const MAX_IDLE_WARM_IMPORTS = 3;
3739
const WIDGETS_LOCKED_STORAGE_KEY = 'widgetsLocked';
@@ -72,6 +74,7 @@ const WeatherWidget = lazy(loadWeatherWidget);
7274
const ChoreWidget = lazy(loadChoreWidget);
7375
const TabIconModal = lazy(loadTabIconModal);
7476
const ScreenSaver = lazy(loadScreenSaver);
77+
const VacationScreensaver = lazy(loadVacationScreensaver);
7578

7679
// region #98 - expected to get removed in the future (localStorage migration bridge)
7780
const DEVICE_SETTINGS_UPDATED_EVENT = 'homeglow:device-settings-updated';
@@ -147,6 +150,7 @@ const App = () => {
147150
const [widgetsLocked, setWidgetsLocked] = useState(readLocalWidgetsLocked);
148151
const [screensaverActive, setScreensaverActive] = useState(false);
149152
const [screensaverSettings, setScreensaverSettings] = useState(readLocalScreensaverSettings);
153+
const [vacationModeSettings, setVacationModeSettings] = useState(readLocalVacationModeSettings);
150154
const inactivityTimerRef = useRef(null);
151155
const lastActivityRef = useRef(Date.now());
152156
const [widgetSettings, setWidgetSettings] = useState({ ...DEFAULT_WIDGET_SETTINGS });
@@ -500,6 +504,7 @@ const App = () => {
500504
setInterfaceColors(readLocalInterfaceColors());
501505
setScreensaverSettings(readLocalScreensaverSettings());
502506
setAutoDarkModeSettings(readLocalAutoDarkModeSettings());
507+
setVacationModeSettings(readLocalVacationModeSettings());
503508

504509
const localTheme = readLocalTheme();
505510
const localThemeMode = readLocalThemeMode(localTheme);
@@ -588,7 +593,7 @@ const App = () => {
588593
!!widgetSettings?.chores?.enabled && loadChoreWidget,
589594
!!widgetSettings?.weather?.enabled && loadWeatherWidget,
590595
!!widgetSettings?.photos?.enabled && loadPhotoWidget,
591-
!!screensaverSettings?.enabled && loadScreenSaver,
596+
!!screensaverSettings?.enabled && (vacationModeSettings?.enabled ? loadVacationScreensaver : loadScreenSaver),
592597
]
593598
.filter(Boolean)
594599
.slice(0, MAX_IDLE_WARM_IMPORTS);
@@ -606,6 +611,7 @@ const App = () => {
606611
widgetSettings?.weather?.enabled,
607612
widgetSettings?.photos?.enabled,
608613
screensaverSettings?.enabled,
614+
vacationModeSettings?.enabled,
609615
]);
610616

611617
const screensaverActiveRef = useRef(false);
@@ -924,7 +930,9 @@ const App = () => {
924930
enabled:
925931
widgetSettings.chores.enabled &&
926932
choreSoundGlobalEnabled &&
927-
choreSoundDeviceEnabled,
933+
choreSoundDeviceEnabled &&
934+
// Vacation mode (issue #121) mutes chore due-time sounds.
935+
!(vacationModeSettings.enabled && vacationModeSettings.muteSounds),
928936
defaultSound: apiKeys.CHORE_SOUND_DEFAULT || null,
929937
volume: Number.isFinite(parsedSoundVolume) ? parsedSoundVolume / 100 : 1,
930938
});
@@ -954,6 +962,32 @@ const App = () => {
954962
Demo Mode — sample data resets every {demoStatus.resetHours || 6} hours
955963
</Box>
956964
)}
965+
{vacationModeSettings.enabled && (
966+
<Box
967+
aria-label="Vacation mode active"
968+
sx={{
969+
position: 'fixed',
970+
top: 8,
971+
right: 8,
972+
zIndex: 1200,
973+
px: 1.5,
974+
py: 0.5,
975+
borderRadius: '16px',
976+
backgroundColor: 'var(--card-bg)',
977+
border: '1px solid var(--card-border)',
978+
color: 'var(--text-color)',
979+
fontSize: '0.8rem',
980+
fontWeight: 600,
981+
boxShadow: 'var(--shadow)',
982+
pointerEvents: 'none',
983+
display: 'flex',
984+
alignItems: 'center',
985+
gap: 0.5,
986+
}}
987+
>
988+
🏖️ Vacation Mode
989+
</Box>
990+
)}
957991
{/* The one mobile/kiosk fork (issue #118): below 600px the grid —
958992
react-grid-layout, drag/resize, lock — never mounts. */}
959993
{isMobile && mobileWidgets.length > 0 && (
@@ -1095,13 +1129,19 @@ const App = () => {
10951129

10961130
{!isMobile && screensaverActive && screensaverSettings.enabled && (
10971131
<Suspense fallback={null}>
1098-
<ScreenSaver
1099-
mode={screensaverSettings.mode}
1100-
slideshowInterval={screensaverSettings.slideshowInterval}
1101-
tabs={tabs}
1102-
onExit={handleExitScreensaver}
1103-
onTabChange={handleScreensaverTabChange}
1104-
/>
1132+
{vacationModeSettings.enabled ? (
1133+
// Vacation mode (issue #121) replaces the standard screensaver
1134+
// with the popcorn vacation-emoji one.
1135+
<VacationScreensaver onExit={handleExitScreensaver} />
1136+
) : (
1137+
<ScreenSaver
1138+
mode={screensaverSettings.mode}
1139+
slideshowInterval={screensaverSettings.slideshowInterval}
1140+
tabs={tabs}
1141+
onExit={handleExitScreensaver}
1142+
onTabChange={handleScreensaverTabChange}
1143+
/>
1144+
)}
11051145
</Suspense>
11061146
)}
11071147
</>

client/src/components/AdminPanel.jsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
Timer,
5858
Lock,
5959
Nightlight,
60+
BeachAccess,
6061
Tab as TabIcon,
6162
DragIndicator,
6263
PhotoLibrary,
@@ -89,12 +90,15 @@ import {
8990
INTERFACE_COLORS_STORAGE_KEY,
9091
SCREENSAVER_SETTINGS_STORAGE_KEY,
9192
AUTO_DARK_MODE_SETTINGS_STORAGE_KEY,
93+
VACATION_MODE_STORAGE_KEY,
9294
DEFAULT_INTERFACE_COLORS,
9395
normalizeInterfaceColors,
9496
normalizeScreensaverSettings,
97+
normalizeVacationModeSettings,
9598
readLocalInterfaceColors,
9699
readLocalScreensaverSettings,
97100
readLocalAutoDarkModeSettings,
101+
readLocalVacationModeSettings,
98102
} from '../utils/interfaceSettings.js';
99103

100104
const USERS_UPDATED_EVENT = 'homeglow:users-updated';
@@ -209,6 +213,7 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
209213
const [pluginDeclaredDirty, setPluginDeclaredDirty] = useState({});
210214
const [photoSources, setPhotoSources] = useState([]);
211215
const [screensaverSettings, setScreensaverSettings] = useState(readLocalScreensaverSettings);
216+
const [vacationModeSettings, setVacationModeSettings] = useState(readLocalVacationModeSettings);
212217
const [autoDarkModeSettings, setAutoDarkModeSettings] = useState(readLocalAutoDarkModeSettings);
213218
const [isSavingAutoDarkMode, setIsSavingAutoDarkMode] = useState(false);
214219
const [autoDarkModeSunTimes, setAutoDarkModeSunTimes] = useState({
@@ -263,6 +268,7 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
263268
if (isAuthenticated) {
264269
setInterfaceColors(readLocalInterfaceColors());
265270
setScreensaverSettings(readLocalScreensaverSettings());
271+
setVacationModeSettings(readLocalVacationModeSettings());
266272
setAutoDarkModeSettings(readLocalAutoDarkModeSettings());
267273
fetchSettings();
268274
fetchDeviceSettings();
@@ -1082,6 +1088,23 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
10821088
}
10831089
};
10841090

1091+
const saveVacationModeSettings = async () => {
1092+
try {
1093+
setIsLoading(true);
1094+
const normalizedVacationMode = normalizeVacationModeSettings(vacationModeSettings);
1095+
localStorage.setItem(VACATION_MODE_STORAGE_KEY, JSON.stringify(normalizedVacationMode));
1096+
window.dispatchEvent(new Event(INTERFACE_SETTINGS_UPDATED_EVENT));
1097+
setSaveMessage({ show: true, type: 'success', text: 'Vacation mode settings saved for this display.' });
1098+
setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000);
1099+
} catch (error) {
1100+
console.error('Error saving vacation mode settings:', error);
1101+
setSaveMessage({ show: true, type: 'error', text: 'Failed to save vacation mode settings. Please try again.' });
1102+
setTimeout(() => setSaveMessage({ show: false, type: '', text: '' }), 3000);
1103+
} finally {
1104+
setIsLoading(false);
1105+
}
1106+
};
1107+
10851108
const resolveAutoDarkModeLocation = async (locationQuery) => {
10861109
const apiKey = settings.WEATHER_API_KEY?.trim();
10871110
if (!apiKey) {
@@ -2660,6 +2683,52 @@ const AdminPanel = ({ setWidgetSettings, onPluginsChanged, onTabsChanged }) => {
26602683

26612684
<Divider sx={{ my: 4 }} />
26622685

2686+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
2687+
<BeachAccess />
2688+
<Typography variant="h6">Vacation Mode</Typography>
2689+
</Box>
2690+
2691+
<Alert severity="info" sx={{ mb: 2 }}>
2692+
While on vacation, chore due-time chimes are muted and the screensaver becomes a
2693+
fun vacation animation. Settings apply to this display and persist until you turn
2694+
vacation mode off.
2695+
</Alert>
2696+
2697+
<FormControlLabel
2698+
control={
2699+
<Switch
2700+
checked={vacationModeSettings.enabled}
2701+
onChange={(e) => setVacationModeSettings(prev => ({ ...prev, enabled: e.target.checked }))}
2702+
/>
2703+
}
2704+
label="Enable Vacation Mode"
2705+
sx={{ mb: 1, display: 'block' }}
2706+
/>
2707+
2708+
<FormControlLabel
2709+
control={
2710+
<Switch
2711+
checked={vacationModeSettings.muteSounds}
2712+
disabled={!vacationModeSettings.enabled}
2713+
onChange={(e) => setVacationModeSettings(prev => ({ ...prev, muteSounds: e.target.checked }))}
2714+
/>
2715+
}
2716+
label="Mute chore sounds while on vacation"
2717+
sx={{ mb: 1, display: 'block' }}
2718+
/>
2719+
2720+
<Button
2721+
variant="contained"
2722+
onClick={saveVacationModeSettings}
2723+
startIcon={<Save />}
2724+
fullWidth
2725+
sx={{ mt: 2 }}
2726+
>
2727+
Save Vacation Mode Settings
2728+
</Button>
2729+
2730+
<Divider sx={{ my: 4 }} />
2731+
26632732
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
26642733
<Nightlight />
26652734
<Typography variant="h6">Daylight Auto Dark Mode</Typography>

0 commit comments

Comments
 (0)