PWA widgets dashboard, lock screen, screensaver with active category

- Dashboard (/dashboard): configurable widget system with 6 widget types
  (current_tasks, category_time, today_progress, next_task, motivace, calendar_mini)
  stored in localStorage widget_config
- Lock screen (/lockscreen): fullscreen with clock, active group badge,
  up to 4 current tasks, gradient from group color, swipe/tap to unlock
- InactivityMonitor: auto-redirect to lockscreen after configurable timeout
  (only in PWA standalone/fullscreen mode)
- Settings: widget toggle switches, inactivity timeout slider, lockscreen preview
- manifest.json: added display_override ["fullscreen","standalone"] + orientation portrait

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 11:32:08 +00:00
parent f2915b79fa
commit 4ace4d5f7d
7 changed files with 686 additions and 1 deletions

View File

@@ -0,0 +1,54 @@
"use client";
import { useEffect, useRef } from "react";
import { useRouter, usePathname } from "next/navigation";
export default function InactivityMonitor() {
const router = useRouter();
const pathname = usePathname();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
// Only activate in PWA standalone mode
const isStandalone =
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches ||
(window.navigator as unknown as { standalone?: boolean }).standalone === true;
if (!isStandalone) return;
// Skip on lockscreen/login pages
if (pathname?.startsWith("/lockscreen") || pathname?.startsWith("/login") || pathname?.startsWith("/register")) return;
// Read timeout from localStorage
let timeoutMinutes = 5;
try {
const stored = localStorage.getItem("widget_config");
if (stored) {
const cfg = JSON.parse(stored);
if (cfg.inactivityTimeout > 0) timeoutMinutes = cfg.inactivityTimeout;
}
} catch { /* ignore */ }
const timeoutMs = timeoutMinutes * 60 * 1000;
function resetTimer() {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
router.push("/lockscreen");
}, timeoutMs);
}
const events = ["mousedown", "mousemove", "keydown", "touchstart", "scroll", "click"];
events.forEach(e => window.addEventListener(e, resetTimer, { passive: true }));
resetTimer();
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
events.forEach(e => window.removeEventListener(e, resetTimer));
};
}, [pathname, router]);
return null;
}