mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01:40 +00:00
General UI Refinements (#1678)
Does the following: - Adjusts the shade of red buttons and banners to increase readability and reduce eye strain   - Cleans up factory reset and camera deletion modals   - Removes matchCamerasOnlyByPath as it is no longer used and throws errors in the console  - Limits the criteria to flag a camera mismatch in Camera Matching to only what is necessary based on camera type and highlights differences in table properties (testing on this is appreciated)  - Only displays both saved vs. current info in camera matching if there is a difference between the two  - Some general code cleanup (reduced unnecessary padding/margin/row-col statements, style="display:flex;" -> class="d-flex", etc. - Moves Compact Mode button to the bottom away from all the menu items (cleaner imo, open to thoughts) - Establishes a general spacing format for cards and pages and applies this to existing cards and pages to create a consistent look and feel to the UI (e.g. keeping things in line and less erratic spacing/placement of UI elements)     - Delete protection for camera matching modules - Anti-backend-spam for activate/deactivate/delete modules to hopefully prevent any odd behavior from button spamming - Enforces a common camera stream size on camera matching view (NEEDS MORE TESTING)  https://private-user-images.githubusercontent.com/29715865/400783758-dc99c151-b8a7-4367-a173-74c2fc5b2666.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzYyNTc3NzEsIm5iZiI6MTczNjI1NzQ3MSwicGF0aCI6Ii8yOTcxNTg2NS80MDA3ODM3NTgtZGM5OWMxNTEtYjhhNy00MzY3LWExNzMtNzRjMmZjNWIyNjY2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAxMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMTA3VDEzNDQzMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwOWM1MDc2ZTVlOWZhM2MxYjAwZjAyZTc2MTYyZTk1ZTVmOGFhZmVkMzlmODRlZTk1ODVlOTk2ZGQzZmM0Y2EmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.ovtRnObwbkEfljr9d5fqaory0nH91LWJSSkmrUUe_4Y
This commit is contained in:
@@ -1,49 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { computed, inject, onMounted, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import {
|
||||
PlaceholderCameraSettings,
|
||||
PVCameraInfo,
|
||||
type PVCSICameraInfo,
|
||||
type PVFileCameraInfo,
|
||||
type PVUsbCameraInfo
|
||||
type PVUsbCameraInfo,
|
||||
type UiCameraConfiguration
|
||||
} from "@/types/SettingTypes";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
|
||||
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
const host = inject<string>("backendHost");
|
||||
|
||||
const activatingModule = ref(false);
|
||||
const activateModule = (moduleUniqueName: string) => {
|
||||
if (activatingModule.value) return;
|
||||
activatingModule.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/activateMatchedCamera`);
|
||||
url.searchParams.set("cameraUniqueName", moduleUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
}).finally(() => {
|
||||
activatingModule.value = false;
|
||||
setTimeout(() => enforceStreamHeight(), 1000);
|
||||
});
|
||||
};
|
||||
const activateCamera = (cameraInfo: PVCameraInfo) => {
|
||||
|
||||
const assigningCamera = ref(false);
|
||||
const assignCamera = (cameraInfo: PVCameraInfo) => {
|
||||
if (assigningCamera.value) return;
|
||||
assigningCamera.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`);
|
||||
url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo));
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
}).finally(() => {
|
||||
assigningCamera.value = false;
|
||||
setTimeout(() => enforceStreamHeight(), 1000);
|
||||
});
|
||||
};
|
||||
const deactivateCamera = (cameraUniqueName: string) => {
|
||||
console.log("Deactivating " + cameraUniqueName);
|
||||
|
||||
const deactivatingModule = ref(false);
|
||||
const deactivateModule = (cameraUniqueName: string) => {
|
||||
if (deactivatingModule.value) return;
|
||||
deactivatingModule.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/unassignCamera`);
|
||||
url.searchParams.set("cameraUniqueName", cameraUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
});
|
||||
}).finally(() => (deactivatingModule.value = false));
|
||||
};
|
||||
|
||||
const deletingCamera = ref(false);
|
||||
const deleteThisCamera = (cameraName: string) => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = true;
|
||||
const payload = {
|
||||
cameraUniqueName: cameraName
|
||||
};
|
||||
@@ -52,7 +73,7 @@ const deleteThisCamera = (cameraName: string) => {
|
||||
.post("/utils/nukeOneCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully deleted " + cameraName,
|
||||
message: "Camera deleted successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
@@ -73,9 +94,34 @@ const deleteThisCamera = (cameraName: string) => {
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setCameraDeleting(null);
|
||||
deletingCamera.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
|
||||
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
|
||||
return (
|
||||
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
|
||||
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
|
||||
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
|
||||
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
|
||||
);
|
||||
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
|
||||
return (
|
||||
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
|
||||
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
|
||||
);
|
||||
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
|
||||
return (
|
||||
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
|
||||
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
|
||||
);
|
||||
else return false;
|
||||
};
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
@@ -155,38 +201,76 @@ const activeVisionModules = computed(() =>
|
||||
const disabledVisionModules = computed(() => useStateStore().vsmState.disabledConfigs);
|
||||
|
||||
const viewingDetails = ref(false);
|
||||
const showCurrentView = ref(false);
|
||||
const viewingCamera = ref<PVCameraInfo | null>(null);
|
||||
|
||||
const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false) => {
|
||||
const setCameraView = (camera: PVCameraInfo | null) => {
|
||||
viewingDetails.value = camera !== null;
|
||||
viewingCamera.value = camera;
|
||||
showCurrentView.value = showCurrent;
|
||||
};
|
||||
|
||||
const viewingDeleteCamera = ref(false);
|
||||
const cameraToDelete = ref<UiCameraConfiguration | WebsocketCameraSettingsUpdate | null>(null);
|
||||
const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettingsUpdate | null) => {
|
||||
yesDeleteMySettingsText.value = "";
|
||||
viewingDeleteCamera.value = camera !== null;
|
||||
cameraToDelete.value = camera;
|
||||
};
|
||||
const yesDeleteMySettingsText = ref("");
|
||||
const exportSettings = ref();
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
};
|
||||
|
||||
const enforceStreamHeight = () => {
|
||||
const streamWidth = document.getElementById("stream-container-0")?.offsetWidth ?? 0;
|
||||
if (streamWidth === 0) return;
|
||||
|
||||
Object.values(useCameraSettingsStore().cameras)
|
||||
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
||||
.forEach((element, index) => {
|
||||
let stream = document.getElementById(`outer-output-camera-stream-${index}`);
|
||||
if (!stream) return;
|
||||
|
||||
stream?.classList.remove("tall-stream", "wide-stream", "d-none");
|
||||
let streamRes = element.validVideoFormats[0].resolution.width / element.validVideoFormats[0].resolution.height;
|
||||
let containerRes = streamWidth / 250.0;
|
||||
if (element.pipelineSettings.inputImageRotationMode % 2 == 1) streamRes = 1 / streamRes;
|
||||
if (streamRes > containerRes) stream?.classList.add("wide-stream");
|
||||
else stream?.classList.add("tall-stream");
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => enforceStreamHeight(), 1000);
|
||||
window.addEventListener("resize", enforceStreamHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-5">
|
||||
<v-row>
|
||||
<!-- Active modules -->
|
||||
<v-col v-for="module in activeVisionModules" :key="`enabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
|
||||
<v-col
|
||||
v-for="(module, index) in activeVisionModules"
|
||||
:key="`enabled-${module.uniqueName}`"
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
<v-card-subtitle v-if="_.isEqual(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)"
|
||||
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
|
||||
<v-card-subtitle v-if="camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)"
|
||||
>Status: <span class="active-status">Active</span></v-card-subtitle
|
||||
>
|
||||
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-simple-table dark dense>
|
||||
<v-simple-table dark dense class="mb-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Streams:</td>
|
||||
<td>
|
||||
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="active-status">
|
||||
Input Stream
|
||||
</a>
|
||||
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input Stream </a>
|
||||
/
|
||||
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="active-status">
|
||||
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link">
|
||||
Output Stream
|
||||
</a>
|
||||
</td>
|
||||
@@ -219,18 +303,24 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
class="mt-3"
|
||||
:camera-settings="module"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
<div
|
||||
:id="`stream-container-${index}`"
|
||||
class="d-flex flex-column justify-center align-center"
|
||||
style="height: 250px"
|
||||
>
|
||||
<photon-camera-stream
|
||||
:id="`output-camera-stream-${index}`"
|
||||
:camera-settings="module"
|
||||
stream-type="Processed"
|
||||
:outer-id="`outer-output-camera-stream-${index}`"
|
||||
class="d-none"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
|
||||
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo, true)">
|
||||
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo)">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -239,18 +329,14 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
class="black--text"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
@click="deactivateCamera(module.uniqueName)"
|
||||
:loading="deactivatingModule"
|
||||
@click="deactivateModule(module.uniqueName)"
|
||||
>
|
||||
Deactivate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
>
|
||||
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -305,18 +391,14 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
class="black--text"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
:loading="activatingModule"
|
||||
@click="activateModule(module.uniqueName)"
|
||||
>
|
||||
Activate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
>
|
||||
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -347,7 +429,13 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn class="black--text" color="accent" style="width: 100%" @click="activateCamera(camera)">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
:loading="assigningCamera"
|
||||
@click="assignCamera(camera)"
|
||||
>
|
||||
Activate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -373,7 +461,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</v-row>
|
||||
|
||||
<!-- Camera details modal -->
|
||||
<v-dialog v-model="viewingDetails">
|
||||
<v-dialog v-model="viewingDetails" max-width="800">
|
||||
<v-card v-if="viewingCamera !== null" dark flat color="primary">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>{{ cameraInfoFor(viewingCamera)?.name ?? cameraInfoFor(viewingCamera)?.baseName }}</span>
|
||||
@@ -381,28 +469,62 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-icon>mdi-close-thick</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-banner
|
||||
v-show="!_.isEqual(getMatchedDevice(viewingCamera), viewingCamera)"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
class="mb-3"
|
||||
>
|
||||
Camera Mismatched:<br />It looks like a different camera has been connected to this device! Compare the
|
||||
below information carefully.
|
||||
<v-card-text v-if="!camerasMatch(getMatchedDevice(viewingCamera), viewingCamera)">
|
||||
<v-banner rounded color="error" text-color="white" icon="mdi-information-outline" class="mb-3">
|
||||
It looks like a different camera may have been connected to this device! Compare the following information
|
||||
carefully.
|
||||
</v-banner>
|
||||
<div v-if="showCurrentView">
|
||||
<h3>Saved camera</h3>
|
||||
<PvCameraInfoCard :camera="viewingCamera" :show-title="false" />
|
||||
<br />
|
||||
<h3>Current camera</h3>
|
||||
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :show-title="false" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<PvCameraInfoCard :camera="viewingCamera" />
|
||||
</div>
|
||||
<PvCameraMatchCard :saved="viewingCamera" :current="getMatchedDevice(viewingCamera)" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Camera delete modal -->
|
||||
<v-dialog v-model="viewingDeleteCamera" dark width="800">
|
||||
<v-card v-if="cameraToDelete !== null" dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="align-center pt-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" block @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${host}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + cameraToDelete.nickname + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
block
|
||||
color="error"
|
||||
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
|
||||
:loading="deletingCamera"
|
||||
@click="deleteThisCamera(cameraToDelete.uniqueName)"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">DELETE (UNRECOVERABLE)</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -414,7 +536,6 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
background-color: #006492 !important;
|
||||
}
|
||||
|
||||
a:link,
|
||||
.active-status {
|
||||
color: rgb(14, 240, 14);
|
||||
background-color: transparent;
|
||||
@@ -434,9 +555,20 @@ a:hover {
|
||||
}
|
||||
|
||||
a:active,
|
||||
.stream-link,
|
||||
.mismatch-status {
|
||||
color: yellow;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wide-stream {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tall-stream {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user