963 lines
38 KiB
HTML
963 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta
|
||
name="viewport"
|
||
content="width=device-width, initial-scale=1"
|
||
/>
|
||
<title>Media Dashboard</title>
|
||
<link rel="icon" type="image/x-icon" href="../static/images/favicon.ico" />
|
||
<link rel="stylesheet" href="../static/css/styles.css" />
|
||
<link rel="stylesheet" href="../static/fontawesome/css/all.min.css" />
|
||
<style>
|
||
/* High-contrast text everywhere */
|
||
body,
|
||
.content,
|
||
h1, h2, h3, h4, h5, h6,
|
||
p, label, .status,
|
||
.checkbox-inline,
|
||
.container,
|
||
.form-group,
|
||
.form-group * {
|
||
color: #1f2937; /* gray-800 */
|
||
}
|
||
|
||
*{ box-sizing: border-box; }
|
||
html, body{ height: 100%; }
|
||
body{
|
||
margin: 0;
|
||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||
line-height: 1.55;
|
||
/* Page/background image/color is controlled by your existing CSS; unchanged */
|
||
}
|
||
|
||
#navbar{ position: sticky; top: 0; z-index: 50; }
|
||
|
||
.content-wrapper{
|
||
position: relative; /* anchor for the underlay image */
|
||
max-width: 980px;
|
||
margin: 0 auto;
|
||
padding: 16px;
|
||
z-index: 1; /* keep main content above the underlay */
|
||
}
|
||
@media (min-width: 980px){
|
||
.content-wrapper{ padding: 24px; }
|
||
}
|
||
|
||
.content{
|
||
position: relative;
|
||
z-index: 2; /* ensure UI is above the image */
|
||
display: flex;
|
||
flex-direction: column; /* stack containers vertically */
|
||
align-items: center; /* center containers horizontally */
|
||
gap: 20px;
|
||
}
|
||
|
||
/* Tabs */
|
||
.tabs{ width:100%; max-width: 740px; margin: 0 auto; display:flex; flex-direction:column; align-items:center; }
|
||
/* When the Tools tab is active make the tabs area wider so the tools container can expand */
|
||
.tabs.tools-wide{ max-width: 880px; }
|
||
.tab-buttons{ display:flex; gap:8px; margin-bottom:12px; justify-content:center; }
|
||
.tab-button{
|
||
appearance: none;
|
||
border: 1px solid #d1d5db;
|
||
background: #f8fafc;
|
||
color: #111827;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
}
|
||
.tab-button[aria-selected="true"]{
|
||
background: #1e40af;
|
||
color: #fff;
|
||
border-color: #1e40af;
|
||
}
|
||
.tab-panel{ display: none; }
|
||
.tab-panel[data-open="true"]{ display: block; }
|
||
|
||
/* Tools UI: 3-column layout where center column is fixed width and
|
||
left/right columns have both a minimum and maximum width so they
|
||
shrink and grow smoothly depending on available space. */
|
||
.tools-grid{
|
||
display: grid;
|
||
/* side columns: min 140px, max 360px; center column fixed at 160px */
|
||
grid-template-columns: minmax(140px, 360px) 160px minmax(140px, 360px);
|
||
gap: 12px;
|
||
align-items: start;
|
||
}
|
||
.tools-column{
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
/* ensure columns can shrink but not collapse */
|
||
min-width: 140px;
|
||
max-width: 500px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* Make the Tools tab container wider than the default small .container so both columns have space.
|
||
This changes only the container inside the Tools panel; textareas remain their own size. */
|
||
#tab-tools .container{
|
||
max-width: 1420px; /* wider than the default 420px */
|
||
width: 100%;
|
||
margin: 0 auto 16px;
|
||
padding: 16px;
|
||
}
|
||
.tools-list{ border:1px solid #d1d5db; border-radius:8px; padding:10px; height:220px; overflow:auto; background:#fff; }
|
||
.tools-textarea{ width:100%; height:80px; padding:8px; border:1px solid #d1d5db; border-radius:6px; overflow:auto; }
|
||
/* Center buttons column: fixed width and top-aligned. Padding-top is
|
||
initially 0; alignment can be adjusted dynamically by JS for pixel-perfect
|
||
alignment when needed. */
|
||
.tools-buttons{
|
||
width: 160px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding-top: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Ensure the main title spans full width and is centered above the panels */
|
||
.content > h1{
|
||
flex-basis: 100%;
|
||
text-align: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* Underlay image: left-aligned, same size, no layout impact */
|
||
.side-img{
|
||
position: absolute;
|
||
left: 0;
|
||
top: 110px; /* adjust as you like */
|
||
width: 230px; /* keep current size */
|
||
height: auto;
|
||
z-index: 0; /* behind everything */
|
||
pointer-events: none; /* clicks go through */
|
||
opacity: 1; /* fully visible; change if you want subtler */
|
||
}
|
||
|
||
h1{
|
||
margin: 0 0 8px;
|
||
font-size: clamp(1.6rem, 2.2vw, 2.2rem);
|
||
color: #333; /* explicit, high-contrast */
|
||
}
|
||
h2{
|
||
margin: 0 0 10px;
|
||
font-size: clamp(1.1rem, 1.8vw, 1.4rem);
|
||
color: #333;
|
||
}
|
||
|
||
/* Card containers – background color kept EXACTLY as before */
|
||
.container{
|
||
width: 100%;
|
||
max-width: 420px;
|
||
margin: 0 auto 16px;
|
||
padding: 16px;
|
||
border: 2px solid #ccc;
|
||
border-radius: 10px;
|
||
background-color: #ffffffb6; /* unchanged */
|
||
position: relative; /* creates its own stacking context above image */
|
||
z-index: 1;
|
||
}
|
||
|
||
.form-group{ margin-bottom: 14px; }
|
||
.form-group label{ display: block; margin-bottom: 6px; font-weight: 600; }
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea{
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
font-size: 1rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.checkbox-row{
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.checkbox-inline{
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px; /* more space between box and label */
|
||
font-weight: 600;
|
||
padding: 4px 6px; /* larger hit area for touch */
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* Larger, touch-friendly checkboxes while preserving native behavior */
|
||
.checkbox-inline input[type="checkbox"]{
|
||
/* scale is reliable across browsers for visually larger boxes */
|
||
transform: scale(1.3);
|
||
-webkit-transform: scale(1.3);
|
||
margin: 0; /* reset default margins so gap controls spacing */
|
||
/* align the scaled box nicely with text */
|
||
transform-origin: left center;
|
||
-webkit-transform-origin: left center;
|
||
width: 18px; /* keep a sensible layout size in some UAs */
|
||
height: 18px;
|
||
appearance: auto; /* keep native look (checked glyph) */
|
||
accent-color: #1e40af; /* modern browsers: use theme color for check mark */
|
||
}
|
||
/* utility: push an inline checkbox/label to the far right inside a flex row */
|
||
.checkbox-inline.right { margin-left: auto; }
|
||
|
||
/* Label with icon helper */
|
||
.label-icon{ display: inline-flex; align-items: flex-end; gap: 8px; font-weight: 700; }
|
||
/* Keep both icons same size and bottom-aligned with the label text */
|
||
.usb-icon, .pc-icon { width: 40px; height: 40px; object-fit: contain; display: inline-block; vertical-align: bottom; margin-bottom: 2px; }
|
||
|
||
.button-group{
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-start;
|
||
margin: 12px 0 6px;
|
||
}
|
||
|
||
.button-row{
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.button-row .controls{ display:flex; gap:10px; }
|
||
.button-row .actions{ margin-left: auto; }
|
||
|
||
.btn{
|
||
appearance: none;
|
||
border: 1px solid transparent;
|
||
border-radius: 8px;
|
||
padding: 10px 16px;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
color: #fff; /* keep buttons readable */
|
||
background: #1e40af; /* blue-800 */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
text-decoration: none;
|
||
}
|
||
.btn:hover{ background: #15327f; }
|
||
.btn:focus-visible{
|
||
outline: 3px solid #2563eb;
|
||
outline-offset: 2px;
|
||
}
|
||
.btn[disabled]{ opacity: 0.6; cursor: not-allowed; }
|
||
|
||
.status{
|
||
margin-top: 8px;
|
||
font-size: 0.98rem;
|
||
min-height: 1.2em;
|
||
}
|
||
|
||
.input-label{ text-align: left; }
|
||
|
||
.form-group textarea{
|
||
resize: none;
|
||
overflow-wrap: break-word;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
/* Optional: make sure the underlay doesn't collide on very small screens */
|
||
@media (max-width: 480px){
|
||
.side-img{ top: 140px; width: 190px; }
|
||
}
|
||
|
||
/* Strong override: ensure Tools container can expand even if other rules limit .container.
|
||
Uses !important to defeat competing rules when needed. */
|
||
#tab-tools .container{
|
||
max-width: 1100px !important;
|
||
width: 100% !important;
|
||
margin: 0 auto 16px !important;
|
||
padding: 24px !important;
|
||
min-height: 540px !important; /* make the tools container taller */
|
||
}
|
||
|
||
/* Make the checkbox lists taller so the Tools panel looks taller */
|
||
#toolsLeftList, #toolsRightList, .tools-list{
|
||
min-height: 420px !important;
|
||
height: 420px !important;
|
||
}
|
||
/* Responsive: stack the three columns on narrow screens */
|
||
@media (max-width: 599px){
|
||
.tools-grid{ display:flex; flex-direction:column; gap:12px; }
|
||
.tools-buttons{ width: auto; padding-top: 20px; }
|
||
.tools-column{ min-width: 0; }
|
||
/* slightly shorter lists on small screens so the stacked layout fits */
|
||
#toolsLeftList, #toolsRightList, .tools-list{ min-height: 280px !important; height: 280px !important; }
|
||
}
|
||
/* Tools footer: status text left, disk-usage bar right */
|
||
.tools-footer{
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
}
|
||
.tools-status-msg{
|
||
flex: 1 1 auto;
|
||
text-align: left;
|
||
font-size: 0.95rem;
|
||
color: #111827;
|
||
min-height: 1.2em;
|
||
}
|
||
.disk-usage-wrap{
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 220px;
|
||
justify-content: flex-end;
|
||
}
|
||
.disk-usage-bar{
|
||
width: 160px;
|
||
height: 14px;
|
||
background: #e5e7eb; /* gray-200 */
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
|
||
}
|
||
.disk-usage-fill{
|
||
height: 100%;
|
||
width: 0%;
|
||
background: #10b981; /* green-500 */
|
||
transition: width 300ms ease, background 200ms ease;
|
||
}
|
||
.disk-usage-text{ font-weight: 600; font-size: 0.95rem; color: #111827; }
|
||
@media (max-width: 639px){
|
||
.disk-usage-wrap{ min-width: 140px; }
|
||
.disk-usage-bar{ width: 120px; }
|
||
}
|
||
|
||
/* Confirmation modal for delete action */
|
||
.confirm-overlay{
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.45);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
}
|
||
.confirm-box{
|
||
background: #fff;
|
||
padding: 18px;
|
||
border-radius: 8px;
|
||
max-width: 480px;
|
||
width: calc(100% - 48px);
|
||
box-shadow: 0 8px 24px rgba(15,23,42,0.2);
|
||
color: #111827;
|
||
}
|
||
.confirm-actions{ display:flex; gap:12px; justify-content:flex-end; margin-top:14px; }
|
||
.confirm-actions .btn{ min-width: 84px; }
|
||
.confirm-message{ font-weight:600; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="navbar"></div>
|
||
<script>
|
||
fetch('/static/html/nav.html')
|
||
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
||
.then(html => { document.getElementById('navbar').innerHTML = html; })
|
||
.catch(() => { document.getElementById('navbar').innerHTML = '<div class="container" role="alert">Navigation failed to load.</div>'; });
|
||
</script>
|
||
|
||
<div class="background-image"></div>
|
||
|
||
<main class="content-wrapper">
|
||
<!-- Underlay image (beneath everything, left-aligned, fixed size) -->
|
||
<img src="/static/images/helio-posh.png" alt="Helio Posh" class="side-img" />
|
||
|
||
<div class="content">
|
||
<h1>Media Dashboard</h1>
|
||
|
||
<div class="tabs" role="tablist" aria-label="Main tabs">
|
||
<div class="tab-buttons">
|
||
<button class="tab-button" role="tab" id="tab-btn-dashboard" aria-controls="tab-dashboard" aria-selected="true">Dashboard</button>
|
||
<button class="tab-button" role="tab" id="tab-btn-tools" aria-controls="tab-tools" aria-selected="false">Tools</button>
|
||
</div>
|
||
|
||
<!-- Dashboard panel: existing controls moved here -->
|
||
<div id="tab-dashboard" class="tab-panel" data-open="true" role="tabpanel" aria-labelledby="tab-btn-dashboard">
|
||
<section class="container" aria-labelledby="playlist-heading">
|
||
<h2 id="playlist-heading">Playlist Loop</h2>
|
||
|
||
<div class="checkbox-row">
|
||
<label class="checkbox-inline" for="autoPlayAtBoot">
|
||
<input type="checkbox" id="autoPlayAtBoot" name="autoPlayAtBoot">
|
||
Autostart @ Boot
|
||
</label>
|
||
|
||
<label class="checkbox-inline" for="saveSettings">
|
||
<input type="checkbox" id="saveSettings" name="saveSettings" title="Click to make sure your settings are saved permanently" aria-describedby="saveSettingsHint">
|
||
Save
|
||
</label>
|
||
<span id="saveSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="input-label" for="mediaLocation">Media Sources</label>
|
||
<select id="mediaLocation" name="mediaLocation" aria-describedby="mediaHelp">
|
||
<option value="USB">No Media Available</option>
|
||
</select>
|
||
<div id="mediaHelp" class="sr-only">Choose a folder to loop through images.</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="input-label" for="imageDuration">Image Duration (secs)</label>
|
||
<input type="number" id="imageDuration" name="imageDuration" min="1" max="60" inputmode="numeric">
|
||
</div>
|
||
|
||
<div class="button-group" role="group" aria-label="Playlist loop controls">
|
||
<button class="btn" id="btnStartLoop" onclick="startMediaLoop()">
|
||
<i class="fa fa-play" aria-hidden="true"></i> Start
|
||
</button>
|
||
<button class="btn" id="btnStopLoop" onclick="stopMediaLoop()">
|
||
<i class="fa fa-stop" aria-hidden="true"></i> Stop
|
||
</button>
|
||
</div>
|
||
|
||
<div class="status" id="mediaLoopStatus" role="status" aria-live="polite">status</div>
|
||
</section>
|
||
|
||
<section class="container" aria-labelledby="gallery-heading">
|
||
<h2 id="gallery-heading">Web Gallery</h2>
|
||
|
||
<div class="checkbox-row" style="justify-content:flex-start;">
|
||
<label class="checkbox-inline" for="autoStart">
|
||
<input type="checkbox" id="autoStart" name="autoStart">
|
||
Autostart
|
||
</label>
|
||
|
||
<label class="checkbox-inline right" for="saveWebSettings">
|
||
<input type="checkbox" id="saveWebSettings" name="saveWebSettings" title="Click to make sure your settings are saved permanently" aria-describedby="saveWebSettingsHint">
|
||
Save
|
||
</label>
|
||
<span id="saveWebSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
|
||
</div>
|
||
|
||
|
||
<div class="form-group">
|
||
<label class="input-label" for="galleryURL">URL</label>
|
||
<textarea id="galleryURL" name="galleryURL" rows="3" placeholder="https://ataphotobooths.com"></textarea>
|
||
</div>
|
||
|
||
<div class="button-row" role="group" aria-label="Web gallery controls">
|
||
<div class="controls">
|
||
<button class="btn" id="btnStartWeb" onclick="startWebGallery()">
|
||
<i class="fa fa-play" aria-hidden="true"></i> Start
|
||
</button>
|
||
<button class="btn" id="btnStopWeb" onclick="stopWebGallery()">
|
||
<i class="fa fa-stop" aria-hidden="true"></i> Stop
|
||
</button>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn" type="button" onclick="clearURL()">
|
||
<i class="fa fa-eraser" aria-hidden="true"></i> Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status" id="webGalleryStatus" role="status" aria-live="polite">status</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- Tools panel -->
|
||
<div id="tab-tools" class="tab-panel" data-open="false" role="tabpanel" aria-labelledby="tab-btn-tools">
|
||
<section class="container" aria-labelledby="tools-heading">
|
||
<h2 id="tools-heading">Media Transfer</h2>
|
||
|
||
<div class="tools-grid">
|
||
<div class="tools-column">
|
||
<label for="toolsLeftTextarea" class="label-icon"><img src="../static/images/usb_blk.png" alt="" aria-hidden="true" class="usb-icon"><span>USB folders</span></label>
|
||
<label class="checkbox-inline" for="overwriteLeft" style="margin-top:6px;">
|
||
<input type="checkbox" id="overwriteLeft" name="overwriteLeft"> Allow Overwrite
|
||
</label>
|
||
<div id="toolsLeftList" class="tools-list" aria-label="USB items list"></div>
|
||
</div>
|
||
|
||
<div class="tools-buttons" aria-hidden="false">
|
||
<div style="height:100px; width:100%;"></div>
|
||
<button class="btn" type="button" title="Copy selected from USB to PC" onclick="moveSelected('left','right')">USB to PC →</button>
|
||
<button class="btn" type="button" title="Copy selected from PC to USB" onclick="moveSelected('right','left')">← PC to USB</button>
|
||
<button class="btn" type="button" onclick="refreshToolsLists()">Refresh Lists</button>
|
||
<div style="height:30px; width:100%;"></div>
|
||
<button class="btn" type="button" title="Delete selected PC folders" onclick="deleteSelectedPC()" style="background:#dc2626">Delete →</button>
|
||
</div>
|
||
|
||
<div class="tools-column">
|
||
<label for="toolsRightTextarea" class="label-icon"><img src="../static/images/helio-posh.png" alt="" aria-hidden="true" class="pc-icon"><span>PC folders</span></label>
|
||
<label class="checkbox-inline right" for="overwriteRight" style="margin-top:6px;">
|
||
<input type="checkbox" id="overwriteRight" name="overwriteRight"> Allow Overwrite
|
||
</label>
|
||
<div id="toolsRightList" class="tools-list" aria-label="PC items list"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Footer area: left-aligned status message and right-aligned disk usage bar -->
|
||
<div class="tools-footer" role="status" aria-live="polite">
|
||
<div id="toolsStatusMessage" class="tools-status-msg">Ready.</div>
|
||
<div class="disk-usage-wrap" aria-hidden="false">
|
||
<div class="disk-usage-bar" title="Free space">
|
||
<div id="diskUsageFill" class="disk-usage-fill" style="width:0%"></div>
|
||
</div>
|
||
<div id="diskUsageText" class="disk-usage-text">-- free</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
function setBusy(el, busy){
|
||
if (!el) return;
|
||
el.disabled = !!busy;
|
||
if (busy) el.setAttribute('aria-busy','true'); else el.removeAttribute('aria-busy');
|
||
}
|
||
function setText(id, text){
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = text || '';
|
||
}
|
||
|
||
async function loadMediaSources(){
|
||
try{
|
||
const res = await fetch('../get_media_sources');
|
||
const data = await res.json();
|
||
const select = document.getElementById('mediaLocation');
|
||
if (!select) return;
|
||
// Clear existing
|
||
select.innerHTML = '';
|
||
const arr = Array.isArray(data?.folders) ? data.folders : [];
|
||
if (arr.length === 0){
|
||
const opt = document.createElement('option');
|
||
opt.value = 'USB';
|
||
opt.text = 'No Media Available';
|
||
select.add(opt);
|
||
return;
|
||
}
|
||
arr.forEach(m => {
|
||
const opt = document.createElement('option');
|
||
opt.text = m.folder_name_display;
|
||
opt.value = m.folder_path;
|
||
select.add(opt);
|
||
});
|
||
}catch(err){
|
||
console.error('Error loading media sources:', err);
|
||
}
|
||
}
|
||
|
||
async function loadLastUsedData(){
|
||
try{
|
||
const res = await fetch('../get_screen_settings');
|
||
const data = await res.json();
|
||
|
||
// Only default if undefined (avoid forcing true when false)
|
||
const apb = (data.autoPlayAtBoot === undefined) ? true : !!data.autoPlayAtBoot;
|
||
const dur = (data.imageDuration === undefined) ? 5 : Number(data.imageDuration);
|
||
const as = (data.autoStart === undefined) ? true : !!data.autoStart;
|
||
const url = (data.url === undefined) ? 'google.com' : String(data.url);
|
||
|
||
document.getElementById('autoPlayAtBoot').checked = apb;
|
||
document.getElementById('imageDuration').value = dur;
|
||
document.getElementById('autoStart').checked = as;
|
||
document.getElementById('galleryURL').value = url;
|
||
document.getElementById('saveSettings').checked = false;
|
||
}catch(err){
|
||
console.error('Error loading screen settings:', err);
|
||
}
|
||
}
|
||
|
||
async function startMediaLoop(){
|
||
const mediaLocation = document.getElementById('mediaLocation').value;
|
||
const imageDuration = document.getElementById('imageDuration').value;
|
||
const autoPlayAtBoot = document.getElementById('autoPlayAtBoot').checked;
|
||
const saveSettings = document.getElementById('saveSettings').checked;
|
||
|
||
// Reset the save toggle after reading
|
||
document.getElementById('saveSettings').checked = false;
|
||
|
||
const btnStart = document.getElementById('btnStartLoop');
|
||
const btnStop = document.getElementById('btnStopLoop');
|
||
setBusy(btnStart, true); setBusy(btnStop, true);
|
||
setText('mediaLoopStatus', 'Starting media loop…');
|
||
|
||
try{
|
||
const res = await fetch('../start_media_loop', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ mediaLocation, imageDuration, autoPlayAtBoot, saveSettings })
|
||
});
|
||
const data = await res.json();
|
||
setText('mediaLoopStatus', data.message || 'Started.');
|
||
}catch(err){
|
||
console.error('Error during start media loop:', err);
|
||
setText('mediaLoopStatus', 'Error during start media loop.');
|
||
}finally{
|
||
setBusy(btnStart, false); setBusy(btnStop, false);
|
||
}
|
||
}
|
||
|
||
async function stopMediaLoop(){
|
||
const btnStart = document.getElementById('btnStartLoop');
|
||
const btnStop = document.getElementById('btnStopLoop');
|
||
setBusy(btnStart, true); setBusy(btnStop, true);
|
||
setText('mediaLoopStatus', 'Stopping media loop…');
|
||
try{
|
||
const res = await fetch('../stop_media_loop', { method: 'POST' });
|
||
const data = await res.json();
|
||
setText('mediaLoopStatus', data.message || 'Stopped.');
|
||
}catch(err){
|
||
console.error('Error during stop media loop:', err);
|
||
setText('mediaLoopStatus', 'Error during stop media loop.');
|
||
}finally{
|
||
setBusy(btnStart, false); setBusy(btnStop, false);
|
||
}
|
||
}
|
||
|
||
async function startWebGallery(){
|
||
const autoStart = document.getElementById('autoStart').checked;
|
||
const url = document.getElementById('galleryURL').value;
|
||
|
||
const btnStart = document.getElementById('btnStartWeb');
|
||
const btnStop = document.getElementById('btnStopWeb');
|
||
const saveWebSettings = document.getElementById('saveWebSettings').checked;
|
||
|
||
setBusy(btnStart, true); setBusy(btnStop, true);
|
||
setText('webGalleryStatus', 'Starting web gallery…');
|
||
|
||
try{
|
||
const res = await fetch('../start_web_gallery', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ url, autoStart, saveWebSettings })
|
||
});
|
||
const data = await res.json();
|
||
setText('webGalleryStatus', data.message || 'Started.');
|
||
}catch(err){
|
||
console.error('Error during start web gallery:', err);
|
||
setText('webGalleryStatus', 'Error during start web gallery.');
|
||
}finally{
|
||
setBusy(btnStart, false); setBusy(btnStop, false);
|
||
document.getElementById('saveWebSettings').checked = false;
|
||
}
|
||
}
|
||
|
||
async function stopWebGallery(){
|
||
const btnStart = document.getElementById('btnStartWeb');
|
||
const btnStop = document.getElementById('btnStopWeb');
|
||
setBusy(btnStart, true); setBusy(btnStop, true);
|
||
setText('webGalleryStatus', 'Stopping web gallery…');
|
||
|
||
try{
|
||
const res = await fetch('../stop_web_gallery', { method: 'POST' });
|
||
const data = await res.json();
|
||
setText('webGalleryStatus', data.message || 'Stopped.');
|
||
}catch(err){
|
||
console.error('Error during stop web gallery:', err);
|
||
setText('webGalleryStatus', 'Error during stop web gallery.');
|
||
}finally{
|
||
setBusy(btnStart, false); setBusy(btnStop, false);
|
||
}
|
||
}
|
||
|
||
function clearURL(){ document.getElementById('galleryURL').value = ''; }
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadLastUsedData();
|
||
loadMediaSources();
|
||
setupTabs();
|
||
// initial alignment of the buttons column
|
||
// initial disk usage fetch
|
||
try{ refreshDiskUsage(); }catch(e){}
|
||
});
|
||
|
||
/* Tab control */
|
||
function setupTabs(){
|
||
const btnDashboard = document.getElementById('tab-btn-dashboard');
|
||
const btnTools = document.getElementById('tab-btn-tools');
|
||
const panelDashboard = document.getElementById('tab-dashboard');
|
||
const panelTools = document.getElementById('tab-tools');
|
||
|
||
function select(panelToOpen, btn){
|
||
[panelDashboard, panelTools].forEach(p => p.setAttribute('data-open','false'));
|
||
panelToOpen.setAttribute('data-open','true');
|
||
[btnDashboard, btnTools].forEach(b => b.setAttribute('aria-selected','false'));
|
||
btn.setAttribute('aria-selected','true');
|
||
// if Tools panel opened, refresh disk usage and lists
|
||
if (panelToOpen === panelTools){
|
||
try{ refreshToolsLists(); }catch(e){}
|
||
try{ refreshDiskUsage(); }catch(e){}
|
||
}
|
||
}
|
||
|
||
btnDashboard.addEventListener('click', () => select(panelDashboard, btnDashboard));
|
||
btnTools.addEventListener('click', () => select(panelTools, btnTools));
|
||
|
||
// toggle a class on the .tabs wrapper so we can widen it when Tools is active
|
||
const tabsWrapper = document.querySelector('.tabs');
|
||
function updateTabsWideState(){
|
||
if (panelTools.getAttribute('data-open') === 'true') tabsWrapper.classList.add('tools-wide'); else tabsWrapper.classList.remove('tools-wide');
|
||
}
|
||
// watch clicks to update state
|
||
btnDashboard.addEventListener('click', updateTabsWideState);
|
||
btnTools.addEventListener('click', updateTabsWideState);
|
||
|
||
// and set initial state
|
||
updateTabsWideState();
|
||
}
|
||
|
||
// debounce helper for resize
|
||
function debounce(fn, wait){
|
||
let t;
|
||
return function(...args){ clearTimeout(t); t = setTimeout(() => fn.apply(this,args), wait); };
|
||
}
|
||
|
||
/* Tools UI: render items array into checkbox list */
|
||
function renderItems(listId, items){
|
||
const list = document.getElementById(listId);
|
||
if (!list) return;
|
||
list.innerHTML = '';
|
||
(items || []).forEach((it, idx) => {
|
||
const id = listId + '-item-' + idx;
|
||
const label = document.createElement('label');
|
||
label.className = 'checkbox-inline';
|
||
label.style.display = 'flex';
|
||
label.style.alignItems = 'center';
|
||
label.style.justifyContent = 'flex-start';
|
||
label.style.gap = '8px';
|
||
const cb = document.createElement('input');
|
||
cb.type = 'checkbox';
|
||
|
||
let value, text;
|
||
if (typeof it === 'string'){
|
||
value = it;
|
||
text = it;
|
||
} else if (it && typeof it === 'object'){
|
||
value = it.folder_path || it.path || it.value || JSON.stringify(it);
|
||
text = it.folder_name_display || it.name || it.label || value;
|
||
} else {
|
||
value = String(it);
|
||
text = String(it);
|
||
}
|
||
|
||
cb.value = value;
|
||
cb.id = id;
|
||
const span = document.createElement('span');
|
||
span.textContent = text;
|
||
label.appendChild(cb);
|
||
label.appendChild(span);
|
||
list.appendChild(label);
|
||
});
|
||
}
|
||
|
||
async function refreshToolsLists(){
|
||
try{
|
||
const res = await fetch('../get_media_lists');
|
||
if (!res.ok) throw new Error('Network response not ok');
|
||
const data = await res.json();
|
||
|
||
let usbItems = [];
|
||
if (Array.isArray(data.usb)) usbItems = data.usb;
|
||
else if (data.usb_folders){
|
||
if (Array.isArray(data.usb_folders.folders)) usbItems = data.usb_folders.folders;
|
||
else if (Array.isArray(data.usb_folders)) usbItems = data.usb_folders;
|
||
}
|
||
|
||
let pcItems = [];
|
||
if (Array.isArray(data.pc)) pcItems = data.pc;
|
||
else if (data.desk_folders){
|
||
if (Array.isArray(data.desk_folders.folders)) pcItems = data.desk_folders.folders;
|
||
else if (Array.isArray(data.desk_folders)) pcItems = data.desk_folders;
|
||
}
|
||
|
||
if (usbItems.length === 0 && Array.isArray(data.usb_folders)) usbItems = data.usb_folders;
|
||
if (pcItems.length === 0 && Array.isArray(data.desk_folders)) pcItems = data.desk_folders;
|
||
|
||
//console.log('Refreshing media lists:', { usbItems, pcItems });
|
||
|
||
renderItems('toolsLeftList', usbItems || []);
|
||
renderItems('toolsRightList', pcItems || []);
|
||
setText('toolsStatusMessage', 'Media lists refreshed.');
|
||
// refresh disk usage indicator shown in the Tools footer
|
||
try{ refreshDiskUsage(); }catch(e){ console.warn('refreshDiskUsage failed', e); }
|
||
}catch(err){
|
||
console.error('Error refreshing media lists:', err);
|
||
setText('toolsStatusMessage', 'Error refreshing media lists.');
|
||
}
|
||
}
|
||
|
||
/* Fetch disk usage from server and update the bar + text
|
||
Assumption: the server returns percent_used (0-100) and free_human. We color the bar red
|
||
when percent_used > 85 (i.e., disk is more than 85% full). The bar fill represents percent free. */
|
||
async function refreshDiskUsage(){
|
||
try{
|
||
const res = await fetch('../get_disk_usage');
|
||
if (!res.ok) throw new Error('Network response not ok');
|
||
const data = await res.json();
|
||
if (!data || data.status !== 'success') throw new Error('Bad disk usage response');
|
||
const total = Number(data.total) || 0;
|
||
const free = Number(data.free) || 0;
|
||
const percentUsed = Number(data.percent_used) || 0;
|
||
const percentFree = total > 0 ? Math.max(0, Math.min(100, (free / total) * 100)) : 0;
|
||
|
||
const fill = document.getElementById('diskUsageFill');
|
||
const text = document.getElementById('diskUsageText');
|
||
const statusMsg = document.getElementById('toolsStatusMessage');
|
||
if (fill) {
|
||
fill.style.width = percentFree.toFixed(1) + '%';
|
||
// color red when used > 85%
|
||
if (percentUsed > 85) fill.style.background = '#ef4444'; else fill.style.background = '#10b981';
|
||
}
|
||
if (text) text.textContent = (data.free_human || (free + ' B')) + ' free';
|
||
if (statusMsg){
|
||
statusMsg.textContent = `Disk: ${percentUsed.toFixed(1)}% used`;
|
||
}
|
||
}catch(err){
|
||
console.warn('Failed to refresh disk usage', err);
|
||
const statusMsg = document.getElementById('toolsStatusMessage');
|
||
if (statusMsg) statusMsg.textContent = 'Disk usage unavailable';
|
||
}
|
||
}
|
||
|
||
/* Confirmation modal utilities */
|
||
function showConfirmModal(message){
|
||
return new Promise((resolve) => {
|
||
let overlay = document.getElementById('confirmOverlay');
|
||
if (!overlay){
|
||
// create modal
|
||
overlay = document.createElement('div');
|
||
overlay.id = 'confirmOverlay';
|
||
overlay.className = 'confirm-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="confirm-box" role="dialog" aria-modal="true" aria-labelledby="confirmTitle">
|
||
<div id="confirmTitle" class="confirm-message"></div>
|
||
<div style="margin-top:10px; font-size:0.95rem; color:#374151;">These folders and their contents will be permanently deleted.</div>
|
||
<div class="confirm-actions">
|
||
<button id="confirmCancelBtn" class="btn" type="button">Cancel</button>
|
||
<button id="confirmYesBtn" class="btn" type="button" style="background:#dc2626">Yes</button>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(overlay);
|
||
const cancelBtn = overlay.querySelector('#confirmCancelBtn');
|
||
const yesBtn = overlay.querySelector('#confirmYesBtn');
|
||
const titleEl = overlay.querySelector('#confirmTitle');
|
||
|
||
cancelBtn.addEventListener('click', () => {
|
||
overlay.style.display = 'none';
|
||
resolve(false);
|
||
});
|
||
yesBtn.addEventListener('click', () => {
|
||
overlay.style.display = 'none';
|
||
resolve(true);
|
||
});
|
||
}
|
||
overlay.querySelector('.confirm-message').textContent = message || 'Warning, these folders and their contents will be deleted';
|
||
overlay.style.display = 'flex';
|
||
// Focus Cancel by default (per requirement)
|
||
const cancelBtn = overlay.querySelector('#confirmCancelBtn');
|
||
if (cancelBtn){
|
||
cancelBtn.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
async function deleteSelectedPC(){
|
||
const rightList = document.getElementById('toolsRightList');
|
||
if (!rightList) return;
|
||
const selected = Array.from(rightList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
|
||
if (!selected || selected.length === 0) return; // nothing selected
|
||
|
||
const ok = await showConfirmModal('Warning, these folders and their contents will be deleted');
|
||
if (!ok) return; // cancelled
|
||
|
||
// disable buttons while working
|
||
const btns = document.querySelectorAll('#tab-tools .tools-buttons .btn');
|
||
btns.forEach(b => b.disabled = true);
|
||
setText('toolsStatusMessage', 'Deleting...');
|
||
|
||
try{
|
||
const res = await fetch('../delete_pc_folders', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ items: selected })
|
||
});
|
||
if (!res.ok) throw new Error('Network response not ok');
|
||
const data = await res.json();
|
||
if (data && data.success){
|
||
setText('toolsStatusMessage', 'Delete completed. Refreshing lists...');
|
||
await refreshToolsLists();
|
||
try{ refreshDiskUsage(); }catch(e){}
|
||
} else {
|
||
setText('toolsStatusMessage', (data && data.message) ? data.message : 'Delete failed');
|
||
}
|
||
}catch(err){
|
||
console.error('Error during delete:', err);
|
||
setText('toolsStatusMessage', 'Error during delete operation.');
|
||
}finally{
|
||
btns.forEach(b => b.disabled = false);
|
||
}
|
||
}
|
||
|
||
/* Move/transfer selected items from one list to the other by calling the server.
|
||
The server endpoints expected (GET) are: ../transfer_usb_to_pc and ../transfer_pc_to_usb
|
||
with URL params: items (JSON-encoded array) and overwrite (true|false).
|
||
On success the server should return JSON { success: true } and the client will refresh lists. */
|
||
async function moveSelected(from, to){
|
||
const fromList = document.getElementById(from === 'left' ? 'toolsLeftList' : 'toolsRightList');
|
||
if (!fromList) return;
|
||
const selected = Array.from(fromList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
|
||
if (selected.length === 0) return; // nothing selected
|
||
|
||
// Determine overwrite checkbox depending on direction
|
||
const overwriteId = (from === 'left') ? 'overwriteLeft' : 'overwriteRight';
|
||
const overwrite = !!(document.getElementById(overwriteId) && document.getElementById(overwriteId).checked);
|
||
|
||
// choose endpoint
|
||
const endpoint = (from === 'left' && to === 'right') ? '../transfer_usb_to_pc' :
|
||
(from === 'right' && to === 'left') ? '../transfer_pc_to_usb' : null;
|
||
if (!endpoint) return;
|
||
|
||
// disable buttons while working
|
||
const btns = document.querySelectorAll('#tab-tools .tools-buttons .btn');
|
||
btns.forEach(b => b.disabled = true);
|
||
setText('toolsStatusMessage', 'Transferring...');
|
||
|
||
try{
|
||
const itemsParam = encodeURIComponent(JSON.stringify(selected));
|
||
const url = endpoint + '?items=' + itemsParam + '&overwrite=' + (overwrite ? 'true' : 'false');
|
||
const res = await fetch(url, { method: 'GET' });
|
||
if (!res.ok) throw new Error('Network response not ok');
|
||
const data = await res.json();
|
||
// Support both response shapes: { success: true } and { status: 'success' }
|
||
const okResponse = data && ((typeof data.success !== 'undefined' && data.success === true) || data.status === 'success');
|
||
if (okResponse){
|
||
setText('toolsStatusMessage', 'Transfer successful. Refreshing lists...');
|
||
await refreshToolsLists();
|
||
} else {
|
||
console.error('Transfer failed', data);
|
||
setText('toolsStatusMessage', (data && data.message) ? data.message : 'Transfer failed');
|
||
}
|
||
}catch(err){
|
||
console.error('Error during transfer:', err);
|
||
setText('toolsStatusMessage', 'Error during transfer.');
|
||
}finally{
|
||
btns.forEach(b => b.disabled = false);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|