Simplify home view and drop motion detection
- Top nav tabs (Home/Media/Cameras/Settings) replace the right-side button cluster - Home view now shows calendar 90% / todo 10% vertically; lights, locks, alarm, thermostats removed from the dashboard since the photo frame now owns the idle space and the nav covers the remaining sections - Motion detection deleted: the go2rtc-based Kitchen_Panel poll was only there to wake the screen before idle timeout, which photo-frame exit on touch replaces
This commit is contained in:
@@ -4,14 +4,12 @@ import * as fs from 'fs';
|
|||||||
import { ScreenManager } from './services/ScreenManager';
|
import { ScreenManager } from './services/ScreenManager';
|
||||||
import { PresenceDetector } from './services/PresenceDetector';
|
import { PresenceDetector } from './services/PresenceDetector';
|
||||||
import { FrigateStreamer } from './services/FrigateStreamer';
|
import { FrigateStreamer } from './services/FrigateStreamer';
|
||||||
import { MotionDetector } from './services/MotionDetector';
|
|
||||||
import { PhotoManager } from './services/PhotoManager';
|
import { PhotoManager } from './services/PhotoManager';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let screenManager: ScreenManager | null = null;
|
let screenManager: ScreenManager | null = null;
|
||||||
let presenceDetector: PresenceDetector | null = null;
|
let presenceDetector: PresenceDetector | null = null;
|
||||||
let frigateStreamer: FrigateStreamer | null = null;
|
let frigateStreamer: FrigateStreamer | null = null;
|
||||||
let motionDetector: MotionDetector | null = null;
|
|
||||||
let photoManager: PhotoManager | null = null;
|
let photoManager: PhotoManager | null = null;
|
||||||
let powerSaveBlockerId: number | null = null;
|
let powerSaveBlockerId: number | null = null;
|
||||||
|
|
||||||
@@ -63,23 +61,6 @@ function createWindow(): void {
|
|||||||
// Initialize services
|
// Initialize services
|
||||||
screenManager = new ScreenManager(mainWindow);
|
screenManager = new ScreenManager(mainWindow);
|
||||||
|
|
||||||
// Initialize motion detector (runs in main process, not throttled by browser)
|
|
||||||
// Uses file size comparison which is more reliable for JPEG streams
|
|
||||||
motionDetector = new MotionDetector({
|
|
||||||
go2rtcUrl: 'http://192.168.1.241:1985',
|
|
||||||
cameraName: 'Kitchen_Panel',
|
|
||||||
sensitivityThreshold: 5, // % file size change to trigger (5% = significant motion)
|
|
||||||
checkIntervalMs: 2000, // Check every 2 seconds for responsiveness
|
|
||||||
});
|
|
||||||
|
|
||||||
motionDetector.on('motion', () => {
|
|
||||||
console.log('MotionDetector: Motion detected, waking screen');
|
|
||||||
screenManager?.wakeScreen();
|
|
||||||
mainWindow?.webContents.send('motion:detected');
|
|
||||||
});
|
|
||||||
|
|
||||||
motionDetector.start();
|
|
||||||
|
|
||||||
// Photo frame slideshow source
|
// Photo frame slideshow source
|
||||||
photoManager = new PhotoManager(resolvePhotosDir());
|
photoManager = new PhotoManager(resolvePhotosDir());
|
||||||
console.log(`PhotoManager: watching ${photoManager.getDir()}`);
|
console.log(`PhotoManager: watching ${photoManager.getDir()}`);
|
||||||
@@ -243,7 +224,6 @@ app.on('window-all-closed', () => {
|
|||||||
}
|
}
|
||||||
presenceDetector?.stop();
|
presenceDetector?.stop();
|
||||||
frigateStreamer?.stop();
|
frigateStreamer?.stop();
|
||||||
motionDetector?.stop();
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ export interface ElectronAPI {
|
|||||||
startStream: (rtspUrl: string) => Promise<boolean>;
|
startStream: (rtspUrl: string) => Promise<boolean>;
|
||||||
stopStream: () => Promise<boolean>;
|
stopStream: () => Promise<boolean>;
|
||||||
};
|
};
|
||||||
motion: {
|
|
||||||
onDetected: (callback: () => void) => () => void;
|
|
||||||
};
|
|
||||||
app: {
|
app: {
|
||||||
quit: () => void;
|
quit: () => void;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
@@ -61,13 +58,6 @@ const electronAPI: ElectronAPI = {
|
|||||||
startStream: (rtspUrl: string) => ipcRenderer.invoke('frigate:startStream', rtspUrl),
|
startStream: (rtspUrl: string) => ipcRenderer.invoke('frigate:startStream', rtspUrl),
|
||||||
stopStream: () => ipcRenderer.invoke('frigate:stopStream'),
|
stopStream: () => ipcRenderer.invoke('frigate:stopStream'),
|
||||||
},
|
},
|
||||||
motion: {
|
|
||||||
onDetected: (callback: () => void) => {
|
|
||||||
const handler = (_event: IpcRendererEvent) => callback();
|
|
||||||
ipcRenderer.on('motion:detected', handler);
|
|
||||||
return () => ipcRenderer.removeListener('motion:detected', handler);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
app: {
|
app: {
|
||||||
quit: () => ipcRenderer.invoke('app:quit'),
|
quit: () => ipcRenderer.invoke('app:quit'),
|
||||||
toggleFullscreen: () => ipcRenderer.invoke('app:toggleFullscreen'),
|
toggleFullscreen: () => ipcRenderer.invoke('app:toggleFullscreen'),
|
||||||
|
|||||||
86
src/App.tsx
86
src/App.tsx
@@ -3,7 +3,6 @@ import { Dashboard } from '@/components/layout';
|
|||||||
import { ThermostatOverlay } from '@/components/climate';
|
import { ThermostatOverlay } from '@/components/climate';
|
||||||
import { LightsOverlay } from '@/components/lights';
|
import { LightsOverlay } from '@/components/lights';
|
||||||
import { LocksOverlay } from '@/components/locks';
|
import { LocksOverlay } from '@/components/locks';
|
||||||
import { AlarmoPanel } from '@/components/alarm';
|
|
||||||
import { CalendarWidget } from '@/components/calendar';
|
import { CalendarWidget } from '@/components/calendar';
|
||||||
import { TodoWidget } from '@/components/todo';
|
import { TodoWidget } from '@/components/todo';
|
||||||
import { SettingsPanel, ConnectionModal } from '@/components/settings';
|
import { SettingsPanel, ConnectionModal } from '@/components/settings';
|
||||||
@@ -16,7 +15,7 @@ import { useHomeAssistant } from '@/hooks';
|
|||||||
import { useIdle } from '@/hooks/useIdle';
|
import { useIdle } from '@/hooks/useIdle';
|
||||||
// Motion detection now runs in Electron main process (MotionDetector.ts)
|
// Motion detection now runs in Electron main process (MotionDetector.ts)
|
||||||
// import { useSimpleMotion } from '@/hooks/useSimpleMotion';
|
// import { useSimpleMotion } from '@/hooks/useSimpleMotion';
|
||||||
import { useHAStore, useEntityAttribute } from '@/stores/haStore';
|
import { useHAStore } from '@/stores/haStore';
|
||||||
import { useUIStore, useCameraOverlay } from '@/stores/uiStore';
|
import { useUIStore, useCameraOverlay } from '@/stores/uiStore';
|
||||||
import { useSettingsStore } from '@/stores/settingsStore';
|
import { useSettingsStore } from '@/stores/settingsStore';
|
||||||
import { env } from '@/config/environment';
|
import { env } from '@/config/environment';
|
||||||
@@ -71,12 +70,6 @@ function PersonAlert({ cameraName, onClose }: { cameraName: string; onClose: ()
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple thermostat temp display
|
|
||||||
function ThermostatTemp({ entityId }: { entityId: string }) {
|
|
||||||
const currentTemp = useEntityAttribute<number>(entityId, 'current_temperature');
|
|
||||||
return <>{currentTemp?.toFixed(0) ?? '--'}°</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectionPrompt() {
|
function ConnectionPrompt() {
|
||||||
const openSettings = useUIStore((state) => state.openSettings);
|
const openSettings = useUIStore((state) => state.openSettings);
|
||||||
|
|
||||||
@@ -104,72 +97,20 @@ function ConnectionPrompt() {
|
|||||||
|
|
||||||
function DashboardContent() {
|
function DashboardContent() {
|
||||||
const config = useSettingsStore((state) => state.config);
|
const config = useSettingsStore((state) => state.config);
|
||||||
const openLightsOverlay = useUIStore((state) => state.openLightsOverlay);
|
|
||||||
const openLocksOverlay = useUIStore((state) => state.openLocksOverlay);
|
|
||||||
const openThermostatsOverlay = useUIStore((state) => state.openThermostatsOverlay);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="col-span-12 flex flex-col gap-3 min-h-0">
|
||||||
{/* Left Column - Calendar (spans 2 columns) */}
|
{config.calendar && (
|
||||||
<div className="col-span-8 flex flex-col gap-4">
|
<div className="flex-[9] min-h-0">
|
||||||
{config.calendar && (
|
<CalendarWidget />
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<CalendarWidget />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Controls, Alarm, Todo */}
|
|
||||||
<div className="col-span-4 flex flex-col gap-3">
|
|
||||||
{/* Control Buttons Row - Lights, Locks, Thermostats */}
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{config.lights.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={openLightsOverlay}
|
|
||||||
className="widget flex-col items-center justify-center gap-1 py-3 hover:bg-dark-hover transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6 text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-xs">Lights</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{config.locks.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={openLocksOverlay}
|
|
||||||
className="widget flex-col items-center justify-center gap-1 py-3 hover:bg-dark-hover transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6 text-status-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-xs">Locks</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{config.thermostats.map((thermostat) => (
|
|
||||||
<button
|
|
||||||
key={thermostat.entityId}
|
|
||||||
onClick={openThermostatsOverlay}
|
|
||||||
className="widget flex-col items-center justify-center gap-1 py-3 hover:bg-dark-hover transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<span className="text-xl font-light text-orange-400">
|
|
||||||
<ThermostatTemp entityId={thermostat.entityId} />
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-400">{thermostat.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Alarm Panel */}
|
{config.todoList && (
|
||||||
{config.alarm && <AlarmoPanel />}
|
<div className="flex-[1] min-h-0">
|
||||||
|
<TodoWidget />
|
||||||
{/* Todo List */}
|
</div>
|
||||||
{config.todoList && (
|
)}
|
||||||
<div className="flex-1 min-h-0">
|
</div>
|
||||||
<TodoWidget />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +132,6 @@ export default function App() {
|
|||||||
const [alertCamera, setAlertCamera] = useState<string | null>(null);
|
const [alertCamera, setAlertCamera] = useState<string | null>(null);
|
||||||
const alertShownForRef = useRef<Set<string>>(new Set());
|
const alertShownForRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
// Motion detection now runs in the Electron main process (MotionDetector.ts)
|
|
||||||
// This prevents browser throttling when the screensaver is active
|
|
||||||
|
|
||||||
// Report touch/click activity to main process for screen wake on Wayland
|
// Report touch/click activity to main process for screen wake on Wayland
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleActivity = () => {
|
const handleActivity = () => {
|
||||||
|
|||||||
@@ -82,13 +82,50 @@ function PackageStatus() {
|
|||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const connectionState = useConnectionState();
|
const connectionState = useConnectionState();
|
||||||
|
const cameraOverlayOpen = useUIStore((state) => state.cameraOverlayOpen);
|
||||||
|
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
|
||||||
|
const settingsOpen = useUIStore((state) => state.settingsOpen);
|
||||||
const openCameraOverlay = useUIStore((state) => state.openCameraOverlay);
|
const openCameraOverlay = useUIStore((state) => state.openCameraOverlay);
|
||||||
|
const closeCameraOverlay = useUIStore((state) => state.closeCameraOverlay);
|
||||||
const openMediaOverlay = useUIStore((state) => state.openMediaOverlay);
|
const openMediaOverlay = useUIStore((state) => state.openMediaOverlay);
|
||||||
|
const closeMediaOverlay = useUIStore((state) => state.closeMediaOverlay);
|
||||||
const openSettings = useUIStore((state) => state.openSettings);
|
const openSettings = useUIStore((state) => state.openSettings);
|
||||||
|
const closeSettings = useUIStore((state) => state.closeSettings);
|
||||||
const people = useSettingsStore((state) => state.config.people);
|
const people = useSettingsStore((state) => state.config.people);
|
||||||
const cameras = useSettingsStore((state) => state.config.cameras);
|
const cameras = useSettingsStore((state) => state.config.cameras);
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
|
||||||
|
const activeTab: 'home' | 'media' | 'cameras' | 'settings' =
|
||||||
|
settingsOpen ? 'settings' : cameraOverlayOpen ? 'cameras' : mediaOverlayOpen ? 'media' : 'home';
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
closeCameraOverlay();
|
||||||
|
closeMediaOverlay();
|
||||||
|
closeSettings();
|
||||||
|
};
|
||||||
|
const goMedia = () => {
|
||||||
|
closeCameraOverlay();
|
||||||
|
closeSettings();
|
||||||
|
openMediaOverlay();
|
||||||
|
};
|
||||||
|
const goCameras = () => {
|
||||||
|
closeMediaOverlay();
|
||||||
|
closeSettings();
|
||||||
|
openCameraOverlay();
|
||||||
|
};
|
||||||
|
const goSettings = () => {
|
||||||
|
closeMediaOverlay();
|
||||||
|
closeCameraOverlay();
|
||||||
|
openSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabClass = (name: typeof activeTab) =>
|
||||||
|
`flex items-center gap-2 px-4 py-2 rounded-2xl font-semibold text-sm transition-all ${
|
||||||
|
activeTab === name
|
||||||
|
? 'bg-accent text-white shadow-card'
|
||||||
|
: 'bg-transparent text-ink-muted hover:bg-dark-hover'
|
||||||
|
}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCurrentTime(new Date());
|
setCurrentTime(new Date());
|
||||||
@@ -122,23 +159,18 @@ export function Header() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-14 bg-dark-secondary border-b border-dark-border flex items-center justify-between px-5">
|
<header className="h-16 bg-dark-secondary border-b border-dark-border flex items-center justify-between px-5 gap-4">
|
||||||
{/* Left - Time and Date */}
|
{/* Left - Time, Date, People */}
|
||||||
<div className="flex items-baseline gap-3">
|
<div className="flex items-center gap-5 min-w-0">
|
||||||
<span className="text-2xl font-semibold text-white tracking-tight">
|
<div className="flex items-baseline gap-3">
|
||||||
{format(currentTime, 'h:mm')}
|
<span className="text-2xl font-bold text-ink tracking-tight">
|
||||||
</span>
|
{format(currentTime, 'h:mm')}
|
||||||
<span className="text-sm text-gray-500">
|
</span>
|
||||||
{format(currentTime, 'EEE, MMM d')}
|
<span className="text-sm font-semibold text-ink-muted">
|
||||||
</span>
|
{format(currentTime, 'EEE, MMM d')}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
{/* Center - Status Icons */}
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Package Status */}
|
|
||||||
<PackageStatus />
|
<PackageStatus />
|
||||||
|
|
||||||
{/* People */}
|
|
||||||
{people.length > 0 && (
|
{people.length > 0 && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{people.map((person) => (
|
{people.map((person) => (
|
||||||
@@ -153,53 +185,44 @@ export function Header() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right - Connection Status, Cameras, Settings */}
|
{/* Center - Tab Navigation */}
|
||||||
<div className="flex items-center gap-4">
|
<nav className="flex items-center gap-1 bg-dark-tertiary rounded-2xl p-1">
|
||||||
{/* Connection Status */}
|
<button onClick={goHome} className={tabClass('home')}>
|
||||||
<div className="status-badge">
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div className={`status-dot ${getConnectionStatusClass()}`} />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
<span>{getConnectionText()}</span>
|
</svg>
|
||||||
</div>
|
Home
|
||||||
|
</button>
|
||||||
{/* Media Button */}
|
<button onClick={goMedia} className={tabClass('media')}>
|
||||||
<button
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
onClick={openMediaOverlay}
|
|
||||||
className="btn btn-sm"
|
|
||||||
aria-label="Open media"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Media
|
Media
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Camera Button */}
|
|
||||||
{cameras.length > 0 && (
|
{cameras.length > 0 && (
|
||||||
<button
|
<button onClick={goCameras} className={tabClass('cameras')}>
|
||||||
onClick={() => openCameraOverlay()}
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
className="btn btn-sm"
|
|
||||||
aria-label="View cameras"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
Cameras
|
Cameras
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<button onClick={goSettings} className={tabClass('settings')}>
|
||||||
{/* Settings Button */}
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<button
|
|
||||||
onClick={openSettings}
|
|
||||||
className="btn btn-sm"
|
|
||||||
aria-label="Settings"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right - Connection Status */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="status-badge">
|
||||||
|
<div className={`status-dot ${getConnectionStatusClass()}`} />
|
||||||
|
<span>{getConnectionText()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
type ElectronAPILike = {
|
|
||||||
motion?: { onDetected: (cb: () => void) => () => void };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks touch/mouse/keyboard activity. After `timeoutMs` of no activity,
|
* Tracks touch/mouse/keyboard activity. After `timeoutMs` of no activity,
|
||||||
* flips the UI into idle mode (photo frame). Any activity exits idle.
|
* flips the UI into idle mode (photo frame). Any activity exits idle.
|
||||||
* Motion detected by Electron's MotionDetector also cancels idle.
|
|
||||||
*/
|
*/
|
||||||
export function useIdle(timeoutMs: number) {
|
export function useIdle(timeoutMs: number) {
|
||||||
const isIdle = useUIStore((s) => s.isIdle);
|
const isIdle = useUIStore((s) => s.isIdle);
|
||||||
@@ -34,15 +29,11 @@ export function useIdle(timeoutMs: number) {
|
|||||||
];
|
];
|
||||||
for (const e of events) document.addEventListener(e, reset, { passive: true });
|
for (const e of events) document.addEventListener(e, reset, { passive: true });
|
||||||
|
|
||||||
const api = (window as unknown as { electronAPI?: ElectronAPILike }).electronAPI;
|
|
||||||
const unsubMotion = api?.motion?.onDetected?.(() => reset());
|
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
for (const e of events) document.removeEventListener(e, reset);
|
for (const e of events) document.removeEventListener(e, reset);
|
||||||
if (unsubMotion) unsubMotion();
|
|
||||||
};
|
};
|
||||||
}, [timeoutMs, setIdle]);
|
}, [timeoutMs, setIdle]);
|
||||||
|
|
||||||
|
|||||||
3
src/vite-env.d.ts
vendored
3
src/vite-env.d.ts
vendored
@@ -51,9 +51,6 @@ interface ElectronAPI {
|
|||||||
getDir: () => Promise<string>;
|
getDir: () => Promise<string>;
|
||||||
getUrl: (relative: string) => Promise<string | null>;
|
getUrl: (relative: string) => Promise<string | null>;
|
||||||
};
|
};
|
||||||
motion: {
|
|
||||||
onDetected: (callback: () => void) => () => void;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
Reference in New Issue
Block a user