Add photo frame idle mode and switch to Skylight-style theme

- After 5 min of no touch/motion, dashboard hides behind a fullscreen
  photo slideshow with centered time and date overlay
- Photos loaded from PHOTOS_PATH env var (defaults to ~/Pictures/dashboard)
  via IPC + file:// URLs; traversal-guarded, recursive up to 2 levels
- Motion or touch exits idle back to dashboard
- Theme repainted warm cream / sage / stone ink with Nunito font and
  rounded cards; dark tokens kept so component classes still resolve
- Adds PHOTO_FRAME.md with Samba cifs mount + systemd env instructions
This commit is contained in:
root
2026-04-14 10:44:51 -05:00
parent 58ebd3e239
commit 5fe7bc71ef
14 changed files with 566 additions and 77 deletions

View File

@@ -5,14 +5,23 @@ import { ScreenManager } from './services/ScreenManager';
import { PresenceDetector } from './services/PresenceDetector';
import { FrigateStreamer } from './services/FrigateStreamer';
import { MotionDetector } from './services/MotionDetector';
import { PhotoManager } from './services/PhotoManager';
let mainWindow: BrowserWindow | null = null;
let screenManager: ScreenManager | null = null;
let presenceDetector: PresenceDetector | null = null;
let frigateStreamer: FrigateStreamer | null = null;
let motionDetector: MotionDetector | null = null;
let photoManager: PhotoManager | null = null;
let powerSaveBlockerId: number | null = null;
// Photos directory: env var PHOTOS_PATH wins, else fall back to ~/Pictures/dashboard
function resolvePhotosDir(): string {
const env = process.env.PHOTOS_PATH;
if (env && env.trim()) return env.trim();
return path.join(app.getPath('home'), 'Pictures', 'dashboard');
}
// Check if we're in dev mode: explicitly set NODE_ENV or running from source with vite dev server
// When running from unpacked folder, app.isPackaged is false but we still want production behavior
const isDev = process.env.NODE_ENV === 'development';
@@ -30,7 +39,7 @@ function createWindow(): void {
kiosk: !isDev, // Kiosk mode in production
frame: false,
autoHideMenuBar: true,
backgroundColor: '#0a0a0a',
backgroundColor: '#faf6f0',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
@@ -71,6 +80,10 @@ function createWindow(): void {
motionDetector.start();
// Photo frame slideshow source
photoManager = new PhotoManager(resolvePhotosDir());
console.log(`PhotoManager: watching ${photoManager.getDir()}`);
// Handle window close
mainWindow.on('closed', () => {
mainWindow = null;
@@ -174,6 +187,24 @@ function setupIpcHandlers(): void {
}
});
// Photo frame
ipcMain.handle('photos:list', async () => {
return photoManager?.list() ?? [];
});
ipcMain.handle('photos:getDir', async () => {
return photoManager?.getDir() ?? '';
});
// Given a relative filename returned by photos:list, return an absolute
// file:// URL the renderer can drop into an <img src>. Returns null if the
// file is missing or resolves outside the photos dir (traversal guard).
ipcMain.handle('photos:getUrl', async (_event, rel: string) => {
const full = photoManager?.resolve(rel);
if (!full || !fs.existsSync(full)) return null;
return 'file://' + full.split(path.sep).map((p, i) => (i === 0 && p === '' ? '' : encodeURIComponent(p))).join('/');
});
// App control
ipcMain.handle('app:quit', () => {
app.quit();