mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-29 02:21:41 +00:00
UI patches (#905)
- Show 0 clients when NT server props are undefined - Add Prettier --------- Co-authored-by: Matthew Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import { TrackballControls } from "three/examples/jsm/controls/TrackballControls
|
||||
import { type Object3D } from "three";
|
||||
|
||||
const props = defineProps<{
|
||||
targets: PhotonTarget[]
|
||||
targets: PhotonTarget[];
|
||||
}>();
|
||||
|
||||
let scene: Scene | undefined;
|
||||
@@ -29,25 +29,20 @@ let controls: TrackballControls | undefined;
|
||||
let previousTargets: Object3D[] = [];
|
||||
const drawTargets = (targets: PhotonTarget[]) => {
|
||||
// Check here, since if we check in watchEffect this never gets called
|
||||
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
scene.remove(...previousTargets);
|
||||
previousTargets = [];
|
||||
|
||||
targets.forEach(target => {
|
||||
if(target.pose === undefined) return;
|
||||
targets.forEach((target) => {
|
||||
if (target.pose === undefined) return;
|
||||
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
|
||||
const quaternion = new Quaternion(
|
||||
target.pose.qx,
|
||||
target.pose.qy,
|
||||
target.pose.qz,
|
||||
target.pose.qw
|
||||
);
|
||||
const quaternion = new Quaternion(target.pose.qx, target.pose.qy, target.pose.qz, target.pose.qw);
|
||||
|
||||
const cube = new Mesh(geometry, material);
|
||||
cube.position.set(target.pose.x, target.pose.y, target.pose.z);
|
||||
@@ -72,7 +67,7 @@ const drawTargets = (targets: PhotonTarget[]) => {
|
||||
previousTargets.push(arrow);
|
||||
});
|
||||
|
||||
if(previousTargets.length > 0) {
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
@@ -80,7 +75,7 @@ const onWindowResize = () => {
|
||||
const container = document.getElementById("container");
|
||||
const canvas = document.getElementById("view");
|
||||
|
||||
if(container === null || canvas === null || camera === undefined || renderer === undefined) {
|
||||
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,7 +86,7 @@ const onWindowResize = () => {
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
||||
};
|
||||
const resetCamFirstPerson = () => {
|
||||
if(scene === undefined || camera === undefined || controls === undefined) {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,12 +95,12 @@ const resetCamFirstPerson = () => {
|
||||
camera.up.set(0, 0, 1);
|
||||
controls.target.set(4.0, 0.0, 0.0);
|
||||
controls.update();
|
||||
if(previousTargets.length > 0) {
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
const resetCamThirdPerson = () => {
|
||||
if(scene === undefined || camera === undefined || controls === undefined) {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,7 +109,7 @@ const resetCamThirdPerson = () => {
|
||||
camera.up.set(0, 0, 1);
|
||||
controls.target.set(4.0, 0.0, 0.0);
|
||||
controls.update();
|
||||
if(previousTargets.length > 0) {
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
@@ -124,7 +119,7 @@ onMounted(() => {
|
||||
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
|
||||
const canvas = document.getElementById("view");
|
||||
if(canvas === null) return;
|
||||
if (canvas === null) return;
|
||||
renderer = new WebGLRenderer({ canvas: canvas });
|
||||
|
||||
scene.background = new Color(0xa9a9a9);
|
||||
@@ -133,14 +128,20 @@ onMounted(() => {
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
|
||||
const referenceFrameCues: Object3D[] = [];
|
||||
referenceFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1));
|
||||
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1));
|
||||
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1));
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1)
|
||||
);
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1)
|
||||
);
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1)
|
||||
);
|
||||
|
||||
// Draw the Camera Body
|
||||
const camSize = 0.2;
|
||||
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
|
||||
const camLensGeometry = new ConeGeometry(camSize*0.4, camSize*0.8, 30);
|
||||
const camLensGeometry = new ConeGeometry(camSize * 0.4, camSize * 0.8, 30);
|
||||
const camMaterial = new MeshNormalMaterial();
|
||||
const camBody = new Mesh(camBodyGeometry, camMaterial);
|
||||
const camLens = new Mesh(camLensGeometry, camMaterial);
|
||||
@@ -150,10 +151,7 @@ onMounted(() => {
|
||||
referenceFrameCues.push(camBody);
|
||||
referenceFrameCues.push(camLens);
|
||||
|
||||
controls = new TrackballControls(
|
||||
camera,
|
||||
renderer.domElement
|
||||
);
|
||||
controls = new TrackballControls(camera, renderer.domElement);
|
||||
controls.rotateSpeed = 1.0;
|
||||
controls.zoomSpeed = 1.2;
|
||||
controls.panSpeed = 0.8;
|
||||
@@ -168,7 +166,7 @@ onMounted(() => {
|
||||
controls.update();
|
||||
|
||||
const animate = () => {
|
||||
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,41 +183,23 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
});
|
||||
watchEffect(() => {
|
||||
drawTargets(props.targets);
|
||||
drawTargets(props.targets);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="container"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div id="container" style="width: 100%">
|
||||
<v-row>
|
||||
<v-col
|
||||
align-self="stretch"
|
||||
style="display: flex; justify-content: center"
|
||||
>
|
||||
<canvas
|
||||
id="view"
|
||||
/>
|
||||
<v-col align-self="stretch" style="display: flex; justify-content: center">
|
||||
<canvas id="view" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="margin-bottom: 24px">
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
</v-btn>
|
||||
<v-btn color="secondary" @click="resetCamFirstPerson"> First Person </v-btn>
|
||||
</v-col>
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
</v-btn>
|
||||
<v-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,15 @@ import loadingImage from "@/assets/images/loading.svg";
|
||||
import type { StyleValue } from "vue/types/jsx";
|
||||
|
||||
const props = defineProps<{
|
||||
streamType: "Raw" | "Processed",
|
||||
id?: string
|
||||
streamType: "Raw" | "Processed";
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const src = computed<string>(() => {
|
||||
const port = useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
const port =
|
||||
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
|
||||
if(!useStateStore().backendConnected || port === 0) {
|
||||
if (!useStateStore().backendConnected || port === 0) {
|
||||
return loadingImage;
|
||||
}
|
||||
|
||||
@@ -22,9 +23,9 @@ const src = computed<string>(() => {
|
||||
const alt = computed<string>(() => `${props.streamType} Stream View`);
|
||||
|
||||
const style = computed<StyleValue>(() => {
|
||||
if(useStateStore().colorPickingMode) {
|
||||
if (useStateStore().colorPickingMode) {
|
||||
return { cursor: "crosshair" };
|
||||
} else if(src.value !== loadingImage) {
|
||||
} else if (src.value !== loadingImage) {
|
||||
return { cursor: "pointer" };
|
||||
}
|
||||
|
||||
@@ -32,19 +33,12 @@ const style = computed<StyleValue>(() => {
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if(!useStateStore().colorPickingMode && src.value !== loadingImage) {
|
||||
if (!useStateStore().colorPickingMode && src.value !== loadingImage) {
|
||||
window.open(src.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
:id="id"
|
||||
crossorigin="anonymous"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:style="style"
|
||||
@click="handleClick"
|
||||
>
|
||||
<img :id="id" crossorigin="anonymous" :src="src" :alt="alt" :style="style" @click="handleClick" />
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
|
||||
|
||||
const logs = computed<LogMessage[]>(() => useStateStore().logMessages.filter(message => selectedLogLevels.value.includes(message.level)));
|
||||
const logs = computed<LogMessage[]>(() =>
|
||||
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
|
||||
);
|
||||
|
||||
const backendHost = inject<string>("backendHost");
|
||||
|
||||
@@ -33,38 +35,22 @@ const handleLogExport = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
useStateStore().$patch(state => state.showLogModal = !state.showLogModal);
|
||||
useStateStore().$patch((state) => (state.showLogModal = !state.showLogModal));
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="useStateStore().showLogModal"
|
||||
width="1500"
|
||||
dark
|
||||
>
|
||||
<v-card
|
||||
dark
|
||||
class="pt-3"
|
||||
color="primary"
|
||||
flat
|
||||
>
|
||||
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
|
||||
<v-card dark class="pt-3" color="primary" flat>
|
||||
<v-card-title>
|
||||
View Program Logs
|
||||
<v-btn
|
||||
color="secondary"
|
||||
style="margin-left: auto;"
|
||||
depressed
|
||||
@click="handleLogExport"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
<v-btn color="secondary" style="margin-left: auto" depressed @click="handleLogExport">
|
||||
<v-icon left> mdi-download </v-icon>
|
||||
Download Current Log
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
@@ -79,34 +65,16 @@ document.addEventListener("keydown", e => {
|
||||
</v-card-title>
|
||||
|
||||
<div class="pr-6 pl-6">
|
||||
<v-btn-toggle
|
||||
v-model="selectedLogLevels"
|
||||
dark
|
||||
multiple
|
||||
class="fill mb-4"
|
||||
>
|
||||
<v-btn
|
||||
v-for="(level) in [0, 1, 2, 3]"
|
||||
:key="level"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-btn-toggle v-model="selectedLogLevels" dark multiple class="fill mb-4">
|
||||
<v-btn v-for="level in [0, 1, 2, 3]" :key="level" color="secondary" class="fill">
|
||||
{{ getLogLevelFromIndex(level) }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-card-text
|
||||
v-if="logs.length === 0"
|
||||
style="font-size: 18px; font-weight: 600"
|
||||
>
|
||||
<v-card-text v-if="logs.length === 0" style="font-size: 18px; font-weight: 600">
|
||||
There are no Logs to show
|
||||
</v-card-text>
|
||||
<v-virtual-scroll
|
||||
v-else
|
||||
:items="logs"
|
||||
item-height="50"
|
||||
height="600"
|
||||
>
|
||||
<template #default="{item}">
|
||||
<v-virtual-scroll v-else :items="logs" item-height="50" height="600">
|
||||
<template #default="{ item }">
|
||||
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
|
||||
{{ item.message }}
|
||||
</div>
|
||||
@@ -118,13 +86,7 @@ document.addEventListener("keydown", e => {
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="() => useStateStore().showLogModal = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -4,8 +4,12 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => { return useStateStore().sidebarFolded; },
|
||||
set: (val) => { useStateStore().setSidebarFolded(val); }
|
||||
get: () => {
|
||||
return useStateStore().sidebarFolded;
|
||||
},
|
||||
set: (val) => {
|
||||
useStateStore().setSidebarFolded(val);
|
||||
}
|
||||
});
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
@@ -13,39 +17,17 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
dark
|
||||
app
|
||||
permanent
|
||||
:mini-variant="compact || !mdAndUp"
|
||||
color="primary"
|
||||
>
|
||||
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
|
||||
<v-list>
|
||||
<!-- 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="(compact || !mdAndUp) ? 'pr-0 pl-0' : ''"
|
||||
style="display: flex; justify-content: center"
|
||||
>
|
||||
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img
|
||||
v-if="!(compact || !mdAndUp)"
|
||||
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-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
to="/dashboard"
|
||||
>
|
||||
<v-list-item link to="/dashboard">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -53,11 +35,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
ref="camerasTabOpener"
|
||||
link
|
||||
to="/cameras"
|
||||
>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -65,10 +43,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/settings"
|
||||
>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -76,10 +51,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/docs"
|
||||
>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -87,46 +59,27 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="mdAndUp"
|
||||
link
|
||||
@click="() => compact = !compact"
|
||||
>
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
mdi-chevron-left
|
||||
</v-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0;">
|
||||
<div style="position: absolute; bottom: 0; left: 0">
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer">
|
||||
mdi-server
|
||||
</v-icon>
|
||||
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected">
|
||||
mdi-robot
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
style="border-radius: 100%"
|
||||
>
|
||||
mdi-robot-off
|
||||
</v-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
|
||||
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-if="useSettingsStore().network.runNTServer"
|
||||
class="text-wrap"
|
||||
>
|
||||
NetworkTables server running for <span class="accent--text">{{ useStateStore().ntConnectionStatus.clients }}</span> clients
|
||||
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
|
||||
NetworkTables server running for
|
||||
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
@@ -134,17 +87,11 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span
|
||||
class="accent--text"
|
||||
>
|
||||
<span class="accent--text">
|
||||
{{ useStateStore().ntConnectionStatus.address }}
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
@@ -152,15 +99,8 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useStateStore().backendConnected">
|
||||
mdi-server-network
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
style="border-radius: 100%;"
|
||||
>
|
||||
mdi-server-network-off
|
||||
</v-icon>
|
||||
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="text-wrap">
|
||||
|
||||
Reference in New Issue
Block a user