Skip to content

Commit 2556feb

Browse files
Merge pull request #316 from ZoneMinder/feat/313-live-activity
feat: Live Activity page, plus stream-teardown and event-poller fixes
2 parents 32664be + 7d46a8b commit 2556feb

75 files changed

Lines changed: 8441 additions & 179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/.lint-baseline.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"react-hooks/globals": 1,
88
"react-hooks/preserve-manual-memoization": 12,
99
"react-hooks/refs": 22,
10-
"react-hooks/set-state-in-effect": 14,
10+
"react-hooks/set-state-in-effect": 15,
1111
"react-hooks/static-components": 9,
1212
"react-refresh/only-export-components": 8,
1313
"unused-eslint-disable-directive": 1

app/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const Dashboard = lazy(() => import('./pages/Dashboard'));
3636
const Monitors = lazy(() => import('./pages/Monitors'));
3737
const MonitorDetail = lazy(() => import('./pages/MonitorDetail'));
3838
const Montage = lazy(() => import('./pages/Montage'));
39+
const LiveActivity = lazy(() => import('./pages/LiveActivity'));
3940
const Events = lazy(() => import('./pages/Events'));
4041
const EventDetail = lazy(() => import('./pages/EventDetail'));
4142
const Timeline = lazy(() => import('./pages/Timeline'));
@@ -255,6 +256,14 @@ function AppRoutes() {
255256
</RouteErrorBoundary>
256257
}
257258
/>
259+
<Route
260+
path="/live-activity"
261+
element={
262+
<RouteErrorBoundary routePath="/live-activity">
263+
<LiveActivity />
264+
</RouteErrorBoundary>
265+
}
266+
/>
258267
<Route
259268
path="/events"
260269
element={

app/src/components/layout/SidebarContent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
HelpCircle,
3636
Megaphone,
3737
Command,
38+
Activity,
3839
} from 'lucide-react';
3940
import { useCommandPaletteStore } from '../../stores/commandPalette';
4041
import { useDeveloperNotices } from '../../hooks/useDeveloperNotices';
@@ -106,6 +107,7 @@ export function SidebarContent({ onMobileClose, isCollapsed }: SidebarContentPro
106107
{ path: '/dashboard', label: t('sidebar.dashboard'), icon: LayoutDashboard },
107108
{ path: '/monitors', label: t('sidebar.monitors'), icon: Video },
108109
{ path: '/montage', label: t('sidebar.montage'), icon: LayoutGrid },
110+
{ path: '/live-activity', label: t('sidebar.live_activity'), icon: Activity },
109111
{ path: '/events', label: t('sidebar.events'), icon: Clock },
110112
{ path: '/timeline', label: t('sidebar.timeline'), icon: ChartGantt },
111113
{ path: '/notifications', label: t('sidebar.notifications'), icon: Bell },
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* The page furniture around the Live Activity grid, in both of its modes.
3+
*
4+
* Normally that is a heading and the controls; in fullscreen it is a thin
5+
* translucent bar and nothing else, so a wall display shows tiles. Montage's
6+
* own fullscreen bar is not reused: it carries the kiosk lock and the tile
7+
* label toggle, and importing it would pull the kiosk store and the PIN pad
8+
* onto a page that offers neither.
9+
*/
10+
11+
import { useTranslation } from 'react-i18next';
12+
import { Maximize, Minimize, Settings } from 'lucide-react';
13+
import { EventMontageGridControls } from '../events/EventMontageGridControls';
14+
import { Button } from '../ui/button';
15+
16+
interface LiveActivityHeaderProps {
17+
gridCols: number;
18+
customCols: string;
19+
isCustomGridDialogOpen: boolean;
20+
onApplyGridLayout: (cols: number) => void;
21+
onCustomColsChange: (value: string) => void;
22+
onCustomGridDialogOpenChange: (open: boolean) => void;
23+
onCustomGridSubmit: () => void;
24+
onEnterFullscreen: () => void;
25+
onOpenSettings: () => void;
26+
}
27+
28+
export function LiveActivityHeader({
29+
gridCols,
30+
customCols,
31+
isCustomGridDialogOpen,
32+
onApplyGridLayout,
33+
onCustomColsChange,
34+
onCustomGridDialogOpenChange,
35+
onCustomGridSubmit,
36+
onEnterFullscreen,
37+
onOpenSettings,
38+
}: LiveActivityHeaderProps) {
39+
const { t } = useTranslation();
40+
41+
return (
42+
<div className="flex items-center justify-between gap-2 mb-3">
43+
<h1 className="text-lg font-semibold min-w-0 truncate" title={t('live_activity.title')}>
44+
{t('live_activity.title')}
45+
</h1>
46+
<div className="flex items-center gap-1">
47+
<EventMontageGridControls
48+
gridCols={gridCols}
49+
customCols={customCols}
50+
isCustomGridDialogOpen={isCustomGridDialogOpen}
51+
onApplyGridLayout={onApplyGridLayout}
52+
onCustomColsChange={onCustomColsChange}
53+
onCustomGridDialogOpenChange={onCustomGridDialogOpenChange}
54+
onCustomGridSubmit={onCustomGridSubmit}
55+
/>
56+
<Button
57+
variant="ghost"
58+
size="icon"
59+
onClick={onEnterFullscreen}
60+
title={t('live_activity.fullscreen')}
61+
aria-label={t('live_activity.fullscreen')}
62+
data-testid="live-activity-fullscreen-btn"
63+
>
64+
<Maximize className="h-4 w-4" />
65+
</Button>
66+
<Button
67+
variant="ghost"
68+
size="icon"
69+
onClick={onOpenSettings}
70+
title={t('live_activity.settings_title')}
71+
aria-label={t('live_activity.settings_title')}
72+
data-testid="live-activity-settings-btn"
73+
>
74+
<Settings className="h-4 w-4" />
75+
</Button>
76+
</div>
77+
</div>
78+
);
79+
}
80+
81+
export function LiveActivityFullscreenBar({ onExit }: { onExit: () => void }) {
82+
const { t } = useTranslation();
83+
84+
return (
85+
<div className="flex items-center justify-between gap-2 h-9 px-3 pt-[var(--sai-top,env(safe-area-inset-top))] bg-black/50 backdrop-blur-sm shrink-0">
86+
<span className="text-white/70 font-medium text-xs min-w-0 truncate">
87+
{t('live_activity.title')}
88+
</span>
89+
<Button
90+
onClick={onExit}
91+
variant="ghost"
92+
size="icon"
93+
className="text-white/70 hover:text-white hover:bg-white/10 h-7 w-7"
94+
title={t('live_activity.exit_fullscreen')}
95+
aria-label={t('live_activity.exit_fullscreen')}
96+
data-testid="live-activity-exit-fullscreen-btn"
97+
>
98+
<Minimize className="h-4 w-4" />
99+
</Button>
100+
</div>
101+
);
102+
}

0 commit comments

Comments
 (0)