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

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;
}
}