Release 260111

This commit is contained in:
Comma Device
2026-01-11 18:23:29 +08:00
commit 3721ecbf8a
2601 changed files with 855070 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
{% extends "layout.html" %}
{% block title %}
About
{% endblock %}
{% block main %}
<br>
<h1>About</h1>
<br>
<footer class="small text-center text-muted" style="word-wrap: break-word;">
Special thanks to:<br><br>
ntegan1<br>
royjr<br>
AlexandreSato<br>
actuallylemoncurd<br>
sunnyhaibin<br>
dragonpilot<br>
chatgpt<br>
</footer>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
Navigation
{% endblock %}
{% block main %}
{% with gmap_key=gmap_key, lon=lon, lat=lat, home=home, work=work, fav1=fav1, fav2=fav2, fav3=fav3 %}
{% include "addr_input.html" %}
{% endwith %}
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% block main %}
<!-- Your form markup -->
<form name="searchForm" method="post">
<fieldset class="uk-fieldset">
<div class="uk-margin">
{% if home or work or fav1 or fav2 or fav3 %}
<select class="uk-select" name="fav_val">
<option value="favorites">Select Saved Destinations</option>
{% if home %} <option value="home">Home: {{home}}</option> {% endif %}
{% if work %} <option value="work">Work: {{work}}</option> {% endif %}
{% if fav1 %} <option value="fav1">Favorite 1: {{fav1}}</option> {% endif %}
{% if fav2 %} <option value="fav2">Favorite 2: {{fav2}}</option> {% endif %}
{% if fav3 %} <option value="fav3">Favorite 3: {{fav3}}</option> {% endif %}
</select>
{% endif %}
<input class="uk-input" type="text" name="addr_val" id="pac-input" placeholder="Search a place">
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Search">
</div>
</fieldset>
</form>
<!-- Include the Google Maps Places API script conditionally with JavaScript -->
<script>
// attach gmap_key to variable
let gmap = "{{gmap_key}}";
// Check if gmap_key is defined
if (gmap && gmap !== "None") {
var script = document.createElement('script');
script.src = 'https://maps.googleapis.com/maps/api/js?key={{gmap_key}}&libraries=places&callback=initAutocomplete';
script.async = true;
script.defer = true;
document.head.appendChild(script);
// Define the callback function for place_changed
function onPlaceChanged() {
var place = autocomplete.getPlace();
// Check if the place has a formatted address
if (place.formatted_address) {
// Set the value of the input field to the formatted address
document.getElementById('pac-input').value = place.formatted_address;
}
}
// Define the autocomplete variable
var autocomplete;
// Define the initAutocomplete function with initial bounds
function initAutocomplete() {
var center = new google.maps.LatLng({{lat}}, {{lon}});
var bounds = new google.maps.Circle({ center: center, radius: 5000 }).getBounds();
autocomplete = new google.maps.places.Autocomplete(
document.getElementById('pac-input'),
{
bounds: bounds // Set initial bounds here
}
);
autocomplete.addListener('place_changed', onPlaceChanged);
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "layout.html" %}
{% block title %}
amap_addr_input
{% endblock %}
{% block main %}
<!-- Head section moved into body -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>输入提示后查询</title>
<!-- UIkit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/css/uikit.min.css" />
<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit-icons.min.js"></script>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode:'{{amap_key_2}}',
}
</script>
<style type="text/css">
body {
margin: 0;
height: 100%;
width: 100%;
position: absolute;
}
#mapContainer {
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 400px;
}
#amap-container {
height: 400px; /* Adjust height as needed */
}
.button-group {
position: absolute;
bottom: 20px;
right: 20px;
font-size: 12px;
padding: 10px;
}
.button-group .button {
height: 28px;
line-height: 28px;
background-color: #0D9BF2;
color: #FFF;
border: 0;
outline: none;
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
margin-bottom: 4px;
cursor: pointer;
}
.amap-info-content {
font-size: 12px;
}
</style>
<script type="text/javascript"
src="https://webapi.amap.com/maps?v=1.4.2&key={{amap_key}}"></script>
<!-- Rest of the HTML body content -->
<div class="uk-grid-match uk-grid-small uk-text-center" uk-grid>
<div class="uk-width-1-3@m">
<select id="save_type" class="uk-select">
<option value="recent">最近</option>
<option value="home">住家</option>
<option value="work">工作</option>
</select>
</div>
<div class="uk-width-expand@m">
<input class="uk-input" type="text" id="keyword" name="keyword"
placeholder="请输入关键字:(选定后搜索)" onfocus='this.value=""' />
</div>
</div>
<input type="hidden" id="longitude" />
<input type="hidden" id="latitude" />
<div style="height: 600px" id="container"></div>
<script type="text/javascript">
var windowsArr = [];
var markers = [];
var map = new AMap.Map("container", {
resizeEnable: true,
center: [{{lat}}, {{lon}}], //地图中心点
zoom: 13, //地图显示的缩放级别
keyboardEnable: false,
});
var infoWindow;
function openInfo(name, addr, lng, lat) {
//构建信息窗体中显示的内容
var info = [];
info.push('<div class="uk-card uk-card-default uk-card-body">');
info.push('<a class="uk-card-badge uk-label" onClick="javascript:infoWindow.close()" uk-close></a>');
info.push("<h3 style=\"padding-top: 10px;\" class=\"uk-card-title\">" + name + "</h3>");
info.push("<p>" + addr + "</p>");
info.push('<div class="uk-card-footer">');
info.push('<form name="navForm" method="post">');
info.push(' <input type="hidden" name="lat" value="' + lat + '">');
info.push(' <input type="hidden" name="lon" value="' + lng + '">');
info.push(' <input type="hidden" name="save_type" value="' + document.getElementById("save_type").value + '">');
info.push(' <input type="hidden" name="name" value="' + name + '">');
info.push(' <input class="uk-button uk-button-primary" type="submit" value="导航" >');
info.push('</form>');
info.push('</div>');
info.push("</div>");
var pos = new AMap.LngLat(lng, lat)
infoWindow = new AMap.InfoWindow({
position: pos,
isCustom: true,
offset: new AMap.Pixel(0, -30),
content: info.join(""), //使用默认信息窗体框样式,显示信息内容
});
infoWindow.open(map, pos);
}
AMap.plugin(["AMap.Autocomplete", "AMap.PlaceSearch"], function () {
var autoOptions = {
city: "全国", //城市,默认全国
input: "keyword", //使用联想输入的input的id
};
autocomplete = new AMap.Autocomplete(autoOptions);
var placeSearch = new AMap.PlaceSearch({
map: "",
});
AMap.event.addListener(autocomplete, "select", function (e) {
//TODO 针对选中的poi实现自己的功能
//重寫搜尋點及其提示資訊begin=====
placeSearch.setCity(e.poi.adcode);
if (e.poi && e.poi.location) {
map.setZoom(17);
map.setCenter(e.poi.location);
}
placeSearch.search(e.poi.name, check_dest); //關鍵字查詢查詢
function check_dest(status, result) {
if (status === "complete" && result.info === "OK") {
for (var h = 0; h < result.poiList.pois.length; h++) {
//返回搜尋列表迴圈繫結marker
var jy = result.poiList.pois[h]["location"]; //經緯度
var name = result.poiList.pois[h]["name"]; //地址
marker = new AMap.Marker({
//加點
map: map,
position: jy,
});
marker.extData = {
getLng: jy["lng"],
getLat: jy["lat"],
name: name,
address: result.poiList.pois[h]["address"],
}; //自定義想傳入的引數
marker.on("click", function (e) {
var hs = e.target.extData;
var content = openInfo(
hs["name"],
hs["address"],
hs["getLng"],
hs["getLat"]
);
});
markers.push(marker);
}
}
}
//重寫搜尋點及其提示資訊end=====
});
});
var clickEventListener = map.on('click', function(e) {
map.remove(markers);
document.getElementById('longitude').value = e.lnglat.getLng();
document.getElementById('latitude').value = e.lnglat.getLat();
lnglatXY = [e.lnglat.getLng(), e.lnglat.getLat()];
var marker = new AMap.Marker({
//加點
map: map,
position: lnglatXY,
});
marker.extData = {
getLng: e.lnglat.getLng(),
getLat: e.lnglat.getLat(),
}; //自定義想傳入的引數
marker.on("click", function (e) {
var hs = e.target.extData;
var content = openInfo(
"",
"(" + hs["getLat"] + ", " + hs["getLng"] + ")",
hs["getLng"],
hs["getLat"]
);
});
markers.push(marker);
if (typeof(infoWindow) != "undefined") {
infoWindow.close();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block title %}
amap_key_input
{% endblock %}
{% block main %}
<form name="setAmapTokenForm" method="post">
<fieldset class="uk-fieldset">
<legend class="uk-legend">请输入您的高德地图 API KEY</legend>
<div style="color: red">因系统升级,若于 2021/12/02 前申请 key 的人请重新申请新的「<b>key</b>」和「<b>安全密钥</b>」配对。</div>
<div class="uk-margin">
<input class="uk-input" type="text" name="amap_key_val" placeholder="KEY">
<input class="uk-input" type="text" name="amap_key_val_2" placeholder="安全密钥">
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="设置">
</div>
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% block title %}
MapBox key input
{% endblock %}
{% block main %}
<form name="setSkTokenForm" method="post">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Set your Mapbox <b>Secret Token</b></legend>
<div style="padding: 5px; color: red; font-weight: bold;">{{msg}}</div>
<div class="uk-margin">
<input class="uk-input" type="text" name="sk_token_val" placeholder="sk.xxxxxxx...">
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Set">
</div>
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends "layout.html" %}
{% block title %}Vehicle Information{% endblock %}
{% block main %}
<div class="container">
<h1 class="text-center mb-4">Vehicle Information</h1>
{% if car_info %}
{% if car_info.get("error") %}
<div class="alert alert-danger">
{{ car_info.get("error") }}
</div>
{% else %}
{% for section, data in car_info.items() %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{{ section }}</h5>
</div>
<div class="card-body">
{% if data is mapping %}
{% for key, value in data.items() %}
<div class="row mb-2">
<div class="col-6">
<strong>{{ key }}</strong>
</div>
<div class="col-6">
{% if value is string and "km/h" in value %}
<span class="badge bg-primary">{{ value }}</span>
{% else %}
{{ value }}
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
{{ data }}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% else %}
<div class="alert alert-warning">
Unable to retrieve vehicle information
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
Error
{% endblock %}
{% block main %}
<br> Oops
<br><br><br><br>
{{ error | safe }}
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block title %}
Error Log
{% endblock %}
{% block main %}
<br>
<h1>Error Log of<br>{{ file_name }}</h1>
<br>
{% endblock %}
{% block unformatted %}
<pre style="font-size: x-small; margin: 20px;">{{ file_content }}</pre>
<br><br>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block title %}
Error Logs
{% endblock %}
{% block main %}
<br>
<h1>Error Logs</h1>
<br>
{% for row in rows %}
<a href="/error_logs/{{ row }}">{{ row }}</a><br>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Routes
{% endblock %}
{% block main %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-5">
<h1 class="display-5 fw-bold text-white text-shadow-bold">Dashcam Recordings</h1>
<i class="fas fa-video fa-2x text-primary"></i>
</div>
<div class="row g-4">
{% for row, gif in zipped %}
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 border-0 shadow-lg hover-effect">
<div class="image-container" style="height: 200px; overflow: hidden;">
<img src="/previewgif/{{ gif }}"
class="card-img-top img-fluid h-100 object-fit-cover"
alt="Recording preview"
style="filter: brightness(0.9);">
</div>
<div class="card-body bg-dark-gradient position-relative" style="z-index: 10;">
<hr class="text-white-50 mt-0 mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-dark bg-opacity-75 fs-6 text-nowrap">
<i class="fas fa-clock me-2"></i>{{ row }}
</span>
<a href="/footage/{{ row }}"
class="btn btn-primary btn-sm rounded-pill px-3 hover-grow position-relative"
style="z-index: 11;">
<i class="fas fa-play me-2"></i>View
</a>
</div>
<div class="text-white-50 small" id="date-{{ row|replace('--', '__') }}">
<i class="fas fa-calendar-day me-1"></i> Loading date...
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[id^="date-"]').forEach(element => {
const route = element.id.replace('date-', '').replace('__', '--');
fetchFolderDate(route, element);
});
});
async function fetchFolderDate(route, element) {
try {
const seg0Path = `/data/media/0/realdata/${route}--0`;
const seg1Path = `/data/media/0/realdata/${route}--1`;
let response;
const seg1Exists = await checkFolderExists(seg1Path);
if (seg1Exists) {
response = await fetch(`/folder-date?path=${encodeURIComponent(seg1Path)}&subtract_minutes=1`);
} else {
response = await fetch(`/folder-date?path=${encodeURIComponent(seg0Path)}`);
}
if (response.ok) {
const data = await response.json();
if (data.date) {
element.innerHTML = `<i class="fas fa-calendar-day me-1"></i> ${data.date}`;
} else {
element.textContent = "Date not available";
}
} else {
throw new Error('Failed to fetch date');
}
} catch (error) {
console.error(`Error fetching date for ${route}:`, error);
element.textContent = "Date not available";
}
}
async function checkFolderExists(path) {
try {
const response = await fetch(`/folder-exists?path=${encodeURIComponent(path)}`);
const data = await response.json();
return data.exists;
} catch (error) {
console.error(`Error checking folder existence for ${path}:`, error);
return false;
}
}
</script>
<style>
.hover-effect {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
background: linear-gradient(145deg, #1a1e21, #23282b);
border-radius: 12px!important;
}
.hover-effect:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
}
.bg-dark-gradient {
background: linear-gradient(to bottom, #2a2e32, #1f2327);
}
.text-shadow-bold {
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.hover-grow {
transition: transform 0.2s ease-in-out;
}
.hover-grow:hover {
transform: scale(1.05);
opacity: 0.9;
}
.object-fit-cover {
object-fit: cover;
object-position: center;
}
.badge.bg-dark {
font-size: 1rem !important;
padding: 0.5em 0.8em;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block title %}
GMap key input
{% endblock %}
{% block main %}
<form name="setGmapTokenForm" method="post">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Set your Google Map API Key</legend>
<div class="uk-margin">
<input class="uk-input" type="text" name="gmap_key_val" placeholder="Google Map API KEY">
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Submit">
</div>
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,87 @@
{% extends "layout.html" %}
{% block title %}
Fleet Manager Dashboard
{% endblock %}
{% block main %}
<div class="container-fluid">
<div class="text-center mb-5">
<h1 class="display-4">🚚 Fleet Manager</h1>
<p class="lead text-muted">Comprehensive fleet management solution</p>
</div>
<div class="row g-4 justify-content-center">
<div class="col-12 col-md-6 col-lg-4">
<div class="card hover-effect h-100">
<div class="card-header bg-primary text-white">
<i class="fas fa-car fa-2x me-2"></i>Vehicle Operations
</div>
<div class="card-body">
<a href="/carinfo" class="btn btn-outline-primary w-100 mb-3">
<i class="fas fa-info-circle me-2"></i>Vehicle Information
</a>
<a href="/preserved" class="btn btn-outline-success w-100 mb-3">
<i class="fas fa-archive me-2"></i>Preserved Footage
</a>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card hover-effect h-100">
<div class="card-header bg-info text-white">
<i class="fas fa-video fa-2x me-2"></i>Media Management
</div>
<div class="card-body">
<a href="/footage" class="btn btn-outline-info w-100 mb-3">
<i class="fas fa-dashcube me-2"></i>Dashcam Footage
</a>
<a href="/screenrecords" class="btn btn-outline-warning w-100 mb-3">
<i class="fas fa-film me-2"></i>Screen Recordings
</a>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card hover-effect h-100">
<div class="card-header bg-secondary text-white">
<i class="fas fa-cogs fa-2x me-2"></i>System Management
</div>
<div class="card-body">
<a href="/error_logs" class="btn btn-outline-danger w-100 mb-3">
<i class="fas fa-bug me-2"></i>Error Logs
</a>
<a href="/about" class="btn btn-outline-dark w-100 mb-3">
<i class="fas fa-info-circle me-2"></i>About System
</a>
</div>
</div>
</div>
</div>
</div>
<style>
.hover-effect {
transition: transform 0.2s, box-shadow 0.2s;
border-radius: 15px;
}
.hover-effect:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.card-header {
border-radius: 15px 15px 0 0 !important;
}
.btn {
transition: all 0.2s;
text-align: left;
padding: 1rem;
border-radius: 10px;
}
.btn:hover {
transform: scale(1.02);
}
</style>
{% endblock %}

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en" id="htmlElement">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
<link href="/static/favicon.ico" rel="icon">
<!-- http://getbootstrap.com/docs/5.3/ -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<style>
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-brand img {
width: 80px;
height: 80px;
}
</style>
<!-- UIkit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/css/uikit.min.css" />
<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit-icons.min.js"></script>
<style>
/* Dark mode styles */
#htmlElement.dark-mode,
#htmlElement.dark-mode input,
#htmlElement.dark-mode select,
#htmlElement.dark-mode body,
#htmlElement.dark-mode h1 {
background-color: #121212; /* Dark background color */
color: #ffffff; /* Light text color */
}
.nav-link {
display: inline-block;
padding: 0 15px;
text-align: center;
}
</style>
<title>CarrotPilot: {% block title %}{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-expand-sm navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand mb-0 h1">
<img class="d-inline-block align-top mr-2" src="/static/carrot.png" /> CarrotPilot </a>
<button type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" class="navbar-toggler" aria-controls="navbarNav" aria-expanded="false" arial-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse ml-auto" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active">
<a href="/footage" class="nav-link">Dashcam Routes</a>
</li>
<li class="nav-item active">
<a href="/screenrecords" class="nav-link">Screen Recordings</a>
</li>
<li class="nav-item active">
<a href="/error_logs" class="nav-link">Error Logs</a>
</li>
<li class="nav-item active">
<a href="/addr_input" class="nav-link">Navigation</a>
</li>
<li class="nav-item active">
<a href="/tools" class="nav-link">Tools</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid p-7 text-center"> {% block main %}{% endblock %} </main>
{% block unformatted %}{% endblock %}
<button class="uk-button uk-button-default uk-margin-small-right" onclick="toggleDarkMode()">Toggle Dark Mode</button>
<script>
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/`;
}
function getCookie(name) {
return document.cookie.split('; ')
.find(row => row.startsWith(name))
?.split('=')[1] || null;
}
function toggleDarkMode() {
console.log('Toggle Dark Mode function called');
const htmlElement = document.documentElement;
htmlElement.classList.toggle('dark-mode');
setCookie('darkMode', htmlElement.classList.contains('dark-mode'), 365);
}
document.addEventListener('DOMContentLoaded', function () {
const htmlElement = document.documentElement;
htmlElement.classList.toggle('dark-mode', getCookie('darkMode') === 'true');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{% extends "layout.html" %}
{% block title %}
Nav Search Confirmation
{% endblock %}
{% block main %}
<div><img src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s-l+000({{lon}},{{lat}})/{{lon}},{{lat}},14/300x300?access_token={{token}}" /></div>
<div style="padding: 5px; font-size: 10px;">{{addr}}</div>
<form name="navForm" method="post">
<fieldset class="uk-fieldset">
<div class="uk-margin">
<input type="hidden" name="name" value="{{addr}}">
<input type="hidden" name="lat" value="{{lat}}">
<input type="hidden" name="lon" value="{{lon}}">
<select id="save_type" name="save_type" class="uk-select">
<option value="recent">Recent</option>
<option value="home">Set Home</option>
<option value="work">Set Work</option>
<option value="fav1">Set Favorite 1</option>
<option value="fav2">Set Favorite 2</option>
<option value="fav3">Set Favorite 3</option>
</select>
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Start Navigation">
</div>
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,149 @@
{% block main %}
<div id="destinationHeading" style="font-weight: bold;"></div>
<div id="jsonOutput" style="text-align: left;"></div>
<style>
/* Added CSS styles to display images and text on the same line */
#jsonOutput span {
display: flex;
align-items: center;
}
#jsonOutput img {
margin-right: 10px; /* Adjust the margin as needed */
}
</style>
<script>
let useMetricUnits = false;
let previousNavdirectionsUuid = null;
let previousCurrentStepUuid = null;
let jsonData = null;
let initNav = 0;
async function loadCurrentStep() {
try {
const response = await fetch('CurrentStep.json'); // Load CurrentStep.json
if (!response.ok) {
throw new Error('Failed to fetch CurrentStep.json.');
}
const json = await response.json();
return json;
} catch (error) {
console.error('Error fetching or parsing CurrentStep.json:', error);
return null;
}
}
async function loadNavdirectionsData() {
try {
const response = await fetch('navdirections.json'); // Load navdirections.json
if (!response.ok) {
throw new Error(`Failed to fetch JSON file. Status: ${response.status}`);
}
const json = await response.json();
// Check if the UUIDs match
const match = json.uuid === previousCurrentStepUuid;
previousNavdirectionsUuid = json.uuid;
jsonData = json;
initNav = 1;
return jsonData;
} catch (error) {
console.error('Error fetching or parsing JSON data:', error);
return jsonData; // Return the existing data on error
}
}
async function fetchAndDisplayData() {
const currentStepData = await loadCurrentStep();
if (currentStepData !== null) {
// Set the initial value for `currentStep` based on `CurrentStep.json`
previousCurrentStepUuid = currentStepData.uuid;
}
if (currentStepData.uuid != previousNavdirectionsUuid) {
await loadNavdirectionsData();
}
if (initNav === 0) {
await loadNavdirectionsData();
}
// Check if jsonData is available and proceed
if (jsonData) {
// Access the data you need from the loaded JSON
const firstRoute = jsonData.routes[0];
const firstLeg = firstRoute.legs[0];
const steps = firstLeg.steps;
const destination = firstRoute.Destination;
// Determine whether to use metric or imperial units based on the 'Metric' key
useMetricUnits = firstRoute.Metric === true; // Removed `const` to update the global useMetricUnits
// Display the 'destination' value on the webpage
const destinationHeading = document.getElementById('destinationHeading');
destinationHeading.textContent = `Destination: ${destination}`;
// Display values from the steps
const jsonOutputDiv = document.getElementById('jsonOutput');
jsonOutputDiv.innerHTML = '';
for (let i = currentStepData.CurrentStep; i < steps.length - 1; i++) {
const step = steps[i];
const instruction0 = steps[i].maneuver.instruction;
const instruction = steps[i + 1].maneuver.instruction;
const maneuverType = steps[i + 1].maneuver.type;
const modifier = steps[i + 1].maneuver.modifier;
let distance = step.distance;
if (!useMetricUnits) {
// Convert distance to miles if using imperial units
distance = distance * 0.000621371;
} else {
distance = distance / 1000; // Convert meters to kilometers
}
const sanitizedManeuverType = maneuverType.replace(/\s+/g, '_');
const sanitizedModifier = modifier ? `_${modifier.replace(/\s+/g, '_')}` : '';
const filterStyle = !htmlElement.classList.contains('dark-mode') ? 'filter: invert(100%);' : '';
// Display the values on the webpage
if (i === 0) {
jsonOutputDiv.innerHTML += `
<hr>
<span>
<img src="/navigation/direction_depart.png" alt="${maneuverType} icon" width="25" height="25" style="${filterStyle}">
<p>${instruction0}</p>
</span>
<hr>
`;
}
jsonOutputDiv.innerHTML += `
<span>
<img src="/navigation/direction_${sanitizedManeuverType}${sanitizedModifier}.png" alt="${maneuverType} icon" width="25" height="25" style="${filterStyle}">
<p>In ${distance.toFixed(1)} ${useMetricUnits ? 'km' : 'miles'}: ${instruction}</p>
</span>
<hr>
`;
}
}
}
// Load `CurrentStep.json` initially
loadCurrentStep().then((currentStepData) => {
if (currentStepData !== null) {
// Set the initial value for `currentStep` based on `CurrentStep.json`
previousCurrentStepUuid = currentStepData.uuid;
loadNavdirectionsData();
// Fetch and display data initially
fetchAndDisplayData();
}
});
// Periodically fetch `CurrentStep.json` and display data every 5 seconds
setInterval(fetchAndDisplayData, 5000); // Adjust the interval as needed (in milliseconds)
</script>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends "layout.html" %}
{% block title %}
Nav Driving Directions
{% endblock %}
{% block main %}
{% with gmap_key=gmap_key, lon=lon, lat=lat, home=home, work=work, fav1=fav1, fav2=fav2, fav3=fav3 %}
{% include "addr_input.html" %}
{% endwith %}
{% include "nav_directions.html" %}
{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block title %}
Preserved Routes
{% endblock %}
{% block main %}
<br>
<h1>Preserved Routes</h1>
<br>
<div class="row">
{% for route_path, gif_path, segment in zipped %}
<div class="col-xs-6 col-sm-4 col-md-3">
<div class="card mb-4 shadow-sm" style="background-color: #212529; color: white;">
<div class="gif-container">
<img src="/previewgif/{{ gif_path }}" class="card-img-top static-gif" alt="GIF">
</div>
<div class="card-body">
<p class="card-text">{{ segment }}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="/footage/{{ route_path }}" class="btn btn-sm btn-outline-secondary">View Footage</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block title %}
Driving Directions
{% endblock %}
{% block main %}
{% include "nav_directions.html" %}
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% block title %}
addr_input
{% endblock %}
{% block main %}
<form name="setPkTokenForm" method="post">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Set your Mapbox <b>Public Token</b></legend>
<div style="padding: 5px; color: red; font-weight: bold;">{{msg}}</div>
<div class="uk-margin">
<input class="uk-input" type="text" name="pk_token_val" placeholder="pk.xxxxxxx...">
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Set">
</div>
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,409 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Segments
{% endblock %}
{% block main %}
{% autoescape false %}
<br>
<h1>Dashcam Segments (one per minute)</h1>
<br>
<video id="video" width="640" height="480" controls autoplay style="background:black">
</video>
<br><br>
<div class="camera-switcher d-flex justify-content-center gap-2 mb-3">
<a href="{{ route }}?{{ query_segment }},qcamera"
class="btn btn-sm {% if query_type == 'qcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">qcamera</a>
<a href="{{ route }}?{{ query_segment }},fcamera"
class="btn btn-sm {% if query_type == 'fcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">fcamera</a>
<a href="{{ route }}?{{ query_segment }},dcamera"
class="btn btn-sm {% if query_type == 'dcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">dcamera</a>
<a href="{{ route }}?{{ query_segment }},ecamera"
class="btn btn-sm {% if query_type == 'ecamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">ecamera</a>
</div>
<div class="video-info">
current segment: <span id="currentsegment" class="badge bg-primary"></span>
<br>
current view: <span id="currentview" class="badge bg-secondary"></span>
<br>
date: <span id="folderDate" class="badge bg-info"></span>
</div>
<br>
<div class="download-section text-center">
<div class="d-flex flex-column align-items-center gap-3">
<div>
<div class="d-flex align-items-center gap-2">
<span>Full Downloads:</span>
<div class="d-flex flex-wrap gap-2">
<a download="{{ route }}-qcamera.mp4" href="/footage/full/qcamera/{{ route }}" class="btn btn-sm btn-outline-primary">qcamera</a>
<a download="{{ route }}-fcamera.mp4" href="/footage/full/fcamera/{{ route }}" class="btn btn-sm btn-outline-primary">fcamera</a>
<a download="{{ route }}-dcamera.mp4" href="/footage/full/dcamera/{{ route }}" class="btn btn-sm btn-outline-primary">dcamera</a>
<a download="{{ route }}-ecamera.mp4" href="/footage/full/ecamera/{{ route }}" class="btn btn-sm btn-outline-primary">ecamera</a>
</div>
</div>
</div>
<div>
<div class="d-flex align-items-center gap-2">
<span>Segment Downloads:</span>
<div class="d-flex flex-wrap gap-2">
<a download="{{ route }}-rlog-{{query_segment}}.mp4" href="/footage/full/rlog/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">rlog</a>
<a download="{{ route }}-qcamera-{{query_segment}}.mp4" href="/footage/full/qcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">qcamera</a>
<a download="{{ route }}-fcamera-{{query_segment}}.mp4" href="/footage/full/fcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">fcamera</a>
<a download="{{ route }}-dcamera-{{query_segment}}.mp4" href="/footage/full/dcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">dcamera</a>
<a download="{{ route }}-ecamera-{{query_segment}}.mp4" href="/footage/full/ecamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">ecamera</a>
</div>
</div>
</div>
</div>
</div>
<div class="segment-controls mt-4">
<h3>Segment List</h3>
<div class="d-flex align-items-center mb-3">
<button onclick="toggleAllSegments()" class="btn btn-sm btn-outline-secondary me-2">Select All</button>
<div id="selectedCount" class="text-muted">0 segments selected</div>
</div>
<div class="segment-list" style="max-height: 400px; overflow-y: auto;">
{% for segment in segments.split(',') %}
{% set clean_segment = segment.strip().strip("'") %}
{% if clean_segment %}
{% set seg_num = clean_segment.split('--')[2] %}
<div class="segment-item d-flex align-items-start py-2 border-bottom ps-2">
<input type="checkbox"
name="selected_segments"
value="{{ clean_segment }}"
class="form-check-input me-3 segment-checkbox mt-1"
style="transform: scale(1.5)">
<div class="d-flex flex-column">
<a href="{{ route }}?{{ seg_num }},{{ query_type }}" class="text-decoration-none">
{{ clean_segment }}
</a>
<small class="text-muted" id="segment-info-{{ seg_num }}">
<i class="bi bi-clock-history me-1"></i><span id="segment-date-{{ seg_num }}">Loading date...</span>
<span class="mx-2"></span>
<i class="bi bi-hdd me-1"></i><span id="segment-size-{{ seg_num }}">Loading size...</span>
</small>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="mt-4">
<button class="btn btn-danger" onclick="uploadSelectedSegments()">
<i class="bi bi-upload me-2"></i>
Upload All Selected Routes To Carrot Server
</button>
<div id="uploadStatus" class="mt-2"></div>
</div>
</div>
<script>
const segments = [{{ segments }}];
const currentSegment = "{{ route }}--{{ query_segment }}";
let currentSegmentIndex = segments.findIndex(seg => seg === currentSegment);
if (currentSegmentIndex === -1) currentSegmentIndex = 0;
const video = document.getElementById('video');
video.src = `/footage/{{ query_type }}/${currentSegment}`;
document.getElementById("currentsegment").textContent = currentSegment;
document.getElementById("currentview").textContent = "{{ query_type }}";
video.load();
video.play().catch(e => console.log("Autoplay prevented:", e));
video.addEventListener('ended', function() {
currentSegmentIndex = (currentSegmentIndex + 1) % segments.length;
const nextSegment = segments[currentSegmentIndex];
const segNum = nextSegment.split('--')[2];
window.location.href = `{{ route }}?${segNum},{{ query_type }}`;
});
function toggleAllSegments() {
const checkboxes = document.querySelectorAll('.segment-checkbox');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
updateSelectionCount();
}
function updateSelectionCount() {
const count = document.querySelectorAll('.segment-checkbox:checked').length;
document.getElementById('selectedCount').textContent = `${count} segments selected`;
}
async function getFolderInfo(folderPath) {
try {
const response = await fetch(`/folder-info?path=${encodeURIComponent(folderPath)}`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (e) {
console.error(`Error getting folder info for ${folderPath}:`, e);
return null;
}
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
async function updateCurrentFolderInfo() {
const folderPath = `/data/media/0/realdata/${currentSegment}`;
const info = await getFolderInfo(folderPath);
const dateElement = document.getElementById('folderDate');
if (info && info.created_date) {
dateElement.textContent = info.created_date;
if (info.size) {
dateElement.textContent += `${formatBytes(info.size)}`;
}
} else {
dateElement.textContent = "정보 없음";
}
}
async function updateSegmentListInfo() {
for (const segment of segments) {
const cleanSegment = segment.trim().replace(/'/g, '');
if (!cleanSegment) continue;
const segNum = cleanSegment.split('--')[2];
const folderPath = `/data/media/0/realdata/${cleanSegment}`;
const info = await getFolderInfo(folderPath);
const dateElement = document.getElementById(`segment-date-${segNum}`);
const sizeElement = document.getElementById(`segment-size-${segNum}`);
if (info) {
if (info.created_date) {
dateElement.textContent = info.created_date;
} else {
dateElement.textContent = "날짜 없음";
}
if (info.size) {
sizeElement.textContent = formatBytes(info.size);
} else {
sizeElement.textContent = "크기 없음";
}
} else {
dateElement.textContent = "정보 없음";
sizeElement.textContent = "정보 없음";
}
}
}
async function uploadSelectedSegments() {
const video = document.getElementById('video');
video.pause();
const selected = Array.from(document.querySelectorAll('.segment-checkbox:checked'))
.map(cb => cb.value);
if(selected.length === 0) {
showUploadStatus('No segments selected!', 'danger');
return;
}
const progressUI = `
<div class="upload-progress mt-3">
<div class="progress mb-2">
<div id="uploadProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<div class="row">
<div class="col-6 text-start">
<small id="uploadProgressText">Preparing upload...</small>
</div>
<div class="col-6 text-end">
<small id="uploadSpeed">-</small>
</div>
</div>
<div class="row mt-1">
<div class="col-12">
<small id="uploadFileInfo" class="text-muted">-</small>
</div>
</div>
</div>
`;
const statusDiv = document.getElementById('uploadStatus');
statusDiv.innerHTML = progressUI;
try {
let totalSize = 0;
let uploadedSize = 0;
const startTime = Date.now();
const uploads = [];
const selectedSegmentsCount = selected.length;
document.getElementById('uploadProgressText').textContent = 'Calculating total size...';
for (const segment of selected) {
const segmentNum = segment.split('--')[2];
const files = ['rlog.zst', 'qcamera.ts'];
for (const file of files) {
try {
const response = await fetch(`/file-size?path=/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`);
const { size } = await response.json();
if (size) {
totalSize += size;
uploads.push({ segmentNum, file, size });
}
} catch (e) {
console.warn(`Size check failed for ${file}`, e);
}
}
}
const initialUploadMsg = `Uploading ${uploads.length} files (${selectedSegmentsCount} Segments) ${formatBytes(totalSize)}...`;
showUploadStatus(initialUploadMsg, 'info');
document.getElementById('uploadProgressText').textContent = initialUploadMsg;
document.getElementById('uploadFileInfo').textContent =
`${selectedSegmentsCount} Segments | ${uploads.length} files | ${formatBytes(totalSize)}`;
for (const { segmentNum, file, size } of uploads) {
try {
const filePath = `/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`;
document.getElementById('uploadFileInfo').textContent =
`Uploading: ${file} (${formatBytes(size)})`;
const formData = new FormData();
const blob = await fetch(filePath).then(r => r.blob());
formData.append('file', blob, file);
formData.append('segment', segmentNum);
const xhr = new XMLHttpRequest();
xhr.open('POST', `/footage/full/upload_carrot/{{ route }}/${segmentNum}`, true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable && totalSize > 0) {
const loaded = uploadedSize + e.loaded;
const percent = Math.round(loaded / totalSize * 100);
const elapsed = (Date.now() - startTime) / 1000;
const speed = elapsed > 0 ? loaded / (1024 * 1024 * elapsed) : 0;
document.getElementById('uploadProgressBar').style.width = `${percent}%`;
document.getElementById('uploadProgressText').textContent =
`${percent}% (${formatBytes(loaded)}/${formatBytes(totalSize)})`;
document.getElementById('uploadSpeed').textContent =
`${speed.toFixed(2)} MB/s`;
}
};
await new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status === 200) {
uploadedSize += size;
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.send(formData);
});
} catch (error) {
console.error(`Upload error for ${file}:`, error);
showUploadStatus(`Error: ${error.message}`, 'danger');
throw error;
}
}
const successAlert = document.createElement('div');
successAlert.className = 'alert alert-success';
successAlert.textContent = `Upload complete! ${selectedSegmentsCount} segments uploaded`;
statusDiv.prepend(successAlert);
const initialAlert = statusDiv.querySelector('.alert-info');
if (initialAlert) initialAlert.remove();
const progressElements = document.querySelectorAll('.upload-progress, #uploadProgressBar, #uploadProgressText, #uploadSpeed, #uploadFileInfo');
progressElements.forEach(el => el.remove());
} catch (error) {
console.error('Upload failed:', error);
showUploadStatus(`Upload failed: ${error.message}`, 'danger');
document.getElementById('uploadProgressBar').classList.remove('progress-bar-animated');
document.getElementById('uploadProgressBar').classList.add('bg-danger');
}
}
function showUploadStatus(message, type, append = false) {
const statusDiv = document.getElementById('uploadStatus');
if (!append) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} mb-2`;
alertDiv.textContent = message;
statusDiv.prepend(alertDiv);
}
}
document.addEventListener('DOMContentLoaded', function() {
updateSelectionCount();
updateCurrentFolderInfo();
updateSegmentListInfo();
});
</script>
{% endautoescape %}
<br><br>
{% endblock %}
<style>
.upload-progress {
background: #f8f9fa;
border-radius: 8px;
padding: 10px;
margin-top: 10px;
}
.progress {
height: 20px;
}
.progress-bar {
transition: width 0.3s ease;
}
.download-section .btn-group .btn {
border-radius: 20px !important;
margin: 0 2px;
}
.download-section span.me-2 {
font-size: 0.9em;
color: #666;
}
.download-section .btn {
min-width: 90px;
padding: 0.25rem 0.5rem;
}
.download-section .d-flex.align-items-center {
flex-wrap: wrap;
row-gap: 8px;
}
.camera-switcher .btn {
min-width: 80px;
transition: all 0.2s ease;
}
.segment-item {
align-items: flex-start !important;
}
.segment-item small {
font-size: 0.8rem;
margin-top: 2px;
}
.segment-item .form-check-input {
margin-top: 0.3rem;
}
.badge {
font-weight: 500;
}
#folderDate {
font-size: 0.9em;
}
</style>

View File

@@ -0,0 +1,94 @@
{% extends "layout.html" %}
{% block title %}
Screen Recordings
{% endblock %}
{% block main %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div class="card shadow mb-5">
<div class="card-header bg-dark text-white">
<i class="fas fa-video me-2"></i>Currently Playing
</div>
<div class="card-body text-center">
<video id="video" class="screen-player" controls autoplay>
Your browser does not support the video tag.
</video>
<div class="mt-3">
<span class="badge bg-secondary">
<i class="fas fa-eye me-2"></i>
<span id="mycurrentview"></span>
</span>
</div>
</div>
<div class="card-footer bg-light">
<a href="/screenrecords/download/{{ clip }}" class="btn btn-danger btn-sm">
<i class="fas fa-download me-2"></i>Download Current Recording
</a>
</div>
</div>
<div class="card shadow">
<div class="card-header bg-primary text-white">
<i class="fas fa-list-ul me-2"></i>Available Recordings
</div>
<div class="card-body">
<div class="list-group">
{% for row in rows %}
<a href="/screenrecords/{{ row[0] }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-file-video me-3 text-primary"></i>
{{ row[0]|replace('.mp4', '')|replace('_', ' ') }}
</div>
<div class="text-end">
<small class="text-muted d-block">
{{ row[0]|datetimeformat }}
</small>
<small class="text-muted">
{{ (row[1] / 1024 / 1024)|round(2) }} MB
</small>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.screen-player {
width: 100%;
height: auto;
max-width: 640px;
border-radius: 10px;
background: #000;
border: 3px solid #dee2e6;
}
.list-group-item {
transition: all 0.2s;
margin: 2px 0;
border-radius: 5px!important;
}
.list-group-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
<script>
const video = document.getElementById("video");
video.src = "/screenrecords/play/pipe/{{ clip }}";
document.getElementById("mycurrentview").textContent = "{{ clip }}".replace('.mp4', '');
video.addEventListener('play', () => {
video.style.maxWidth = '100%';
video.style.height = 'auto';
});
</script>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% extends "layout.html" %}
{% block title %}
Tools
{% endblock %}
{% block main %}
<div class="container">
<h1 class="my-4">Toggle Values Manager</h1>
<div class="card shadow-sm mb-4">
<div class="card-body">
<textarea
id="toggleValuesBox"
class="form-control"
style="height: 300px; font-family: monospace;"
placeholder='Enter Values here...'
></textarea>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button
id="retrieveButton"
class="btn btn-primary"
onclick="fetchToggleValues()"
>
<i class="fas fa-download me-2"></i>Get Values
</button>
<button
id="submitButton"
class="btn btn-success"
onclick="saveToggleValues()"
>
<i class="fas fa-upload me-2"></i>Save Values
</button>
</div>
</div>
<script>
async function fetchToggleValues() {
try {
const response = await fetch('/get_toggle_values');
const data = await response.json();
document.getElementById('toggleValuesBox').value =
JSON.stringify(data, null, 2);
} catch (error) {
alert('Error fetching values');
}
}
async function saveToggleValues() {
const textarea = document.getElementById('toggleValuesBox');
try {
const jsonData = JSON.parse(textarea.value);
const response = await fetch('/store_toggle_values', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData),
});
if (response.ok) {
alert('Successfully saved!');
} else {
throw new Error('Save failed');
}
} catch (error) {
alert('Invalid JSON format!');
textarea.classList.add('is-invalid');
setTimeout(() => textarea.classList.remove('is-invalid'), 2000);
}
}
</script>
<style>
.card {
border-radius: 12px;
}
textarea {
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
}
</style>
{% endblock %}