Skip to content

Commit fe304df

Browse files
committed
recordings frontend interface
1 parent 17b8348 commit fe304df

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
<script setup lang="ts">
2+
import { ref, inject, computed } from "vue";
3+
import pvInput from "@/components/common/pv-input.vue";
4+
import { useTheme } from "vuetify";
5+
import { axiosPost } from "@/lib/PhotonUtils";
6+
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
7+
import PvSelect from "@/components/common/pv-select.vue";
8+
9+
const theme = useTheme();
10+
11+
const address = inject<string>("backendHost");
12+
13+
// Initialize selected recordings for each camera
14+
const selectedRecordings = ref<Record<string, string | undefined>>({});
15+
16+
const camerasWithRecordings = computed(() => {
17+
const cameras = useCameraSettingsStore().camerasWithRecordings;
18+
// Initialize selectedRecordings for any new cameras
19+
cameras.forEach((camera) => {
20+
if (!(camera.uniqueName in selectedRecordings.value) && camera.recordings.length > 0) {
21+
selectedRecordings.value[camera.uniqueName] = camera.recordings[0];
22+
}
23+
});
24+
console.log(selectedRecordings);
25+
return cameras;
26+
});
27+
28+
const confirmDeleteDialog = ref({ show: false, recordings: {} as string[], cameraUniqueName: "" });
29+
30+
const deleteRecordings = async (recordingsToDelete: string[], cameraUniqueName: string) => {
31+
axiosPost("/recordings/delete", "delete " + recordingsToDelete.join(", "), {
32+
recordings: recordingsToDelete,
33+
cameraUniqueName: cameraUniqueName
34+
});
35+
36+
confirmDeleteDialog.value.show = false;
37+
};
38+
39+
const exportRecordings = ref();
40+
const exportCameraRecordings = ref();
41+
const exportIndividualRecording = ref();
42+
43+
const showNukeDialog = ref(false);
44+
const expected = "Delete Recordings";
45+
const yesDeleteMyRecordingsText = ref("");
46+
const nukeRecordings = () => {
47+
axiosPost("/recordings/nuke", "clear and reset all recordings");
48+
showNukeDialog.value = false;
49+
};
50+
</script>
51+
52+
<template>
53+
<v-card class="mb-3" color="surface">
54+
<v-card-title>Recordings</v-card-title>
55+
<div class="pa-5 pt-0">
56+
<v-row>
57+
<v-col cols="12" sm="6">
58+
<v-btn
59+
color="buttonPassive"
60+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
61+
@click="() => exportRecordings.value.click()"
62+
>
63+
<v-icon start class="open-icon"> mdi-export </v-icon>
64+
<span class="open-label">Export Recordings</span>
65+
</v-btn>
66+
<a
67+
ref="exportRecordings"
68+
style="color: black; text-decoration: none; display: none"
69+
:href="`http://${address}/api/recordings/export`"
70+
download="photonvision-recordings-export.zip"
71+
target="_blank"
72+
/>
73+
</v-col>
74+
<v-col cols="12" sm="6">
75+
<v-btn
76+
color="error"
77+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
78+
@click="() => (showNukeDialog = true)"
79+
>
80+
<v-icon left class="open-icon"> mdi-trash </v-icon>
81+
<span class="open-label">Clear all recordings</span>
82+
</v-btn>
83+
</v-col>
84+
</v-row>
85+
<v-row>
86+
<v-col cols="">
87+
<v-table fixed-header height="100%" density="compact" dark>
88+
<thead style="font-size: 1.25rem">
89+
<tr>
90+
<th>Camera</th>
91+
<th>Selected Recording</th>
92+
<th>Delete Selected</th>
93+
<th>Export Selected</th>
94+
<th>Delete All</th>
95+
<th>Export All</th>
96+
</tr>
97+
</thead>
98+
<tbody>
99+
<tr v-for="camera in camerasWithRecordings" :key="camera.uniqueName">
100+
<td>{{ camera.nickname }}</td>
101+
<td>
102+
<pv-select
103+
v-model="selectedRecordings[camera.uniqueName]"
104+
:items="camera.recordings"
105+
:select-cols="8"
106+
/>
107+
</td>
108+
<td class="text-right">
109+
<v-btn
110+
icon
111+
small
112+
color="error"
113+
title="Delete Selected Recording"
114+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
115+
@click="
116+
() =>
117+
(confirmDeleteDialog = {
118+
show: true,
119+
recordings: [selectedRecordings[camera.uniqueName] || ''],
120+
cameraUniqueName: camera.uniqueName
121+
})
122+
"
123+
>
124+
<v-icon size="large">mdi-trash-can-outline</v-icon>
125+
</v-btn>
126+
</td>
127+
<td class="text-right">
128+
<v-btn
129+
icon
130+
small
131+
color="buttonPassive"
132+
title="Export Selected Recording"
133+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
134+
@click="() => exportIndividualRecording.value.click()"
135+
>
136+
<v-icon size="large">mdi-export</v-icon>
137+
</v-btn>
138+
<a
139+
ref="exportIndividualRecording"
140+
style="color: black; text-decoration: none; display: none"
141+
:href="`http://${address}/api/recordings/exportIndividual?recording=${selectedRecordings[camera.uniqueName]}?camera=${camera.uniqueName}`"
142+
:download="`${camera.nickname}_${camera.recordings[0].slice(camera.recordings[0].lastIndexOf('/'))}_recording.zip`"
143+
target="_blank"
144+
/>
145+
</td>
146+
<td class="text-right">
147+
<v-btn
148+
icon
149+
small
150+
color="error"
151+
title="Delete Recordings"
152+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
153+
@click="
154+
() =>
155+
(confirmDeleteDialog = {
156+
show: true,
157+
recordings: camera.recordings,
158+
cameraUniqueName: camera.uniqueName
159+
})
160+
"
161+
>
162+
<v-icon size="large">mdi-trash-can-outline</v-icon>
163+
</v-btn>
164+
</td>
165+
<td class="text-right">
166+
<v-btn
167+
icon
168+
small
169+
color="buttonPassive"
170+
title="Export Recordings"
171+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
172+
@click="() => exportCameraRecordings.value.click()"
173+
>
174+
<v-icon size="large">mdi-export</v-icon>
175+
</v-btn>
176+
<a
177+
ref="exportCameraRecordings"
178+
style="color: black; text-decoration: none; display: none"
179+
:href="`http://${address}/api/recordings/exportCamera?cameraPath=${camera.uniqueName}`"
180+
:download="`${camera.nickname}_recordings.zip`"
181+
target="_blank"
182+
/>
183+
</td>
184+
</tr>
185+
</tbody>
186+
</v-table>
187+
188+
<v-dialog v-model="confirmDeleteDialog.show" width="600">
189+
<v-card color="surface" dark>
190+
<v-card-title>Delete Recording</v-card-title>
191+
<v-card-text class="pt-0">
192+
Are you sure you want to delete the recording(s) {{ confirmDeleteDialog.recordings.join(", ") }}?
193+
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
194+
<v-btn
195+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
196+
color="buttonPassive"
197+
@click="confirmDeleteDialog.show = false"
198+
>
199+
Cancel
200+
</v-btn>
201+
<v-btn
202+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
203+
color="error"
204+
@click="deleteRecordings(confirmDeleteDialog.recordings, confirmDeleteDialog.cameraUniqueName)"
205+
>
206+
Delete
207+
</v-btn>
208+
</v-card-actions>
209+
</v-card-text>
210+
</v-card>
211+
</v-dialog>
212+
</v-col>
213+
</v-row>
214+
</div>
215+
216+
<v-dialog v-model="showNukeDialog" width="800" dark>
217+
<v-card color="surface" flat>
218+
<v-card-title style="display: flex; justify-content: center">
219+
<span class="open-label">
220+
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
221+
Delete All Recordings
222+
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
223+
</span>
224+
</v-card-title>
225+
<v-card-text class="pt-0 pb-10px">
226+
<v-row class="align-center text-white">
227+
<v-col cols="12" md="6">
228+
<span> This will delete ALL OF YOUR RECORDINGS. </span>
229+
</v-col>
230+
<v-col cols="12" md="6">
231+
<v-btn
232+
color="buttonActive"
233+
style="float: right"
234+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
235+
@click="() => exportRecordings.click()"
236+
>
237+
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
238+
<span class="open-label">Backup Recordings</span>
239+
</v-btn>
240+
</v-col>
241+
</v-row>
242+
</v-card-text>
243+
<v-card-text class="pt-0 pb-0">
244+
<pv-input
245+
v-model="yesDeleteMyRecordingsText"
246+
:label="'Type &quot;' + expected + '&quot;:'"
247+
:label-cols="6"
248+
:input-cols="6"
249+
/>
250+
</v-card-text>
251+
<v-card-text class="pt-10px">
252+
<v-btn
253+
color="error"
254+
width="100%"
255+
:disabled="yesDeleteMyRecordingsText.toLowerCase() !== expected.toLowerCase()"
256+
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
257+
@click="nukeRecordings"
258+
>
259+
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
260+
<span class="open-label">
261+
{{ $vuetify.display.mdAndUp ? "Delete recordings, I have backed up what I need" : "Delete Recordings" }}
262+
</span>
263+
</v-btn>
264+
</v-card-text>
265+
</v-card>
266+
</v-dialog>
267+
</v-card>
268+
</template>
269+
270+
<style scoped lang="scss">
271+
.v-col-12 > .v-btn {
272+
width: 100%;
273+
}
274+
275+
.pt-10px {
276+
padding-top: 10px !important;
277+
}
278+
279+
@media only screen and (max-width: 351px) {
280+
.open-icon {
281+
margin: 0 !important;
282+
}
283+
.open-label {
284+
display: none;
285+
}
286+
}
287+
.v-table {
288+
width: 100%;
289+
height: 100%;
290+
text-align: center;
291+
292+
th,
293+
td {
294+
font-size: 1rem !important;
295+
color: white !important;
296+
text-align: center !important;
297+
}
298+
299+
td {
300+
font-family: monospace !important;
301+
}
302+
303+
::-webkit-scrollbar {
304+
width: 0;
305+
height: 0.55em;
306+
border-radius: 5px;
307+
}
308+
309+
::-webkit-scrollbar-track {
310+
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
311+
border-radius: 10px;
312+
}
313+
314+
::-webkit-scrollbar-thumb {
315+
background-color: rgb(var(--v-theme-accent));
316+
border-radius: 10px;
317+
}
318+
}
319+
</style>

photon-client/src/stores/settings/CameraSettingsStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
9696
},
9797
hasConnected(): boolean {
9898
return this.currentCameraSettings.hasConnected;
99+
},
100+
camerasWithRecordings(): UiCameraConfiguration[] {
101+
return Object.values(this.cameras).filter((camera) => camera.recordings.length > 0);
99102
}
100103
},
101104
actions: {

photon-client/src/views/GeneralSettingsView.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import NetworkingCard from "@/components/settings/NetworkingCard.vue";
66
import LightingControlCard from "@/components/settings/LEDControlCard.vue";
77
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
88
import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
9+
import RecordingsCard from "@/components/settings/RecordingsCard.vue";
10+
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
911
</script>
1012

1113
<template>
@@ -15,6 +17,7 @@ import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
1517
<NetworkingCard />
1618
<ObjectDetectionCard v-if="useSettingsStore().general.supportedBackends.length > 0" />
1719
<LightingControlCard v-if="useSettingsStore().lighting.supported" />
20+
<RecordingsCard v-if="useCameraSettingsStore().camerasWithRecordings.length > 0" />
1821
<Suspense>
1922
<!-- Allows us to import three js when it's actually needed -->
2023
<ApriltagControlCard />

0 commit comments

Comments
 (0)