This commit is contained in:
2026-03-08 11:29:16 -04:00
parent f40a5c551f
commit 1a8857f65a
5 changed files with 215 additions and 135 deletions

View File

@@ -26,12 +26,15 @@
/* Main app container */ /* Main app container */
.app-container { .app-container {
display: flex;
flex-direction: column;
height: 100vh; height: 100vh;
width: 95vw; width: 95vw;
max-width: 95vw; max-width: 95vw;
margin: 0 auto; margin: 0 auto;
padding: 0; padding: 0;
gap: 0; gap: 0;
overflow: hidden;
} }
/* Controls section */ /* Controls section */
@@ -62,8 +65,7 @@
/* Timeline container */ /* Timeline container */
.timeline-section { .timeline-section {
width: 100%; width: 100%;
height: 30vh; min-height: 60px;
min-height: 200px;
background: #20232a; background: #20232a;
border-radius: 10px; border-radius: 10px;
margin: 0 auto; margin: 0 auto;
@@ -72,14 +74,15 @@
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 12px; margin-bottom: 12px;
padding: 0; padding: 0;
overflow: hidden;
transition: flex 0.15s ease;
} }
/* Video player section */ /* Video player section */
.video-section { .video-section {
flex: 1 1 0;
min-height: 0;
width: 100%; width: 100%;
height: 60vw;
margin-top: 12px;
min-height: 40vw;
background: #23272f; background: #23272f;
border-radius: 10px; border-radius: 10px;
margin: 0 auto; margin: 0 auto;
@@ -88,6 +91,8 @@
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 12px; margin-bottom: 12px;
padding: 0; padding: 0;
overflow: hidden;
transition: flex 0.15s ease;
} }
/* Date range picker styles */ /* Date range picker styles */

View File

@@ -37,8 +37,19 @@ function App() {
); );
const [endRange, setEndRange] = useState(new Date()); const [endRange, setEndRange] = useState(new Date());
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000)); // const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
const [queryText, setQueryText] = useState("Two clouded leopards being aggressive"); const [queryText, setQueryText] = useState(
"Two clouded leopards being aggressive",
);
const [sliderValue, setSliderValue] = useState(0); const [sliderValue, setSliderValue] = useState(0);
const [videoFlex, setVideoFlex] = useState(5.8); // ratio: video vs timeline
const handleVideoWheel = useCallback((e) => {
e.preventDefault();
setVideoFlex((prev) => {
const delta = e.deltaY > 0 ? 0.3 : -0.3;
return Math.max(3, Math.min(10, Math.max(1, prev + 0.5*delta)))
});
}, []);
// State to track last submitted values // State to track last submitted values
const [lastSubmitted, setLastSubmitted] = useState({ const [lastSubmitted, setLastSubmitted] = useState({
@@ -157,22 +168,24 @@ function App() {
queryText, queryText,
selectedCamera, selectedCamera,
}); });
}; };
function selectHighResFunc(
function selectHighResFunc(selectedHighRes, setSelectedHighRes, toggleCheckbox = true) { selectedHighRes,
setSelectedHighRes,
sendState = false,
) {
console.log(selectedHighRes); console.log(selectedHighRes);
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("do_high_res", !selectedHighRes); if (sendState) {
params.append("do_high_res", selectedHighRes);
} else {
params.append("do_high_res", !selectedHighRes);
}
authenticatedFetch("api/set_parameter?" + params.toString()) authenticatedFetch("api/set_parameter?" + params.toString())
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
if (toggleCheckbox) { if (!sendState) {
setSelectedHighRes(!selectedHighRes); setSelectedHighRes(!selectedHighRes);
} }
}); });
@@ -267,8 +280,8 @@ function App() {
tech.el_.addEventListener("loadstart", () => { tech.el_.addEventListener("loadstart", () => {
// Force range request capability // Force range request capability
if ( if (
tech.el_.seekable && tech.el_.seekable
tech.el_.seekable.length === 0 && tech.el_.seekable.length === 0
) { ) {
console.log( console.log(
"Video doesn't support seeking - range headers may be needed", "Video doesn't support seeking - range headers may be needed",
@@ -339,11 +352,7 @@ function App() {
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
selectHighResFunc( selectHighResFunc(selectedHighRes, setSelectedHighRes, true);
selectedHighRes,
setSelectedHighRes,
false
)
handleResubmit(params.get("test_mode") === "true"); handleResubmit(params.get("test_mode") === "true");
}, []); }, []);
@@ -367,10 +376,9 @@ function App() {
), ),
); );
setSliderMax(max_value); setSliderMax(max_value);
original_data.current = data; original_data.current = data;
setDataResults(data); setDataResults(data);
updateDataAndValue(sliderValue) updateDataAndValue(sliderValue);
} }
}); });
}; };
@@ -601,20 +609,32 @@ function App() {
</div> </div>
)} )}
<div className="controls-section" style={{ <div
display: "flex", className="controls-section"
alignItems: "center", style={{
justifyContent: "center", display: "flex",
gap: "12px", alignItems: "center",
flexWrap: "nowrap", justifyContent: "center",
padding: "12px 16px", gap: "12px",
background: "linear-gradient(135deg, #2a2d3a 0%, #1f2129 100%)", flexWrap: "nowrap",
borderRadius: "12px", padding: videoFlex >= 6 ? "0" : "12px 16px",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", background:
border: "1px solid #3a3f4f", "linear-gradient(135deg, #2a2d3a 0%, #1f2129 100%)",
overflowX: "auto" borderRadius: "12px",
}}> boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}> border: videoFlex >= 6 ? "none" : "1px solid #3a3f4f",
overflowX: "auto",
maxHeight: videoFlex >= 6 ? "0px" : "200px",
overflow: "hidden",
opacity: videoFlex >= 6 ? 0 : 1,
marginBottom: videoFlex >= 6 ? 0 : 12,
transition: "max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease, margin-bottom 0.3s ease",
}}
>
<div
className="control-group"
style={{ margin: 0, flexShrink: 0 }}
>
<button <button
className="drawer-toggle" className="drawer-toggle"
onClick={() => setDrawerOpen(!drawerOpen)} onClick={() => setDrawerOpen(!drawerOpen)}
@@ -622,35 +642,41 @@ function App() {
padding: "8px 12px", padding: "8px 12px",
fontSize: "0.85em", fontSize: "0.85em",
minWidth: "70px", minWidth: "70px",
background: "linear-gradient(135deg, #4a90e2 0%, #357abd 100%)", background:
"linear-gradient(135deg, #4a90e2 0%, #357abd 100%)",
color: "white", color: "white",
border: "none", border: "none",
borderRadius: "6px", borderRadius: "6px",
cursor: "pointer", cursor: "pointer",
fontWeight: "500", fontWeight: "500",
boxShadow: "0 2px 4px rgba(74, 144, 226, 0.3)", boxShadow: "0 2px 4px rgba(74, 144, 226, 0.3)",
transition: "all 0.2s ease" transition: "all 0.2s ease",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.target.style.transform = "translateY(-1px)"; e.target.style.transform = "translateY(-1px)";
e.target.style.boxShadow = "0 4px 8px rgba(74, 144, 226, 0.4)"; e.target.style.boxShadow =
"0 4px 8px rgba(74, 144, 226, 0.4)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.target.style.transform = "translateY(0)"; e.target.style.transform = "translateY(0)";
e.target.style.boxShadow = "0 2px 4px rgba(74, 144, 226, 0.3)"; e.target.style.boxShadow =
"0 2px 4px rgba(74, 144, 226, 0.3)";
}} }}
> >
{drawerOpen ? "✕" : "Options"} {drawerOpen ? "✕" : "Options"}
</button> </button>
</div> </div>
<div className="control-group" style={{ <div
margin: 0, className="control-group"
flexShrink: 0, style={{
background: "rgba(255, 255, 255, 0.05)", margin: 0,
borderRadius: "6px", flexShrink: 0,
border: "1px solid rgba(255, 255, 255, 0.1)" background: "rgba(255, 255, 255, 0.05)",
}}> borderRadius: "6px",
border: "1px solid rgba(255, 255, 255, 0.1)",
}}
>
<CustomDateRangePicker <CustomDateRangePicker
startDate={startRange} startDate={startRange}
endDate={endRange} endDate={endRange}
@@ -659,7 +685,15 @@ function App() {
/> />
</div> </div>
<div className="control-group" style={{ margin: 0, flex: "1 1 180px", minWidth: "180px", maxWidth: "300px" }}> <div
className="control-group"
style={{
margin: 0,
flex: "1 1 180px",
minWidth: "180px",
maxWidth: "300px",
}}
>
<input <input
type="text" type="text"
placeholder="Enter search query..." placeholder="Enter search query..."
@@ -680,33 +714,38 @@ function App() {
fontWeight: "400", fontWeight: "400",
outline: "none", outline: "none",
transition: "all 0.2s ease", transition: "all 0.2s ease",
boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)" boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)",
}} }}
onFocus={(e) => { onFocus={(e) => {
e.target.style.borderColor = "#4a90e2"; e.target.style.borderColor = "#4a90e2";
e.target.style.boxShadow = "inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(74, 144, 226, 0.15)"; e.target.style.boxShadow =
"inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(74, 144, 226, 0.15)";
}} }}
onBlur={(e) => { onBlur={(e) => {
e.target.style.borderColor = "#3a3f4f"; e.target.style.borderColor = "#3a3f4f";
e.target.style.boxShadow = "inset 0 2px 4px rgba(0, 0, 0, 0.1)"; e.target.style.boxShadow =
"inset 0 2px 4px rgba(0, 0, 0, 0.1)";
}} }}
ref={inputRef} ref={inputRef}
/> />
</div> </div>
<div className="control-group" style={{ <div
margin: 0, className="control-group"
display: "flex", style={{
alignItems: "center", margin: 0,
gap: "6px", display: "flex",
color: "#e1e5e9", alignItems: "center",
fontSize: "0.85em", gap: "6px",
background: "rgba(255, 255, 255, 0.05)", color: "#e1e5e9",
padding: "6px 10px", fontSize: "0.85em",
borderRadius: "6px", background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)", padding: "6px 10px",
flexShrink: 0 borderRadius: "6px",
}}> border: "1px solid rgba(255, 255, 255, 0.1)",
flexShrink: 0,
}}
>
<input <input
type="checkbox" type="checkbox"
name="do_high_res" name="do_high_res"
@@ -720,29 +759,35 @@ function App() {
style={{ style={{
width: "14px", width: "14px",
height: "14px", height: "14px",
accentColor: "#4a90e2" accentColor: "#4a90e2",
}} }}
/> />
<span style={{ fontWeight: "500", whiteSpace: "nowrap" }}>HD</span> <span style={{ fontWeight: "500", whiteSpace: "nowrap" }}>
HD
</span>
</div> </div>
<div style={{ <div
display: "flex", style={{
alignItems: "center", display: "flex",
gap: "8px", alignItems: "center",
visibility: queryChanged ? "hidden" : "visible", gap: "8px",
background: "rgba(255, 255, 255, 0.05)", visibility: queryChanged ? "hidden" : "visible",
padding: "6px 12px", background: "rgba(255, 255, 255, 0.05)",
borderRadius: "6px", padding: "6px 12px",
border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "6px",
flexShrink: 0 border: "1px solid rgba(255, 255, 255, 0.1)",
}}> flexShrink: 0,
<label style={{ }}
color: "#e1e5e9", >
fontSize: "0.85em", <label
whiteSpace: "nowrap", style={{
fontWeight: "500" color: "#e1e5e9",
}}> fontSize: "0.85em",
whiteSpace: "nowrap",
fontWeight: "500",
}}
>
Threshold: Threshold:
</label> </label>
<input <input
@@ -755,35 +800,42 @@ function App() {
style={{ style={{
width: "80px", width: "80px",
height: "6px", height: "6px",
background: "linear-gradient(to right, #3a3f4f 0%, #4a90e2 100%)", background:
"linear-gradient(to right, #3a3f4f 0%, #4a90e2 100%)",
borderRadius: "3px", borderRadius: "3px",
outline: "none", outline: "none",
appearance: "none" appearance: "none",
}} }}
/> />
<span style={{ <span
color: "#4a90e2", style={{
fontSize: "0.8em", color: "#4a90e2",
minWidth: "35px", fontSize: "0.8em",
textAlign: "right", minWidth: "35px",
fontWeight: "600", textAlign: "right",
fontFamily: "monospace" fontWeight: "600",
}}> fontFamily: "monospace",
}}
>
{sliderValue.toFixed(2)} {sliderValue.toFixed(2)}
</span> </span>
</div> </div>
<div className="control-group" style={{ <div
margin: 0, className="control-group"
visibility: queryChanged ? "visible" : "hidden", style={{
flexShrink: 0 margin: 0,
}}> visibility: queryChanged ? "visible" : "hidden",
flexShrink: 0,
}}
>
<button <button
onClick={handleResubmit} onClick={handleResubmit}
style={{ style={{
padding: "8px 14px", padding: "8px 14px",
fontSize: "0.85em", fontSize: "0.85em",
background: "linear-gradient(135deg, #28a745 0%, #20a73a 100%)", background:
"linear-gradient(135deg, #28a745 0%, #20a73a 100%)",
color: "white", color: "white",
border: "none", border: "none",
borderRadius: "6px", borderRadius: "6px",
@@ -791,30 +843,39 @@ function App() {
fontWeight: "600", fontWeight: "600",
boxShadow: "0 2px 4px rgba(40, 167, 69, 0.3)", boxShadow: "0 2px 4px rgba(40, 167, 69, 0.3)",
transition: "all 0.2s ease", transition: "all 0.2s ease",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.target.style.transform = "translateY(-1px)"; e.target.style.transform = "translateY(-1px)";
e.target.style.boxShadow = "0 4px 8px rgba(40, 167, 69, 0.4)"; e.target.style.boxShadow =
"0 4px 8px rgba(40, 167, 69, 0.4)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.target.style.transform = "translateY(0)"; e.target.style.transform = "translateY(0)";
e.target.style.boxShadow = "0 2px 4px rgba(40, 167, 69, 0.3)"; e.target.style.boxShadow =
"0 2px 4px rgba(40, 167, 69, 0.3)";
}} }}
> >
🔄 Resubmit Resubmit
</button> </button>
</div> </div>
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}> <div
<button className="control-group"
onClick={videoPlaying ? () => window.open("api/media_download/low") : undefined} style={{ margin: 0, flexShrink: 0 }}
>
<button
onClick={
videoPlaying
? () => window.open("api/media_download/low")
: undefined
}
disabled={!videoPlaying} disabled={!videoPlaying}
style={{ style={{
padding: "6px 10px", padding: "6px 10px",
fontSize: "0.75em", fontSize: "0.75em",
background: videoPlaying background: videoPlaying
? "linear-gradient(135deg, #17c671 0%, #15b565 100%)" ? "linear-gradient(135deg, #17c671 0%, #15b565 100%)"
: "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)", : "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)",
color: videoPlaying ? "white" : "#adb5bd", color: videoPlaying ? "white" : "#adb5bd",
border: "none", border: "none",
@@ -822,37 +883,46 @@ function App() {
cursor: videoPlaying ? "pointer" : "not-allowed", cursor: videoPlaying ? "pointer" : "not-allowed",
opacity: videoPlaying ? 1 : 0.6, opacity: videoPlaying ? 1 : 0.6,
fontWeight: "500", fontWeight: "500",
boxShadow: videoPlaying boxShadow: videoPlaying
? "0 2px 4px rgba(23, 198, 113, 0.3)" ? "0 2px 4px rgba(23, 198, 113, 0.3)"
: "0 1px 2px rgba(108, 117, 125, 0.2)", : "0 1px 2px rgba(108, 117, 125, 0.2)",
transition: "all 0.2s ease", transition: "all 0.2s ease",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (videoPlaying) { if (videoPlaying) {
e.target.style.transform = "translateY(-1px)"; e.target.style.transform = "translateY(-1px)";
e.target.style.boxShadow = "0 4px 8px rgba(23, 198, 113, 0.4)"; e.target.style.boxShadow =
"0 4px 8px rgba(23, 198, 113, 0.4)";
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (videoPlaying) { if (videoPlaying) {
e.target.style.transform = "translateY(0)"; e.target.style.transform = "translateY(0)";
e.target.style.boxShadow = "0 2px 4px rgba(23, 198, 113, 0.3)"; e.target.style.boxShadow =
"0 2px 4px rgba(23, 198, 113, 0.3)";
} }
}} }}
> >
DL Low-Res Video DL Low-Res Video
</button> </button>
</div> </div>
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}> <div
<button className="control-group"
onClick={videoPlaying ? () => window.open("api/media_download/high") : undefined} style={{ margin: 0, flexShrink: 0 }}
>
<button
onClick={
videoPlaying
? () => window.open("api/media_download/high")
: undefined
}
disabled={!videoPlaying} disabled={!videoPlaying}
style={{ style={{
padding: "6px 10px", padding: "6px 10px",
fontSize: "0.75em", fontSize: "0.75em",
background: videoPlaying background: videoPlaying
? "linear-gradient(135deg, #007bff 0%, #0056b3 100%)" ? "linear-gradient(135deg, #007bff 0%, #0056b3 100%)"
: "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)", : "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)",
color: videoPlaying ? "white" : "#adb5bd", color: videoPlaying ? "white" : "#adb5bd",
border: "none", border: "none",
@@ -860,44 +930,47 @@ function App() {
cursor: videoPlaying ? "pointer" : "not-allowed", cursor: videoPlaying ? "pointer" : "not-allowed",
opacity: videoPlaying ? 1 : 0.6, opacity: videoPlaying ? 1 : 0.6,
fontWeight: "500", fontWeight: "500",
boxShadow: videoPlaying boxShadow: videoPlaying
? "0 2px 4px rgba(0, 123, 255, 0.3)" ? "0 2px 4px rgba(0, 123, 255, 0.3)"
: "0 1px 2px rgba(108, 117, 125, 0.2)", : "0 1px 2px rgba(108, 117, 125, 0.2)",
transition: "all 0.2s ease", transition: "all 0.2s ease",
whiteSpace: "nowrap" whiteSpace: "nowrap",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (videoPlaying) { if (videoPlaying) {
e.target.style.transform = "translateY(-1px)"; e.target.style.transform = "translateY(-1px)";
e.target.style.boxShadow = "0 4px 8px rgba(0, 123, 255, 0.4)"; e.target.style.boxShadow =
"0 4px 8px rgba(0, 123, 255, 0.4)";
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (videoPlaying) { if (videoPlaying) {
e.target.style.transform = "translateY(0)"; e.target.style.transform = "translateY(0)";
e.target.style.boxShadow = "0 2px 4px rgba(0, 123, 255, 0.3)"; e.target.style.boxShadow =
"0 2px 4px rgba(0, 123, 255, 0.3)";
} }
}} }}
> >
DL High-Res Video DL High-Res Video
</button> </button>
</div> </div>
</div> </div>
<div className="status-section"> <div className="status-section">
<StatusesDisplayHUD statusMessages={statusMessages} /> <StatusesDisplayHUD statusMessages={statusMessages} />
</div> </div>
<div className="timeline-section"> <div className="timeline-section" style={{ flex: `${Math.max(1, 5 - videoFlex * 0.5)} 1 0`, minHeight: 60 }}>
<EmbedTimeline <EmbedTimeline
chartRef={chartRef} chartRef={chartRef}
data_in={dataResults} data_in={dataResults}
onTimelineClick={handleTimelineClick} onTimelineClick={handleTimelineClick}
authenticatedFetch={authenticatedFetch} authenticatedFetch={authenticatedFetch}
hideDataZoom={videoFlex >= 6}
/> />
</div> </div>
<div className="video-section vjs-16-9 vjs-fluid"> <div className="video-section" style={{ flex: `${videoFlex} 1 0` }} onWheel={handleVideoWheel}>
<VideoPlayer <VideoPlayer
videoRef={playerRef} videoRef={playerRef}
playerInstanceRef={playerInstanceRef} playerInstanceRef={playerInstanceRef}

View File

@@ -62,7 +62,7 @@ export default function CustomDateRangePicker({ startDate, endDate, setStartRang
</button> </button>
{showCalendar && ( {showCalendar && (
<div style={{ position: "absolute", zIndex: 10 }}> <div style={{ position: "fixed", zIndex: 9999 }}>
<DateRange <DateRange
minDate={minDate} minDate={minDate}
maxDate={maxDate} maxDate={maxDate}

View File

@@ -7,6 +7,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
onTimelineClick, onTimelineClick,
authenticatedFetch, authenticatedFetch,
markerTime, markerTime,
hideDataZoom,
}) { }) {
// Use useRef instead of props/state to avoid re-renders // Use useRef instead of props/state to avoid re-renders
const zoomed_range = useRef([null, null]); const zoomed_range = useRef([null, null]);
@@ -475,16 +476,18 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
}, },
response: true, response: true,
grid: { grid: {
top: 30, // Remove top padding top: 30,
left: 10, left: 10,
right: 20, right: 20,
bottom: 60, bottom: hideDataZoom ? 10 : 10,
containLabel: true, containLabel: true,
}, },
dataZoom: [ dataZoom: [
{ {
type: "slider", type: "slider",
show: true, // show: !hideDataZoom,
show: false,
height: hideDataZoom ? 0 : undefined,
xAxisIndex: [0], xAxisIndex: [0],
startValue: virtual_x_min, startValue: virtual_x_min,
endValue: virtual_x_max, endValue: virtual_x_max,

View File

@@ -22,14 +22,13 @@ const VideoPlayer = function VideoPlayer({
controls: true, controls: true,
preload: "auto", preload: "auto",
autoplay: true, autoplay: true,
fluid: true, fill: true,
html5: { html5: {
vhs: { vhs: {
overrideNative: true, // use Video.js native range handling overrideNative: true, // use Video.js native range handling
}, },
nativeVideoTracks: false, nativeVideoTracks: false,
}, },
responsive: true,
techOrder: ['html5'], techOrder: ['html5'],
}) })