Extract web app client controller helpers

This commit is contained in:
2026-04-16 10:15:00 +01:00
parent 78d14cb73f
commit 9a8603e5cd
2 changed files with 200 additions and 124 deletions

View File

@@ -12,6 +12,7 @@ import {
getMotionDetectionProfile,
pageFromPath
} from './controller-shared';
import { createControllerClientModule } from './controller-client';
import { createControllerMediaModule } from './controller-media';
import { getAppState, patchAppState, resetAppState, setAppState } from './store';
@@ -86,6 +87,14 @@ const {
patchAppState
});
const clientController = createControllerClientModule({
api,
getAppState,
setAppState,
patchAppState,
makeId
});
const mediaController = createControllerMediaModule({
api,
createMotionDetector,
@@ -98,6 +107,17 @@ const mediaController = createControllerMediaModule({
updateMotionDetectionState
});
const {
getLinkedCamera,
getCameraLabel,
markMotionNotificationRead,
markAllNotificationsRead,
pushMotionNotification,
openRecordingModal,
closeRecordingModal,
pollClientData
} = clientController;
const {
refreshCameraInputDevices,
startCameraPreview,
@@ -275,130 +295,6 @@ const removeCameraSessionMapping = (streamSessionId) => {
}));
};
const getLinkedCamera = (cameraDeviceId) =>
getAppState().linkedCameras.find((camera) => camera.cameraDeviceId === cameraDeviceId);
const getCameraLabel = (cameraDeviceId, cameraName) => {
const explicitName = typeof cameraName === 'string' ? cameraName.trim() : '';
if (explicitName) return explicitName;
const linkedName = getLinkedCamera(cameraDeviceId)?.cameraName;
if (typeof linkedName === 'string' && linkedName.trim()) {
return linkedName.trim();
}
return `Camera ${cameraDeviceId?.substring(0, 6) ?? 'Unknown'}`;
};
const mapNotificationDeliveryToMotionNotification = (delivery) => {
const payload = delivery?.payload ?? {};
const cameraDeviceId = typeof payload.cameraDeviceId === 'string' ? payload.cameraDeviceId : '';
const deliveryType = typeof delivery?.type === 'string' ? delivery.type : '';
if (deliveryType !== 'motion_detected') {
return null;
}
return {
id: delivery.id,
eventId: typeof payload.eventId === 'string' ? payload.eventId : null,
cameraDeviceId,
message: `${getCameraLabel(cameraDeviceId)} has detected movement`,
createdAt: delivery.sentAt || delivery.createdAt || new Date().toISOString(),
isRead: delivery.status === 'read'
};
};
const syncMotionNotificationsFromDeliveries = (deliveries) => {
const motionNotifications = (deliveries || [])
.map(mapNotificationDeliveryToMotionNotification)
.filter(Boolean)
.sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime());
setAppState({ motionNotifications });
};
const refreshMotionNotifications = async () => {
const { device } = getAppState();
if (!device || device.role !== 'client') {
setAppState({ motionNotifications: [] });
return [];
}
const result = await api.ops.listNotifications().catch(() => ({ notifications: [] }));
syncMotionNotificationsFromDeliveries(result.notifications);
return result.notifications || [];
};
const pushMotionNotification = (cameraDeviceId) => {
if (!cameraDeviceId) return;
const notification = {
id: makeId(),
eventId: null,
cameraDeviceId,
message: `${getCameraLabel(cameraDeviceId)} has detected movement`,
createdAt: new Date().toISOString(),
isRead: false
};
patchAppState((state) => ({
motionNotifications: [notification, ...state.motionNotifications].slice(0, 50)
}));
void refreshMotionNotifications();
};
const markMotionNotificationRead = async (notificationId) => {
patchAppState((state) => ({
motionNotifications: state.motionNotifications.map((notification) =>
notification.id === notificationId ? { ...notification, isRead: true } : notification
)
}));
try {
await api.pushNotifications.markRead(notificationId);
} catch (error) {
console.error('Failed to persist notification read state', error);
}
};
const markAllNotificationsRead = async () => {
const unreadIds = getAppState().motionNotifications.filter((notification) => !notification.isRead).map((notification) => notification.id);
if (unreadIds.length === 0) {
return;
}
patchAppState((state) => ({
motionNotifications: state.motionNotifications.map((notification) =>
notification.isRead ? notification : { ...notification, isRead: true }
)
}));
await Promise.allSettled(unreadIds.map((notificationId) => api.pushNotifications.markRead(notificationId)));
};
const openRecordingModal = (downloadUrl, title) => {
setAppState({
recordingModal: {
open: true,
title: title || 'Recording Playback',
url: downloadUrl
}
});
};
const closeRecordingModal = () => {
setAppState({
recordingModal: {
open: false,
title: 'Recording Playback',
url: ''
}
});
};
const teardownPeerConnection = (streamSessionId) => {
if (!streamSessionId) {
for (const connection of peerConnections.values()) {