Dark mode and minor interface tweaks (#2016)

Co-authored-by: Sam Freund <samf.236@proton.me>
This commit is contained in:
Devon Doyle
2025-08-04 01:15:33 -04:00
committed by GitHub
parent 3e19cd45cc
commit fce54d12c1
36 changed files with 956 additions and 765 deletions

1
.gitignore vendored
View File

@@ -147,3 +147,4 @@ photon-server/src/main/resources/web/*
node_modules
dist
components.d.ts
photon-server/src/main/resources/web/index.html

View File

@@ -87,30 +87,26 @@ if (!is_demo) {
}
::-webkit-scrollbar-track {
background: #232c37;
background: rgb(var(--v-theme-background));
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #e4c33c;
background-color: rgb(var(--v-theme-primary));
}
.main-container {
background-color: #232c37;
padding: 0 !important;
}
.v-overlay__scrim {
background-color: #202020;
background-color: #111111;
}
#title {
color: #ffd843;
}
div.v-layout {
overflow: unset !important;
}

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgb(0, 100, 146);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<g transform="rotate(0)">
<circle fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.8177570093457943s" type="scale" attributeName="transform"></animateTransform>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 508 507" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1279,0)">
<g id="PhotonVision-Icon-BG" transform="matrix(0.264062,0,0,0.469444,1279.5,0)">
<rect x="0" y="0" width="1920" height="1080" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="0" y="0" width="1920" height="1080"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(4.27015,0,0,2.40196,-20444.8,-3235.56)">
<circle cx="5012.55" cy="1571.77" r="224.918" style="fill:rgb(0,100,146,0);"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-10313.5)">
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-3827.86)">
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -3,7 +3,7 @@
$default-font: "Prompt", sans-serif !default;
$body-font-family: $default-font;
$heading-font-family: $default-font;
$body-background: #282c34;
$body-background: rgb(var(--v-theme-background));
body {
background: $body-background;
@@ -21,11 +21,7 @@ html {
> table
> tbody
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281 !important;
}
.v-banner {
padding: 4px !important;
background: rgba(0, 0, 0, 0.2);
}
.v-card-title,
@@ -72,3 +68,7 @@ html {
.pa-10px {
padding: 10px !important;
}
.rounded-12 {
border-radius: 12px;
}

View File

@@ -6,6 +6,7 @@ import { useStateStore } from "@/stores/StateStore";
<v-snackbar
v-model="useStateStore().snackbarData.show"
location="top"
variant="elevated"
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>

View File

@@ -74,7 +74,7 @@ document.addEventListener("keydown", (e) => {
<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card class="dialog-container pa-5" color="primary" flat>
<v-card class="dialog-container pa-5" color="surface" flat>
<!-- Logs header -->
<v-row class="pb-3">
<v-col cols="4">
@@ -82,7 +82,7 @@ document.addEventListener("keydown", (e) => {
</v-col>
<v-col class="align-self-center pl-3" style="text-align: right">
<v-btn variant="text" color="white" @click="handleLogExport">
<v-icon start class="menu-icon"> mdi-download </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-download </v-icon>
<span class="menu-label">Download</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -95,11 +95,11 @@ document.addEventListener("keydown", (e) => {
/>
</v-btn>
<v-btn variant="text" color="white" @click="handleLogClear">
<v-icon start class="menu-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="menu-label">Clear Client Logs</span>
</v-btn>
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon start class="menu-icon"> mdi-close </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-close </v-icon>
<span class="menu-label">Close</span>
</v-btn>
</v-col>
@@ -110,26 +110,31 @@ document.addEventListener("keydown", (e) => {
<div class="dialog-data">
<!-- Log view options -->
<v-row no-gutters class="pt-4 pt-md-0" style="display: flex; justify-content: space-between">
<v-col cols="12" md="7" style="display: flex; align-items: center">
<v-col cols="12" md="7" style="display: flex; align-items: center" class="pr-3">
<v-text-field
v-model="searchQuery"
density="compact"
clearable
hide-details="auto"
prepend-icon="mdi-magnify"
color="accent"
color="primary"
label="Search"
variant="underlined"
/>
<input v-model="timeInput" type="time" step="1" class="text-white pl-3" />
<v-btn icon variant="flat" @click="timeInput = undefined">
<v-icon>mdi-close-circle-outline</v-icon>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-col>
<v-col v-for="level in [0, 1, 2, 3]" :key="level" class="pr-3">
<div class="pb-0 pt-0" style="display: flex; align-items: center; flex: min-content">
{{ getLogLevelFromIndex(level)
}}<v-switch v-model="selectedLogLevels[level]" class="pl-2" hide-details color="#ffd843"></v-switch>
}}<v-switch
v-model="selectedLogLevels[level]"
class="pl-2"
hide-details
color="rgb(var(--v-theme-primary))"
></v-switch>
</div>
</v-col>
</v-row>
@@ -170,7 +175,7 @@ document.addEventListener("keydown", (e) => {
/* Dialog data size - options */
height: calc(100% - 56px);
padding: 10px;
background-color: #232c37 !important;
background-color: rgb(var(--v-theme-logsBackground)) !important;
border-radius: 5px;
}

View File

@@ -4,7 +4,8 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useRoute } from "vue-router";
import { useDisplay } from "vuetify";
import { useDisplay, useTheme } from "vuetify";
import { onBeforeMount } from "vue";
const compact = computed<boolean>({
get: () => {
@@ -16,17 +17,32 @@ const compact = computed<boolean>({
});
const { mdAndUp } = useDisplay();
const theme = useTheme();
const changeTheme = () => {
const newTheme = theme.global.name.value === "LightTheme" ? "DarkTheme" : "LightTheme";
theme.global.name.value = newTheme;
localStorage.setItem("theme", newTheme);
};
onBeforeMount(() => {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
theme.global.name.value = storedTheme;
}
});
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</script>
<template>
<v-navigation-drawer permanent :rail="renderCompact" color="primary">
<v-list nav>
<v-navigation-drawer permanent :rail="renderCompact" color="sidebar">
<v-list nav color="primary">
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<template #prepend>
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
<img v-else class="logo" src="@/assets/images/logoSmallTransparent.svg" alt="small logo" />
</template>
</v-list-item>
@@ -67,20 +83,35 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
@click="() => (compact = !compact)"
>
<v-list-item-title>Compact Mode</v-list-item-title>
<v-list-item-title>Compact</v-list-item-title>
</v-list-item>
<v-list-item
:prepend-icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
link
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
@click="changeTheme"
>
<v-list-item-title>Theme</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon
:icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
:color="
useSettingsStore().network.runNTServer || useStateStore().ntConnectionStatus.connected
? '#00ff00'
: '#ff0000'
"
/>
</template>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" v-show="!renderCompact" class="text-wrap">
NetworkTables server running for
<span class="text-accent">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
<span class="text-primary">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
@@ -89,9 +120,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="text-accent">
{{ useStateStore().ntConnectionStatus.address }}
</span>
<span class="text-primary"> {{ useStateStore().ntConnectionStatus.address }} </span>
</v-list-item-title>
<v-list-item-title
v-else
@@ -102,10 +131,15 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item>
<v-list-item :prepend-icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'">
<v-list-item>
<template #prepend>
<v-icon
:icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'"
:color="useStateStore().backendConnected ? '#00ff00' : '#ff0000'"
/>
</template>
<v-list-item-title v-show="!renderCompact" class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend..." }}
</v-list-item-title>
</v-list-item>
</v-list>
@@ -114,6 +148,14 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</template>
<style scoped>
.v-navigation-drawer {
border: none;
}
.v-navigation-drawer--rail {
border: none;
}
.v-list-item-title {
font-size: 1rem !important;
line-height: 1.2rem !important;

View File

@@ -13,9 +13,13 @@ import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { getResolutionString, resolutionsAreEqual } from "@/lib/PhotonUtils";
import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInfoCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf");
const theme = useTheme();
const settingsValid = ref(true);
const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
@@ -130,10 +134,7 @@ const downloadCalibBoard = async () => {
charucoImage.src = CharucoImage;
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, { maxWidth: (paperWidth - 2.0) / 2, align: "right" });
break;
}
@@ -216,48 +217,60 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<template>
<div>
<v-card class="mb-3" color="primary" dark>
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title>Camera Calibration</v-card-title>
<v-card-text>
<div v-show="!isCalibrating">
<v-card-subtitle class="pt-0 pl-0 pr-0 text-white">Current Calibration</v-card-subtitle>
<v-table fixed-header height="100%" density="compact">
<thead>
<tr>
<th>Resolution</th>
<th>Mean Error</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
<th>Info</th>
</tr>
</thead>
<tbody style="cursor: pointer">
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
<v-tooltip location="bottom">
<template #activator="{ props }">
<td v-bind="props" @click="setSelectedVideoFormat(value)">
<v-icon size="small">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
</v-tooltip>
</tr>
</tbody>
</v-table>
</div>
<v-card-text v-if="!isCalibrating" class="pb-0">
<v-card-subtitle class="pa-0 pb-3 text-white">Current Calibrations</v-card-subtitle>
<v-table fixed-header height="100%" density="compact">
<thead>
<tr>
<th>Resolution</th>
<th>Mean Error</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
<th>Info</th>
</tr>
</thead>
<tbody style="cursor: pointer">
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
<v-tooltip location="bottom">
<template #activator="{ props }">
<td v-bind="props" @click="setSelectedVideoFormat(value)">
<v-icon size="small" color="primary">mdi-information</v-icon>
</td>
</template>
<span>View calibration information</span>
</v-tooltip>
</tr>
</tbody>
</v-table>
</v-card-text>
<v-card-text class="pt-0">
<div v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column">
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<v-alert
closable
density="compact"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
:text="
useSettingsStore().general.mrCalWorking
? 'Mrcal was successfully loaded and will be used!'
: 'MrCal failed to load, check journalctl logs for details.'
"
/>
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
@@ -268,7 +281,15 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-if="boardType !== CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
@@ -279,15 +300,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-show="boardType == CalibrationBoardTypes.Charuco"
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
tooltip="Dictionary of aruco markers on the charuco board"
@@ -304,7 +317,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:label-cols="4"
/>
<pv-number-input
v-show="boardType == CalibrationBoardTypes.Charuco"
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
@@ -329,35 +342,13 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:label-cols="4"
/>
<pv-switch
v-show="boardType == CalibrationBoardTypes.Charuco"
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
:label-cols="4"
/>
<div class="pb-5 pt-10px">
<v-banner
v-if="useSettingsStore().general.mrCalWorking"
rounded
bg-color="secondary"
color="secondary"
text-color="white"
icon="mdi-alert-circle-outline"
>
Mrcal was successfully loaded and will be used!
</v-banner>
<v-banner
v-else
rounded
bg-color="error"
color="error"
text-color="white"
icon="mdi-alert-circle-outline"
>
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</div>
</v-form>
</div>
<div v-if="isCalibrating">
@@ -386,7 +377,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="7"
:slider-cols="8"
:step="1"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
@@ -397,7 +388,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Brightness"
:min="0"
:max="100"
:slider-cols="7"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
@@ -409,7 +400,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="7"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
"
@@ -420,7 +411,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
:slider-cols="8"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
@@ -432,43 +423,58 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
:slider-cols="8"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
<v-banner
v-if="tooManyPoints"
rounded
class="mt-3"
color="error"
text-color="white"
icon="mdi-alert-circle-outline"
>
Too many corners. Finish calibration now!
</v-banner>
</div>
<div v-if="isCalibrating" class="d-flex justify-center align-center pt-10px pb-5">
<v-chip
variant="flat"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'grey-darken-2'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</div>
<div class="d-flex">
<div>
<v-btn
color="buttonPassive"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon" size="large"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</div>
<v-alert
v-if="tooManyPoints"
class="mt-5"
color="error"
density="compact"
text="Too many corners. Finish calibration now!"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<div class="d-flex pt-5">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
size="small"
block
color="secondary"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon start class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<v-icon start class="calib-btn-icon" size="large">
{{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }}
</v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
@@ -476,11 +482,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-btn
size="small"
block
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon start class="calib-btn-icon">
<v-icon start class="calib-btn-icon" size="large">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
@@ -489,39 +496,26 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</v-btn>
</v-col>
</div>
<div class="pt-5">
<v-btn
color="accent"
size="small"
block
variant="outlined"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</div>
</v-card-text>
</v-card>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title> Camera Calibration </v-card-title>
<div style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
<v-icon color="primary" size="70"> mdi-cancel </v-icon>
<v-card-text>
Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration process.
Camera calibration has been canceled. The backend is attempting to cleanly cancel the calibration process.
</v-card-text>
</template>
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
<v-progress-circular indeterminate :size="70" :width="8" color="primary" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-icon color="#00ff00" size="70"> mdi-check </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
@@ -563,12 +557,10 @@ th {
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}
tbody :hover td {
background-color: #005281 !important;
cursor: pointer;
}
@@ -584,7 +576,7 @@ th {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,6 +4,9 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
const theme = useTheme();
const props = defineProps<{
videoFormat: VideoFormat;
@@ -88,16 +91,20 @@ const exportCalibrationURL = computed<string>(() =>
const calibrationImageURL = (index: number) =>
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
</script>
<template>
<v-card color="primary" dark>
<v-card color="surface" dark>
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
<v-col cols="12" md="6">
<v-card-title class="pa-0"> Calibration Details </v-card-title>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
<v-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
<v-icon start> mdi-import</v-icon>
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -110,12 +117,13 @@ const calibrationImageURL = (index: number) =>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-6 pr-md-3">
<v-btn
color="secondary"
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportCalibrationPrompt"
>
<v-icon start>mdi-export</v-icon>
<v-icon start size="large">mdi-export</v-icon>
<span>Export</span>
</v-btn>
<a
@@ -130,17 +138,14 @@ const calibrationImageURL = (index: number) =>
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
>
<v-card-text v-if="!currentCalibrationCoeffs">
<v-banner
rounded
bg-color="secondary"
color="secondary"
text-color="white"
<v-alert
class="pt-3 pb-3"
color="primary"
density="compact"
text="The selected video format has not been calibrated."
icon="mdi-alert-circle-outline"
>
The selected video format has not been calibrated.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<v-card-text class="pt-0">
<v-table density="compact" style="width: 100%">
@@ -287,9 +292,6 @@ const calibrationImageURL = (index: number) =>
</template>
<style scoped>
.v-data-table {
background-color: #006492 !important;
}
.snapshot-preview {
max-width: 55%;
}

View File

@@ -2,6 +2,9 @@
import { ref } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
const theme = useTheme();
interface SnapshotMetadata {
snapshotName: string;
@@ -91,22 +94,39 @@ const expanded = ref([]);
</script>
<template>
<v-card style="background-color: #006492">
<v-card color="surface" class="rounded-12">
<v-card-title>Camera Control</v-card-title>
<v-card-text class="pt-0">
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon start class="open-icon"> mdi-folder </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-card-text>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>
<v-divider />
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
There are no snapshots saved
<v-card color="surface" flat>
<v-card-title> Saved Frame Snapshots </v-card-title>
<v-card-text v-if="imgData.length === 0" class="pt-0">
<v-alert
color="buttonPassive"
density="compact"
text="There are currently no saved snapshots."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<v-card-text v-else>
<v-card-text v-else class="pt-0">
<v-alert
closable
color="buttonPassive"
density="compact"
text="Snapshot timestamps depend on when the coprocessor was last connected to the internet."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-data-table
v-model:expanded="expanded"
:headers="[
@@ -151,10 +171,6 @@ const expanded = ref([]);
</div>
</template>
</v-data-table>
<span
>Snapshot Timestamps may be incorrect as they depend on when the coprocessor was last connected to the
internet</span
>
</v-card-text>
</v-card>
</v-dialog>
@@ -170,18 +186,12 @@ const expanded = ref([]);
}
.v-table {
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #005281 !important;
font-size: 1rem !important;
}
tbody :hover tr {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -194,7 +204,7 @@ const expanded = ref([]);
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,9 +4,12 @@ import PvInput from "@/components/common/pv-input.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";
import { useTheme } from "vuetify";
const theme = useTheme();
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
@@ -73,10 +76,7 @@ const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
@@ -112,22 +112,13 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const address = inject<string>("backendHost");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
const yesDeleteMySettingsText = ref("");
const deletingCamera = ref(false);
const deleteThisCamera = () => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: useStateStore().currentCameraUniqueName
};
const payload = { cameraUniqueName: useStateStore().currentCameraUniqueName };
axios
.post("/utils/nukeOneCamera", payload)
@@ -169,7 +160,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card class="mb-3" color="primary" dark>
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title class="pb-0">Camera Settings</v-card-title>
<v-card-text class="pt-3">
<pv-select
@@ -204,43 +195,40 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-card-text>
<v-card-text class="d-flex pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn block size="small" color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon start> mdi-content-save </v-icon>
<v-btn
block
size="small"
color="primary"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="saveCameraSettings"
>
<v-icon start size="large"> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn block size="small" color="error" @click="() => (showDeleteCamera = true)">
<v-icon start> mdi-trash-can-outline </v-icon>
<v-btn
block
size="small"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showDeleteCamera = true)"
>
<v-icon start size="large"> mdi-trash-can-outline </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" width="800">
<v-card color="primary" flat>
<v-card color="surface" flat>
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
<v-card-text class="pt-0 pb-10px">
<v-row class="align-center">
<v-col cols="12" md="6">
<span class="text-white"> 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 start 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://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
Are you sure you want to delete "{{ useCameraSettingsStore().currentCameraSettings.nickname }}"? This cannot
be undone.
</v-card-text>
<v-card-text class="pt-0 pb-0">
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
@@ -248,20 +236,28 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:input-cols="6"
/>
</v-card-text>
<v-card-text class="pt-10px">
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="showDeleteCamera = false"
>
Cancel
</v-btn>
<v-btn
block
color="error"
:disabled="
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="deletingCamera"
@click="deleteThisCamera"
>
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-text>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>

View File

@@ -5,6 +5,9 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const value = defineModel<number[]>({ required: true });
@@ -29,7 +32,12 @@ const fpsTooLow = computed<boolean>(() => {
</script>
<template>
<v-card id="camera-settings-camera-view-card" class="camera-settings-camera-view-card" color="primary" dark>
<v-card
id="camera-settings-camera-view-card"
class="camera-settings-camera-view-card rounded-12"
color="surface"
dark
>
<v-card-title class="justify-space-between align-content-center pt-0 pb-0">
<div class="d-flex flex-wrap align-center pt-4 pb-4">
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
@@ -39,8 +47,11 @@ const fpsTooLow = computed<boolean>(() => {
:color="fpsTooLow ? 'error' : 'transparent'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1" :style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }">
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
<span
class="pr-1"
:style="{ color: fpsTooLow ? 'rgb(var(--v-theme-error))' : 'rgb(var(--v-theme-primary))' }"
>
&nbsp;{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
@@ -52,7 +63,7 @@ const fpsTooLow = computed<boolean>(() => {
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin-left: auto"
color="accent"
color="primary"
density="compact"
hide-details="auto"
/>
@@ -88,19 +99,21 @@ const fpsTooLow = computed<boolean>(() => {
base-color="surface-variant"
>
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>

View File

@@ -38,8 +38,9 @@ const handleKeydown = ({ key }) => {
break;
}
};
</script>
// TODO: fix error text theming
</script>
<template>
<div class="d-flex">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0 pt-10px pb-10px">
@@ -50,13 +51,12 @@ const handleKeydown = ({ key }) => {
<v-text-field
v-model="value"
density="compact"
color="accent"
color="primary"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
hide-details="auto"
class="light-error"
variant="underlined"
@keydown="handleKeydown"
/>

View File

@@ -38,7 +38,7 @@ const localValue = computed({
density="compact"
hide-details
single-line
color="accent"
color="primary"
type="number"
variant="underlined"
style="width: 70px"

View File

@@ -30,7 +30,7 @@ withDefaults(
v-for="(radioName, index) in list"
:key="index"
:value="index"
color="#ffd843"
color="rgb(var(--v-theme-primary))"
:label="radioName"
:model-value="index"
:disabled="disabled"

View File

@@ -67,15 +67,15 @@ const checkNumberRange = (v: string): boolean => {
:disabled="disabled"
hide-details
class="align-center ml-0 mr-0"
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
color="primary"
:track-color="inverted ? 'primary' : undefined"
thumb-color="primary"
:step="step"
>
<template #prepend>
<v-text-field
:model-value="localValue[0]"
color="accent"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details
@@ -93,7 +93,7 @@ const checkNumberRange = (v: string): boolean => {
<template #append>
<v-text-field
:model-value="localValue[1]"
color="accent"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details

View File

@@ -13,15 +13,9 @@ const props = withDefaults(
disabled?: boolean;
sliderCols?: number;
}>(),
{
step: 1,
disabled: false,
sliderCols: 8
}
{ step: 1, disabled: false, sliderCols: 8 }
);
const emit = defineEmits<{
(e: "update:modelValue", value: number): void;
}>();
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: any[]) => void, wait: number) {
@@ -54,7 +48,7 @@ const localValue = computed({
:max="max"
:min="min"
hide-details
color="accent"
color="primary"
:disabled="disabled"
:step="step"
append-icon="mdi-menu-right"
@@ -66,7 +60,7 @@ const localValue = computed({
<v-col :cols="1" class="pr-0 pt-10px pb-10px">
<v-text-field
:model-value="localValue"
color="accent"
color="primary"
:max="max"
:min="min"
:disabled="disabled"

View File

@@ -3,18 +3,8 @@ import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<boolean>();
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
}>(),
{
disabled: false,
labelCols: 2,
switchCols: 8
}
defineProps<{ label?: string; tooltip?: string; disabled?: boolean; labelCols?: number; switchCols?: number }>(),
{ disabled: false, labelCols: 2, switchCols: 8 }
);
</script>
@@ -24,7 +14,7 @@ withDefaults(
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
<v-switch v-model="value" :disabled="disabled" color="#ffd843" hide-details density="compact" />
<v-switch v-model="value" :disabled="disabled" color="primary" hide-details density="compact" />
</v-col>
</div>
</template>

View File

@@ -8,6 +8,9 @@ import PvIcon from "@/components/common/pv-icon.vue";
import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
@@ -53,10 +56,7 @@ const saveCameraNameEdit = (newName: string) => {
useCameraSettingsStore()
.changeCameraNickname(newName, false)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch((error) => {
@@ -241,7 +241,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card color="primary">
<v-card color="surface" class="rounded-12">
<v-row no-gutters class="pl-4 pt-2 pb-0">
<v-col cols="10" class="pa-0">
<pv-select
@@ -326,19 +326,24 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCreatePipelineDialog">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
<pv-icon color="green" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<pv-icon color="red-darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
<pv-icon
color="red-darken-2"
:right="true"
icon-name="mdi-trash-can-outline"
tooltip="Delete pipeline"
/>
</v-list-item-title>
</v-list-item>
</v-list>
@@ -370,7 +375,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-col>
</v-row>
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
<v-card color="primary">
<v-card color="surface">
<v-card-title class="pb-0"> Create New Pipeline </v-card-title>
<v-card-text class="pt-0 pb-0">
<pv-input
@@ -391,42 +396,52 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-card-text>
<v-card-actions class="pr-5 pt-10px pb-5">
<v-btn
color="#ffd843"
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="checkPipelineName(newPipelineName) !== true"
variant="flat"
@click="createNewPipeline"
>
Save
Create
</v-btn>
<v-btn color="error" variant="elevated" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
<v-card color="primary">
<v-card-title class="pb-0">Pipeline Deletion Confirmation</v-card-title>
<v-card color="surface">
<v-card-title class="pb-0">Delete Pipeline</v-card-title>
<v-card-text>
Are you sure you want to delete the pipeline
<b style="color: white; font-weight: bold">{{
useCameraSettingsStore().currentPipelineSettings.pipelineNickname
}}</b
>? This cannot be undone.
Are you sure you want to delete
<span style="color: white">"{{ useCameraSettingsStore().currentPipelineSettings.pipelineNickname }}"</span>?
This cannot be undone.
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn variant="flat" color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn
variant="flat"
color="#ffd843"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="showPipelineDeletionConfirmationDialog = false"
>
No, take me back
Cancel
</v-btn>
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="confirmDeleteCurrentPipeline"
>
Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title class="pb-0">Change Pipeline Type</v-card-title>
<v-card-text>
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
@@ -434,9 +449,20 @@ const wrappedCameras = computed<SelectItem[]>(() =>
settings.
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn color="error" variant="elevated" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" variant="elevated" class="text-black" @click="cancelChangePipelineType">
No, take me back
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="text-black"
@click="cancelChangePipelineType"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="confirmChangePipelineType"
>
Confirm
</v-btn>
</v-card-actions>
</v-card>

View File

@@ -39,19 +39,17 @@ const performanceRecommendation = computed<string>(() => {
</script>
<template>
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
<v-card color="surface" height="100%" class="d-flex flex-column rounded-12" dark>
<v-card-title class="justify-space-between align-center pt-1 pb-1 d-flex">
<span>Cameras</span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : ''"
style="font-size: 1rem; padding: 0; margin: 0"
:variant="fpsTooLow ? 'tonal' : 'text'"
:style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }"
:color="fpsTooLow ? 'error' : 'primary'"
style="font-size: 1.1rem; padding: 0; margin: 0"
variant="text"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
<span class="pr-1">{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
<v-chip v-else label variant="text" color="red" style="font-size: 1rem; padding: 0; margin: 0">
@@ -61,7 +59,7 @@ const performanceRecommendation = computed<string>(() => {
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
color="accent"
color="primary"
hide-details="auto"
/>
</v-card-title>

View File

@@ -15,6 +15,9 @@ import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
const theme = useTheme();
interface ConfigOption {
tabName: string;
@@ -22,46 +25,16 @@ interface ConfigOption {
}
const allTabs = Object.freeze({
inputTab: {
tabName: "Input",
component: InputTab
},
thresholdTab: {
tabName: "Threshold",
component: ThresholdTab
},
contoursTab: {
tabName: "Contours",
component: ContoursTab
},
apriltagTab: {
tabName: "AprilTag",
component: AprilTagTab
},
arucoTab: {
tabName: "Aruco",
component: ArucoTab
},
objectDetectionTab: {
tabName: "Object Detection",
component: ObjectDetectionTab
},
outputTab: {
tabName: "Output",
component: OutputTab
},
targetsTab: {
tabName: "Targets",
component: TargetsTab
},
pnpTab: {
tabName: "PnP",
component: PnPTab
},
map3dTab: {
tabName: "3D",
component: Map3DTab
}
inputTab: { tabName: "Input", component: InputTab },
thresholdTab: { tabName: "Threshold", component: ThresholdTab },
contoursTab: { tabName: "Contours", component: ContoursTab },
apriltagTab: { tabName: "AprilTag", component: AprilTagTab },
arucoTab: { tabName: "Aruco", component: ArucoTab },
objectDetectionTab: { tabName: "Object Detection", component: ObjectDetectionTab },
outputTab: { tabName: "Output", component: OutputTab },
targetsTab: { tabName: "Targets", component: TargetsTab },
pnpTab: { tabName: "PnP", component: PnPTab },
map3dTab: { tabName: "3D", component: Map3DTab }
});
const selectedTabs = ref([0, 0, 0, 0]);
@@ -144,13 +117,13 @@ const onBeforeTabUpdate = () => {
<template>
<v-row no-gutters class="tabGroups">
<template v-if="!useCameraSettingsStore().hasConnected">
<v-col cols="12">
<v-card color="error">
<v-card-title class="text-white">
Camera has not connected. Please check your connection and try again.
</v-card-title>
</v-card>
</v-col>
<v-alert
color="error"
density="compact"
text="Camera is not connected. Please check your connection and try again."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</template>
<template v-else>
<v-col
@@ -160,8 +133,8 @@ const onBeforeTabUpdate = () => {
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>
<v-card color="primary" height="100%" class="pr-5 pl-5">
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="primary" height="48" slider-color="accent">
<v-card color="surface" height="100%" class="pr-5 pl-5 rounded-12">
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="surface" height="48" slider-color="buttonActive">
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>

View File

@@ -2,6 +2,9 @@
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const value = defineModel<number[]>();
@@ -18,8 +21,8 @@ const processingMode = computed<number>({
<template>
<v-card
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
class="mt-3"
color="primary"
class="mt-3 rounded-12"
color="surface"
style="flex-grow: 1; display: flex; flex-direction: column"
>
<v-row class="pa-3 pb-0 align-center">
@@ -27,21 +30,27 @@ const processingMode = computed<number>({
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory base-color="surface-variant" class="fill w-100">
<v-btn
color="secondary"
color="buttonPassive"
:disabled="!useCameraSettingsStore().hasConnected"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
prepend-icon="mdi-square-outline"
>
<template #prepend>
<v-icon size="large">mdi-square-outline</v-icon>
</template>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
:disabled="
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
prepend-icon="mdi-cube-outline"
>
<template #prepend>
<v-icon size="large">mdi-cube-outline</v-icon>
</template>
<span>3D</span>
</v-btn>
</v-btn-toggle>
@@ -51,12 +60,20 @@ const processingMode = computed<number>({
<v-col class="pa-4 pt-0">
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="value" :multiple="true" mandatory base-color="surface-variant" class="fill w-100">
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>

View File

@@ -7,6 +7,9 @@ import { computed } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const isTagPipeline = computed(
() =>
@@ -159,8 +162,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -170,7 +174,8 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="yellow-darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -185,8 +190,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -196,8 +202,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -207,7 +214,8 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="yellow-darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -238,6 +246,6 @@ const interactiveCols = computed(() =>
.metric-item-title {
font-size: 18px;
text-decoration: underline;
text-decoration-color: #ffd843;
text-decoration-color: rgb(var(--v-theme-primary));
}
</style>

View File

@@ -4,6 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
import { useStateStore } from "@/stores/StateStore";
import { angleModulus, toDeg } from "@/lib/MathUtils";
import { computed } from "vue";
import { useTheme } from "vuetify";
const theme = useTheme();
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -200,7 +203,12 @@ const resetCurrentBuffer = () => {
>Multi-tag pose standard deviation over the last
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
</v-card-subtitle>
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" variant="flat" @click="resetCurrentBuffer"
<v-btn
color="buttonActive"
class="mb-4 mt-1"
style="width: min-content"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="resetCurrentBuffer"
>Reset Samples</v-btn
>
<v-table density="compact">
@@ -274,7 +282,6 @@ th {
padding-right: 8px !important;
}
.v-table {
background-color: #006492 !important;
width: 100%;
font-size: 1rem !important;
@@ -287,11 +294,6 @@ th {
}
}
tbody {
:hover {
td {
background-color: #005281 !important;
}
}
tr {
td {
padding: 0 !important;
@@ -313,7 +315,7 @@ th {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -6,6 +6,9 @@ import PvSwitch from "@/components/common/pv-switch.vue";
import { useStateStore } from "@/stores/StateStore";
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
@@ -186,17 +189,25 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon start> mdi-minus </v-icon>
<v-icon start size="large"> mdi-minus </v-icon>
Shrink Range
</v-btn>
</v-col>
<v-col cols="4" class="pl-0 pr-0">
<v-btn color="accent" class="text-black" size="small" block @click="enableColorPicking(1)">
<v-icon start> mdi-plus-minus </v-icon>
<v-btn
color="primary"
class="text-black"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(1)"
>
<v-icon start size="large"> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
</v-col>
@@ -204,18 +215,28 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon start> mdi-plus </v-icon>
<v-icon start size="large"> mdi-plus </v-icon>
Expand Range
</v-btn>
</v-col>
</template>
<template v-else>
<v-card-text class="pa-0 pt-3 pb-3">
<v-btn block color="accent" class="text-black" size="small" @click="disableColorPicking"> Cancel </v-btn>
<v-btn
block
color="primary"
class="text-black"
size="small"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="disableColorPicking"
>
Cancel
</v-btn>
</v-card-text>
</template>
</div>

View File

@@ -8,23 +8,19 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
const quat = new ThreeQuat(rot_quat.X, rot_quat.Y, rot_quat.Z, rot_quat.W);
const euler = new Euler().setFromQuaternion(quat, "ZYX");
return {
x: toDeg(euler.x),
y: toDeg(euler.y),
z: toDeg(euler.z)
};
return { x: toDeg(euler.x), y: toDeg(euler.y), z: toDeg(euler.z) };
};
</script>
<template>
<v-card style="background-color: #006492">
<v-card color="surface" class="rounded-12">
<v-card-title>AprilTag Field Layout</v-card-title>
<v-card-text class="pt-0">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-table fixed-header height="100%" density="compact" dark>
<v-table fixed-header height="100%" density="compact">
<template #default>
<thead style="font-size: 1.25rem">
<tr>
@@ -57,11 +53,9 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
}
@@ -70,10 +64,6 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -86,7 +76,7 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,15 +4,15 @@ import { useStateStore } from "@/stores/StateStore";
import PvSelect from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import axios from "axios";
import { useTheme } from "vuetify";
const theme = useTheme();
const restartProgram = () => {
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
useStateStore().showSnackbarMessage({ message: "Successfully sent program restart request", color: "success" });
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
@@ -98,10 +98,7 @@ const handleOfflineUpdate = () => {
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -170,14 +167,9 @@ const handleSettingsImport = () => {
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.post(`/settings${settingsEndpoint}`, formData, { headers: { "Content-Type": "multipart/form-data" } })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -238,35 +230,50 @@ const nukePhotonConfigDirectory = () => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title>Device Control</v-card-title>
<div class="pa-5 pt-0">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartProgram">
<v-icon start class="open-icon"> mdi-restart </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartProgram"
>
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartDevice">
<v-icon start class="open-icon"> mdi-restart-alert </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartDevice"
>
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon start class="open-icon"> mdi-upload </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openOfflineUpdatePrompt"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
<span class="open-label">Offline Update</span>
</v-btn>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon start class="open-icon"> mdi-import </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
<v-dialog
@@ -279,7 +286,7 @@ const nukePhotonConfigDirectory = () => {
}
"
>
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
@@ -299,14 +306,19 @@ const nukePhotonConfigDirectory = () => {
style="width: 100%"
/>
<v-file-input
class="pb-5"
v-model="importFile"
class="pb-5"
variant="underlined"
:disabled="importType === undefined"
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
/>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-btn
color="primary"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleSettingsImport"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
@@ -316,8 +328,12 @@ const nukePhotonConfigDirectory = () => {
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Export Settings</span>
</v-btn>
<a
@@ -329,8 +345,12 @@ const nukePhotonConfigDirectory = () => {
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon start class="open-icon"> mdi-download </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportLogsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
<span class="open-label">Download logs</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -344,36 +364,36 @@ const nukePhotonConfigDirectory = () => {
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon start class="open-icon"> mdi-eye </v-icon>
<span class="open-label">View program logs</span>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useStateStore().showLogModal = true"
>
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
<span class="open-label">View logs</span>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12">
<v-btn color="error" @click="() => (showFactoryReset = true)">
<v-icon start class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.display.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING"
: "Factory Reset PhotonVision"
}}
</span>
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showFactoryReset = true)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-icon"> Factory Reset PhotonVision </span>
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card color="primary" flat>
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
<span class="open-label">
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Factory Reset PhotonVision
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
</span>
</v-card-title>
<v-card-text class="pt-0 pb-10px">
@@ -382,8 +402,13 @@ const nukePhotonConfigDirectory = () => {
<span> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<v-btn
color="primary"
style="float: right"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
@@ -407,10 +432,11 @@ const nukePhotonConfigDirectory = () => {
<v-card-text class="pt-10px">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>

View File

@@ -4,7 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title class="pb-10px">LED Control</v-card-title>
<v-card-text>
<pv-slider

View File

@@ -10,30 +10,14 @@ interface MetricItem {
const generalMetrics = computed<MetricItem[]>(() => {
const stats = [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
{ header: "Version", value: useSettingsStore().general.version || "Unknown" },
{ header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" },
{ header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" },
{ header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "Unknown" }
];
if (!useSettingsStore().network.networkingDisabled) {
stats.push({
header: "IP Address",
value: useSettingsStore().metrics.ipAddress || "Unknown"
});
stats.push({ header: "IP Address", value: useSettingsStore().metrics.ipAddress || "Unknown" });
}
return stats;
@@ -141,16 +125,16 @@ onBeforeMount(() => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title style="display: flex; justify-content: space-between">
<span>Stats</span>
<span>Metrics</span>
<v-btn variant="text" @click="fetchMetrics">
<v-icon start class="open-icon">mdi-reload</v-icon>
<v-icon start class="open-icon" size="large">mdi-reload</v-icon>
Last Fetched: {{ metricsLastFetched }}
</v-btn>
</v-card-title>
<v-card-text class="pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
<v-card-subtitle class="pa-0" style="font-size: 16px">General</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
@@ -187,7 +171,7 @@ onBeforeMount(() => {
</v-table>
</v-card-text>
<v-card-text class="pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
@@ -233,46 +217,52 @@ onBeforeMount(() => {
text-align: center;
}
$stats-table-border: rgba(255, 255, 255, 0.5);
$stats-table-inner: rgba(255, 255, 255, 0.1);
.t {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
}
.b {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
}
.tl {
border-top: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-left-radius: 5px;
}
.tr {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-right-radius: 5px;
}
.bl {
border-bottom: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-left-radius: 5px;
}
.br {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-right-radius: 5px;
}
.metric-item {
font-size: 16px !important;
padding: 1px 15px 1px 10px;
border-right: 1px solid;
border-right: 1px solid $stats-table-border;
font-weight: normal;
color: white !important;
text-align: center !important;
@@ -280,22 +270,9 @@ onBeforeMount(() => {
.metric-item-title {
font-size: 18px !important;
text-decoration: underline;
text-decoration-color: #ffd843;
}
.v-table {
thead,
tbody {
background-color: #006492;
}
:hover {
tbody > tr {
background-color: #005281 !important;
}
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -308,7 +285,7 @@ onBeforeMount(() => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -7,6 +7,9 @@ import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
const theme = useTheme();
// Copy object to remove reference to store
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
@@ -83,16 +86,10 @@ const saveGeneralSettings = () => {
useSettingsStore()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
})
.catch((error) => {
resetTempSettingsStruct();
@@ -141,7 +138,7 @@ watchEffect(() => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title>Global Settings</v-card-title>
<div class="pa-5 pt-0">
<v-divider class="pb-2" />
@@ -159,16 +156,15 @@ watchEffect(() => {
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
<v-alert
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
bg-color="error"
text-color="white"
style="margin: 10px 0"
class="pt-3 pb-3"
color="error"
density="compact"
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
icon="mdi-alert-circle-outline"
>
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.connectionType"
@@ -230,35 +226,34 @@ watchEffect(() => {
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
/>
<v-banner
<v-alert
v-if="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
bg-color="error"
text-color="white"
icon="mdi-information-outline"
>
Photon cannot detect any wired connections! Please send program logs to the developers for help.
</v-banner>
class="pt-3 pb-3"
color="error"
density="compact"
text="Cannot detect any wired connections! Send program logs to the developers for help."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
label="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.runNTServer"
rounded
bg-color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-divider class="mt-10px pb-2" />
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
<pv-switch
@@ -267,21 +262,19 @@ watchEffect(() => {
tooltip="If enabled, Photon will publish all pipeline results in both the Packet and Protobuf formats. This is useful for visualizing pipeline results from NT viewers such as glass and logging software such as AdvantageScope. Note: photon-lib will ignore this value and is not recommended on the field for performance."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.shouldPublishProto"
rounded
bg-color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-divider class="mt-10px pb-5" />
</v-form>
<v-btn
color="accent"
:variant="!settingsValid || !settingsHaveChanged() ? 'tonal' : 'elevated'"
color="primary"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
@@ -296,7 +289,4 @@ watchEffect(() => {
.mt-10px {
margin-top: 10px !important;
}
.v-banner__wrapper {
padding: 6px !important;
}
</style>

View File

@@ -5,6 +5,9 @@ import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
import pvInput from "@/components/common/pv-input.vue";
import { useTheme } from "vuetify";
const theme = useTheme();
const showImportDialog = ref(false);
const showInfo = ref({ show: false, model: {} as ObjectDetectionModelProperties });
@@ -18,7 +21,7 @@ const showRenameDialog = ref({
const address = inject<string>("backendHost");
const importModelFile = ref<File | null>(null);
const importLabels = ref<String | null>(null);
const importLabels = ref<string | null>(null);
const importHeight = ref<number | null>(null);
const importWidth = ref<number | null>(null);
const importVersion = ref<string | null>(null);
@@ -220,7 +223,7 @@ const handleBulkImport = () => {
formData.append("data", importFile.value);
axios
.post(`/objectdetection/bulkimport`, formData, {
.post("/objectdetection/bulkimport", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
@@ -270,12 +273,17 @@ const handleBulkImport = () => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card class="mb-3" color="surface">
<v-card-title>Object Detection</v-card-title>
<div class="pa-5 pt-0">
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" class="justify-center" @click="() => (showImportDialog = true)">
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Model</span>
</v-btn>
@@ -292,7 +300,7 @@ const handleBulkImport = () => {
}
"
>
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import New Object Detection Model</v-card-title>
<v-card-text>
Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
@@ -316,7 +324,7 @@ const handleBulkImport = () => {
:items="['YOLOv5', 'YOLOv8', 'YOLO11']"
/>
<v-btn
color="secondary"
color="buttonActive"
width="100%"
:disabled="
importModelFile === null ||
@@ -325,9 +333,10 @@ const handleBulkImport = () => {
importHeight === null ||
importVersion === null
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleImport()"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Import Object Detection Model</span>
</v-btn>
</div>
@@ -336,20 +345,31 @@ const handleBulkImport = () => {
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" class="justify-center" @click="() => (showBulkImportDialog = true)">
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showBulkImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Bulk Import</span>
</v-btn>
<v-dialog v-model="showBulkImportDialog" width="600">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import Multiple Object Detection Models</v-card-title>
<v-card-text>
Upload a zip file containing multiple object detection models to this device. Note this zip file should
only come from a previous export of object detection models.
<div class="pa-5 pb-0">
<v-file-input v-model="importFile" variant="underlined" label="Zip File" accept=".zip" />
<v-btn color="secondary" width="100%" :disabled="importFile === null" @click="handleBulkImport()">
<v-icon start class="open-icon"> mdi-import </v-icon>
<v-btn
color="buttonActive"
width="100%"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleBulkImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Bulk Import</span>
</v-btn>
</div>
@@ -358,7 +378,11 @@ const handleBulkImport = () => {
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportPrompt">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportPrompt"
>
<v-icon start class="open-icon"> mdi-export </v-icon>
<span class="open-label">Export Models</span>
</v-btn>
@@ -371,7 +395,11 @@ const handleBulkImport = () => {
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="error" @click="() => (showNukeDialog = true)">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showNukeDialog = true)"
>
<v-icon left class="open-icon"> mdi-trash </v-icon>
<span class="open-label">Clear and reset models</span>
</v-btn>
@@ -398,45 +426,66 @@ const handleBulkImport = () => {
icon
small
color="error"
@click="() => (confirmDeleteDialog = { show: true, model })"
title="Delete Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (confirmDeleteDialog = { show: true, model })"
>
<v-icon>mdi-delete</v-icon>
<v-icon size="large">mdi-trash-can-outline</v-icon>
</v-btn>
</td>
<td class="text-right">
<v-btn
icon
small
color="primary"
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
color="buttonActive"
title="Rename Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
>
<v-icon>mdi-pencil</v-icon>
<v-icon size="large">mdi-pencil</v-icon>
</v-btn>
</td>
<td class="text-right">
<v-btn icon small color="info" @click="() => (showInfo = { show: true, model })">
<v-icon>mdi-information</v-icon>
<v-btn
icon
small
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showInfo = { show: true, model })"
>
<v-icon size="large">mdi-information</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
<v-dialog v-model="confirmDeleteDialog.show" width="600">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title>Delete Object Detection Model</v-card-title>
<v-card-text class="pt-0">
Are you sure you want to delete the model {{ confirmDeleteDialog.model.nickname }}?
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn variant="elevated" color="error" @click="deleteModel(confirmDeleteDialog.model)">Delete</v-btn>
<v-btn variant="elevated" @click="confirmDeleteDialog.show = false" color="secondary">Cancel</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonPassive"
@click="confirmDeleteDialog.show = false"
>
Cancel
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="error"
@click="deleteModel(confirmDeleteDialog.model)"
>
Delete
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="showRenameDialog.show" width="600">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title>Rename Object Detection Model</v-card-title>
<v-card-text class="pt-0">
Enter a new name for the model {{ showRenameDialog.model.nickname }}:
@@ -445,22 +494,32 @@ const handleBulkImport = () => {
</div>
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn
variant="elevated"
color="secondary"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="error"
@click="showRenameDialog.show = false"
>Cancel</v-btn
>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonActive"
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
>Rename</v-btn
>
<v-btn variant="elevated" @click="showRenameDialog.show = false" color="error">Cancel</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="showInfo.show" width="600">
<v-card color="primary" dark>
<v-card color="surface" dark>
<v-card-title>Object Detection Model Info</v-card-title>
<v-card-text class="pt-0">
<v-btn color="secondary" width="100%" @click="openExportIndividualModelPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<v-btn
color="buttonPassive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportIndividualModelPrompt"
>
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Export Model</span>
</v-btn>
<a
@@ -486,12 +545,12 @@ const handleBulkImport = () => {
</div>
<v-dialog v-model="showNukeDialog" width="800" dark>
<v-card color="primary" flat>
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
<span class="open-label">
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Clear and Reset Object Detection Models
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
</span>
</v-card-title>
<v-card-text class="pt-0 pb-10px">
@@ -500,8 +559,13 @@ const handleBulkImport = () => {
<span> This will delete ALL OF YOUR MODELS and re-extract the default models. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<v-btn
color="buttonActive"
style="float: right"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Backup Models</span>
<a
ref="exportModels"
@@ -527,9 +591,10 @@ const handleBulkImport = () => {
color="error"
width="100%"
:disabled="yesDeleteMyModelsText.toLowerCase() !== expected.toLowerCase()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="nukeModels"
>
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{ $vuetify.display.mdAndUp ? "Delete models, I have backed up what I need" : "Delete Models" }}
</span>
@@ -561,11 +626,9 @@ const handleBulkImport = () => {
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
text-align: center !important;
@@ -575,10 +638,6 @@ const handleBulkImport = () => {
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -591,7 +650,7 @@ const handleBulkImport = () => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -3,54 +3,71 @@ import("@mdi/font/css/materialdesignicons.css");
import type { ThemeDefinition } from "vuetify/lib/composables/theme";
import { createVuetify } from "vuetify";
const commonColors = {
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
const CommonColors = {
photonBlue: "#006492",
photonYellow: "#FFD843",
lightBlue: "#39A4D5",
darkGray: "#151515",
gray: "#1c232c",
lightGray: "#232C37"
};
const DarkTheme: ThemeDefinition = {
dark: true,
colors: {
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
...commonColors
background: CommonColors.darkGray,
sidebar: CommonColors.darkGray,
surface: CommonColors.gray,
primary: CommonColors.lightBlue,
secondary: CommonColors.photonYellow,
accent: CommonColors.photonBlue,
toggle: CommonColors.photonBlue,
logsBackground: CommonColors.darkGray,
buttonActive: CommonColors.photonYellow,
buttonPassive: CommonColors.lightBlue,
"surface-variant": "#485b70",
"on-surface-variant": "#f0f0f0",
error: "#ff2e2e",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
}
};
const LightTheme: ThemeDefinition = {
dark: false,
colors: {
background: "#232C37",
primary: "#006492",
surface: "#006492",
secondary: "#39A4D5",
background: CommonColors.lightGray,
sidebar: CommonColors.photonBlue,
surface: CommonColors.photonBlue,
primary: CommonColors.photonYellow,
secondary: CommonColors.lightBlue,
accent: CommonColors.photonYellow,
toggle: CommonColors.lightBlue,
logsBackground: CommonColors.lightGray,
buttonActive: CommonColors.photonYellow,
buttonPassive: CommonColors.lightBlue,
"surface-variant": "#358AB0",
accent: "#FFD843",
"surface-light": "#FFD843",
...commonColors
"surface-light": CommonColors.photonYellow,
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
},
variables: {
"medium-emphasis-opacity": 1,
"high-emphasis-opacity": 1
}
variables: { "medium-emphasis-opacity": 1, "high-emphasis-opacity": 1 }
};
export default createVuetify({
theme: {
defaultTheme: "LightTheme",
themes: {
LightTheme: LightTheme,
DarkTheme: DarkTheme
}
},
display: {
thresholds: {
md: 1460,
lg: 2000
}
}
theme: { defaultTheme: "LightTheme", themes: { LightTheme: LightTheme, DarkTheme: DarkTheme } },
display: { thresholds: { md: 1460, lg: 2000 } }
});

View File

@@ -17,9 +17,11 @@ import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
import axios from "axios";
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
import { useTheme } from "vuetify";
const theme = useTheme();
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const host = inject<string>("backendHost");
const activatingModule = ref(false);
const activateModule = (moduleUniqueName: string) => {
@@ -97,7 +99,6 @@ const deactivatingModule = ref(false);
const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
axios
.post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName })
.then(() => {
@@ -273,10 +274,6 @@ const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettin
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
</script>
<template>
@@ -291,7 +288,7 @@ const openExportSettingsPrompt = () => {
lg="4"
class="pr-0"
>
<v-card color="primary">
<v-card color="surface" class="rounded-12">
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
@@ -307,12 +304,24 @@ const openExportSettingsPrompt = () => {
<v-card-text class="pt-3">
<v-table density="compact">
<tbody>
<tr>
<td>Streams:</td>
<tr
v-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
<td style="width: 50%">Frames Processed</td>
<td>
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
/
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
useStateStore().backendResults[module.uniqueName].fps
}}
FPS)
</td>
</tr>
<tr v-else>
<td>Name</td>
<td>
{{ module.nickname }}
</td>
</tr>
<tr>
@@ -328,18 +337,12 @@ const openExportSettingsPrompt = () => {
}}
</td>
</tr>
<tr
v-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
<td style="width: 50%">Frames Processed</td>
<tr>
<td>Streams:</td>
<td>
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
useStateStore().backendResults[module.uniqueName].fps
}}
FPS)
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
/
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
</td>
</tr>
</tbody>
@@ -361,8 +364,9 @@ const openExportSettingsPrompt = () => {
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn
color="secondary"
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
@@ -376,8 +380,9 @@ const openExportSettingsPrompt = () => {
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="deactivatingModule"
@click="deactivateModule(module.uniqueName)"
>
@@ -385,8 +390,14 @@ const openExportSettingsPrompt = () => {
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
<v-btn
class="pa-0"
color="error"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
</v-row>
@@ -394,7 +405,7 @@ const openExportSettingsPrompt = () => {
</v-card>
</v-col>
<!-- Disabled modules -->
<!-- Deactivated modules -->
<v-col
v-for="module in disabledVisionModules"
:key="`disabled-${module.uniqueName}`"
@@ -403,8 +414,8 @@ const openExportSettingsPrompt = () => {
lg="4"
class="pr-0"
>
<v-card class="pr-0" color="primary">
<v-card-title>{{ module.nickname }}</v-card-title>
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>{{ module.cameraQuirks.baseName }}</v-card-title>
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
<v-card-text class="pt-3">
<v-table density="compact">
@@ -412,17 +423,13 @@ const openExportSettingsPrompt = () => {
<tr>
<td>Name</td>
<td>
{{ module.cameraQuirks.baseName }}
{{ module.nickname }}
</td>
</tr>
<tr>
<td>Pipelines</td>
<td>{{ module.pipelineNicknames.join(", ") }}</td>
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
<tr>
<td>Calibrations</td>
<td>
@@ -432,6 +439,10 @@ const openExportSettingsPrompt = () => {
}}
</td>
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
</tbody>
</v-table>
</v-card-text>
@@ -439,8 +450,9 @@ const openExportSettingsPrompt = () => {
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn
color="secondary"
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
@@ -454,8 +466,9 @@ const openExportSettingsPrompt = () => {
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="activatingModule"
@click="activateModule(module.uniqueName)"
>
@@ -463,8 +476,14 @@ const openExportSettingsPrompt = () => {
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
<v-btn
class="pa-0"
color="error"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
</v-row>
@@ -474,7 +493,7 @@ const openExportSettingsPrompt = () => {
<!-- Unassigned cameras -->
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
<v-card class="pr-0" color="primary">
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
@@ -489,16 +508,22 @@ const openExportSettingsPrompt = () => {
<v-card-text class="pt-0">
<v-row>
<v-col cols="6" class="pr-0">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(camera, false)">
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraView(camera, false)"
>
<span>Details</span>
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:loading="assigningCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="assignCamera(camera)"
>
Activate
@@ -517,7 +542,7 @@ const openExportSettingsPrompt = () => {
class="pl-6 pr-6 d-flex flex-column justify-center"
style="background-color: transparent; height: 100%"
>
<v-card-text class="d-flex flex-column align-center justify-center">
<v-card-text class="d-flex flex-column align-center justify-center" style="flex-grow: 0">
<v-icon size="64" color="primary">mdi-plus</v-icon>
</v-card-text>
<v-card-title>Additional plugged in cameras will display here!</v-card-title>
@@ -527,21 +552,25 @@ const openExportSettingsPrompt = () => {
<!-- Camera details modal -->
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="primary">
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
<v-btn variant="text" @click="setCameraView(null, null)">
<v-icon>mdi-close-thick</v-icon>
<v-icon size="x-large">mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="!viewingCamera[1]">
<PvCameraInfoCard :camera="viewingCamera[0]" />
</v-card-text>
<v-card-text v-else-if="!camerasMatch(getMatchedDevice(viewingCamera[0]), viewingCamera[0])">
<v-banner rounded bg-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>
<v-alert
class="mb-3"
color="buttonActive"
density="compact"
text="A different camera may have been connected to this device! Compare the following information carefully."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
</v-card-text>
<v-card-text v-else>
@@ -552,29 +581,12 @@ const openExportSettingsPrompt = () => {
<!-- Camera delete modal -->
<v-dialog v-model="viewingDeleteCamera" width="800">
<v-card v-if="cameraToDelete !== null" class="dialog-container" color="primary" flat>
<v-card v-if="cameraToDelete !== null" class="dialog-container" color="surface" flat>
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
<v-card-text class="pb-10px">
<v-row class="align-center">
<v-col cols="12" md="6">
<span class="text-white"> 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 start 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>
Are you sure you want to delete "{{ cameraToDelete.nickname }}"? This cannot be undone.
</v-card-text>
<v-card-text class="pt-0 pb-0">
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + cameraToDelete.nickname + '&quot;:'"
@@ -582,18 +594,26 @@ const openExportSettingsPrompt = () => {
:input-cols="6"
/>
</v-card-text>
<v-card-text class="pt-10px">
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="cameraToDelete = null"
>
Cancel
</v-btn>
<v-btn
block
color="error"
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
:loading="deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="deleteThisCamera(cameraToDelete.uniqueName)"
>
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-text>
</v-card-actions>
</v-card>
</v-dialog>
</div>
@@ -614,10 +634,6 @@ td {
text-wrap-mode: wrap !important;
}
.v-table {
background-color: #006492 !important;
}
.active-status {
color: rgb(14, 240, 14);
background-color: transparent;
@@ -631,7 +647,6 @@ td {
}
a:hover {
color: pink;
background-color: transparent;
text-decoration: underline;
}

View File

@@ -7,6 +7,9 @@ import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const cameraViewType = computed<number[]>({
get: (): number[] => {
@@ -64,46 +67,43 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
<template>
<v-container class="pa-3" fluid>
<v-banner
<v-alert
v-if="arducamWarningShown"
rounded
bg-color="error"
color="error"
dark
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
<span>
Arducam camera detected! Please configure the camera model in the <a href="#/cameras">Camera tab</a>!
</span>
</v-banner>
<v-banner
</v-alert>
<v-alert
v-if="conflictingHostnameShown"
rounded
bg-color="error"
color="error"
dark
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span
>Conflicting Hostname Detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
<span>
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
</span>
</v-banner>
<v-banner
</v-alert>
<v-alert
v-if="conflictingCameraShown"
rounded
bg-color="error"
color="error"
dark
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span
>Conflicting Camera Name(s) Detected! Please change the name(s) of
>Conflicting camera name(s) detected! Please change the name(s) of
{{ useSettingsStore().general.conflictingCameras }}!
</span>
</v-banner>
</v-alert>
<v-row no-gutters>
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
<CamerasCard v-model="cameraViewType" />
@@ -122,11 +122,11 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
max-width="800"
dark
>
<v-card flat color="primary">
<v-card-title>Setup some cameras to get started!</v-card-title>
<v-card flat color="surface">
<v-card-title>Set up some cameras to get started!</v-card-title>
<v-card-text class="pt-0">
No cameras activated - head to the <router-link to="/cameraConfigs">Camera matching tab</router-link> to set
some up!
No cameras activated - head to the
<router-link to="/cameraConfigs" color="buttonActive">camera matching tab</router-link> to set some up!
</v-card-text>
</v-card>
</v-dialog>
@@ -135,23 +135,19 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
<style scoped>
a:link {
color: #ffd843;
background-color: transparent;
text-decoration: none;
}
a:visited {
color: #ffd843;
color: rgb(var(--v-theme-buttonActive));
background-color: transparent;
text-decoration: none;
}
a:hover {
color: pink;
background-color: transparent;
text-decoration: underline;
}
a:active {
color: yellow;
background-color: transparent;
text-decoration: none;
}

View File

@@ -1,14 +1,23 @@
<script setup lang="ts">
const devMode = process.env.NODE_ENV === "development";
</script>
<template>
<div style="overflow: hidden; height: 100vh; width: 100%">
<div v-if="devMode" style="width: 100%; height: 100%; padding: 16px">
<span style="color: white; font-weight: bold">
PhotonClient is in development mode so the documentation page will not load. Please recompile in production mode
with the documentation copied over after a full build.
</span>
<div v-if="devMode" style="width: 60%; height: 100%; margin: auto">
<v-card
dark
flat
class="pl-6 pr-6 d-flex flex-column justify-center align-center"
style="background-color: transparent; height: 100%"
>
<v-card-text class="d-flex flex-column" style="flex: 0">
<v-icon size="64" color="primary">mdi-web-off</v-icon>
</v-card-text>
<v-card-text style="width: 100%; flex-grow: 0; text-align: center">
PhotonClient is in development mode so the documentation page will not load. Please recompile in production
mode with the documentation copied over after a full build.
</v-card-text>
</v-card>
</div>
<div v-else style="width: 100%; height: 100%">
<!--suppress HtmlUnknownTarget -->