chore: improved web layout
This commit is contained in:
@@ -104,10 +104,51 @@
|
|||||||
|
|
||||||
.empty { text-align: center; padding: 40px; color: var(--text2); }
|
.empty { text-align: center; padding: 40px; color: var(--text2); }
|
||||||
|
|
||||||
|
/* Advanced options toggle */
|
||||||
|
.advanced-toggle { background: none; border: none; color: var(--text2); cursor: pointer; font-size: 0.85rem; padding: 4px 0; display: flex; align-items: center; gap: 6px; font-family: inherit; margin-bottom: 8px; transition: color 0.15s; }
|
||||||
|
.advanced-toggle:hover { color: var(--text); }
|
||||||
|
.advanced-toggle .chevron { display: inline-block; transition: transform 0.2s; font-size: 0.7rem; }
|
||||||
|
.advanced-toggle.open .chevron { transform: rotate(90deg); }
|
||||||
|
.advanced-section { max-height: 0; overflow: hidden; transition: max-height 0.35s ease; }
|
||||||
|
.advanced-section.open { max-height: 600px; }
|
||||||
|
|
||||||
|
/* Drag-and-drop feedback */
|
||||||
|
textarea.drag-over { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(29,185,84,0.25); }
|
||||||
|
textarea.drop-flash { animation: dropFlash 0.6s ease; }
|
||||||
|
@keyframes dropFlash { 0%,100% { box-shadow: none; } 30%,70% { box-shadow: 0 0 0 3px var(--accent); border-color: var(--accent); } }
|
||||||
|
|
||||||
|
/* Running job pulse */
|
||||||
|
.status-running { animation: statusPulse 1.8s ease-in-out infinite; }
|
||||||
|
@keyframes statusPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border-radius: var(--radius); padding: 12px 18px; font-size: 0.88rem; box-shadow: 0 4px 20px rgba(0,0,0,0.5); opacity: 0; transform: translateY(8px); transition: opacity 0.25s, transform 0.25s; pointer-events: none; max-width: 320px; z-index: 999; border-left: 3px solid var(--accent); }
|
||||||
|
#toast.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
||||||
|
#toast.error { border-left-color: var(--danger); }
|
||||||
|
|
||||||
|
/* Bottom navigation bar (mobile only) */
|
||||||
|
#bottom-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; height: 56px; background: var(--surface); border-top: 1px solid var(--surface2); z-index: 100; }
|
||||||
|
.bottom-tab { flex: 1; background: none; border: none; color: var(--text2); cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; font-size: 0.62rem; font-family: inherit; padding: 0; transition: color 0.15s; -webkit-tap-highlight-color: transparent; }
|
||||||
|
.bottom-tab .btab-icon { display: flex; }
|
||||||
|
.bottom-tab.active { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Touch targets */
|
||||||
|
.file-actions .btn-icon { padding: 8px 12px; min-width: 36px; min-height: 36px; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
|
.file-item { min-height: 48px; }
|
||||||
|
.checkbox-label { padding: 6px 0; }
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.form-row { grid-template-columns: 1fr; }
|
.form-row { grid-template-columns: 1fr; }
|
||||||
.header { flex-wrap: wrap; }
|
.header { flex-wrap: wrap; }
|
||||||
.tabs { margin-left: 0; width: 100%; }
|
.tabs { display: none; }
|
||||||
|
.container { padding: 16px; }
|
||||||
|
.card { padding: 16px; }
|
||||||
|
body { padding-bottom: 64px; }
|
||||||
|
textarea, select, input { font-size: 1rem; }
|
||||||
|
#btn-download { width: 100%; border-radius: var(--radius); }
|
||||||
|
.job-preview { max-width: calc(100vw - 120px); }
|
||||||
|
#toast { left: 16px; right: 16px; bottom: 72px; max-width: none; }
|
||||||
|
#bottom-nav { display: flex; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -119,7 +160,6 @@
|
|||||||
<button class="tab" onclick="showPage('jobs')">Jobs</button>
|
<button class="tab" onclick="showPage('jobs')">Jobs</button>
|
||||||
<button class="tab" onclick="showPage('files')">Files</button>
|
<button class="tab" onclick="showPage('files')">Files</button>
|
||||||
<button class="tab" onclick="showPage('settings')">Settings</button>
|
<button class="tab" onclick="showPage('settings')">Settings</button>
|
||||||
{% if auth_enabled %}<a href="/logout" class="tab" style="text-decoration:none">Logout</a>{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,6 +191,13 @@
|
|||||||
<option value="mp3" selected>MP3</option>
|
<option value="mp3" selected>MP3</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="advanced-toggle" id="advanced-toggle" onclick="toggleAdvanced()">
|
||||||
|
<span class="chevron">▶</span> Advanced options
|
||||||
|
</button>
|
||||||
|
<div class="advanced-section" id="advanced-section">
|
||||||
|
<div class="form-row">
|
||||||
<div>
|
<div>
|
||||||
<label for="video_format">Video Format</label>
|
<label for="video_format">Video Format</label>
|
||||||
<select id="video_format">
|
<select id="video_format">
|
||||||
@@ -175,7 +222,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox-row">
|
<div class="checkbox-row">
|
||||||
<label class="checkbox-label"><input type="checkbox" id="save_cover"> Save Cover Art</label>
|
<label class="checkbox-label"><input type="checkbox" id="save_cover"> Save Cover Art</label>
|
||||||
<label class="checkbox-label"><input type="checkbox" id="save_playlist"> Save Playlist File</label>
|
<label class="checkbox-label"><input type="checkbox" id="save_playlist"> Save Playlist File</label>
|
||||||
@@ -183,6 +229,7 @@
|
|||||||
<label class="checkbox-label"><input type="checkbox" id="download_music_videos"> Download Music Videos</label>
|
<label class="checkbox-label"><input type="checkbox" id="download_music_videos"> Download Music Videos</label>
|
||||||
<label class="checkbox-label"><input type="checkbox" id="no_lrc"> No Lyrics</label>
|
<label class="checkbox-label"><input type="checkbox" id="no_lrc"> No Lyrics</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn" id="btn-download" onclick="startDownload()">Start Download</button>
|
<button class="btn" id="btn-download" onclick="startDownload()">Start Download</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,17 +254,23 @@
|
|||||||
|
|
||||||
<!-- SETTINGS PAGE -->
|
<!-- SETTINGS PAGE -->
|
||||||
<div id="page-settings" class="page">
|
<div id="page-settings" class="page">
|
||||||
|
{% if auth_enabled %}
|
||||||
|
<div class="card">
|
||||||
|
<h2>Account</h2>
|
||||||
|
<a href="/logout" class="btn btn-danger btn-sm" style="text-decoration:none">Logout</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Cookies</h2>
|
<h2>Cookies</h2>
|
||||||
<div class="cookie-status" id="cookie-status">
|
<div class="cookie-status" id="cookie-status">
|
||||||
<div class="dot dot-red"></div>
|
<div class="dot dot-red"></div>
|
||||||
<span>Checking...</span>
|
<span>Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<div style="margin-top:16px">
|
||||||
<label>Upload cookies.txt (Netscape format)</label>
|
<label>Upload cookies.txt (Netscape format)</label>
|
||||||
<input type="file" id="cookie-file" accept=".txt" style="margin-top:8px">
|
<input type="file" id="cookie-file" accept=".txt" style="margin-top:8px; display:block">
|
||||||
<br><br>
|
<button class="btn btn-sm btn-secondary" style="margin-top:12px" onclick="uploadCookies()">Upload</button>
|
||||||
<button class="btn btn-sm btn-secondary" onclick="uploadCookies()">Upload</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Widevine Device</h2>
|
<h2>Widevine Device</h2>
|
||||||
@@ -225,14 +278,35 @@
|
|||||||
<div class="dot dot-red"></div>
|
<div class="dot dot-red"></div>
|
||||||
<span>Checking...</span>
|
<span>Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<div style="margin-top:16px">
|
||||||
<label>Upload device.wvd (required for AAC quality)</label>
|
<label>Upload device.wvd (required for AAC quality)</label>
|
||||||
<input type="file" id="wvd-file" accept=".wvd" style="margin-top:8px">
|
<input type="file" id="wvd-file" accept=".wvd" style="margin-top:8px; display:block">
|
||||||
<br><br>
|
<button class="btn btn-sm btn-secondary" style="margin-top:12px" onclick="uploadWvd()">Upload</button>
|
||||||
<button class="btn btn-sm btn-secondary" onclick="uploadWvd()">Upload</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav id="bottom-nav">
|
||||||
|
<button class="bottom-tab active" data-page="download" onclick="showPage('download')">
|
||||||
|
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></span>
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
<button class="bottom-tab" data-page="jobs" onclick="showPage('jobs')">
|
||||||
|
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg></span>
|
||||||
|
<span>Jobs</span>
|
||||||
|
</button>
|
||||||
|
<button class="bottom-tab" data-page="files" onclick="showPage('files')">
|
||||||
|
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
||||||
|
<span>Files</span>
|
||||||
|
</button>
|
||||||
|
<button class="bottom-tab" data-page="settings" onclick="showPage('settings')">
|
||||||
|
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg></span>
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentPath = "";
|
let currentPath = "";
|
||||||
@@ -245,6 +319,7 @@
|
|||||||
document.getElementById('page-' + name).classList.add('active');
|
document.getElementById('page-' + name).classList.add('active');
|
||||||
const matchingTab = document.querySelector(`.tab[onclick="showPage('${name}')"]`);
|
const matchingTab = document.querySelector(`.tab[onclick="showPage('${name}')"]`);
|
||||||
if (matchingTab) matchingTab.classList.add('active');
|
if (matchingTab) matchingTab.classList.add('active');
|
||||||
|
document.querySelectorAll('.bottom-tab').forEach(t => t.classList.toggle('active', t.dataset.page === name));
|
||||||
|
|
||||||
if (name === 'jobs') loadJobs();
|
if (name === 'jobs') loadJobs();
|
||||||
if (name === 'files') loadFiles("");
|
if (name === 'files') loadFiles("");
|
||||||
@@ -285,10 +360,10 @@
|
|||||||
document.getElementById('urls').value = '';
|
document.getElementById('urls').value = '';
|
||||||
document.querySelector('[onclick="showPage(\'jobs\')"]').click();
|
document.querySelector('[onclick="showPage(\'jobs\')"]').click();
|
||||||
} else {
|
} else {
|
||||||
alert(result.error || 'Failed to start download');
|
showToast(result.error || 'Failed to start download', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error: ' + e.message);
|
showToast('Error: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -429,7 +504,7 @@
|
|||||||
const encodedPath = encodeURIComponent(f.path);
|
const encodedPath = encodeURIComponent(f.path);
|
||||||
if (f.is_dir) {
|
if (f.is_dir) {
|
||||||
return `<li class="file-item">
|
return `<li class="file-item">
|
||||||
<span class="file-icon">📁</span>
|
<span class="file-icon"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
||||||
<span class="file-name" onclick="loadFiles(this.dataset.p)" data-p="${safePath}">${escapeHtml(f.name)}</span>
|
<span class="file-name" onclick="loadFiles(this.dataset.p)" data-p="${safePath}">${escapeHtml(f.name)}</span>
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<button class="btn-icon" onclick="window.location='/api/files/download-folder?path=${encodedPath}'" title="Download as ZIP">⬇</button>
|
<button class="btn-icon" onclick="window.location='/api/files/download-folder?path=${encodedPath}'" title="Download as ZIP">⬇</button>
|
||||||
@@ -439,7 +514,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const size = formatSize(f.size);
|
const size = formatSize(f.size);
|
||||||
return `<li class="file-item">
|
return `<li class="file-item">
|
||||||
<span class="file-icon">🎵</span>
|
<span class="file-icon"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>
|
||||||
<span class="file-name" onclick="window.location='/api/files/download?path=${encodedPath}'">${escapeHtml(f.name)}</span>
|
<span class="file-name" onclick="window.location='/api/files/download?path=${encodedPath}'">${escapeHtml(f.name)}</span>
|
||||||
<span class="file-size">${size}</span>
|
<span class="file-size">${size}</span>
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
@@ -487,20 +562,20 @@
|
|||||||
|
|
||||||
async function uploadCookies() {
|
async function uploadCookies() {
|
||||||
const file = document.getElementById('cookie-file').files[0];
|
const file = document.getElementById('cookie-file').files[0];
|
||||||
if (!file) { alert('Select a file first'); return; }
|
if (!file) { showToast('Select a file first', 'error'); return; }
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/cookies', { method: 'POST', body: form });
|
const res = await fetch('/api/cookies', { method: 'POST', body: form });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('Cookies uploaded successfully');
|
showToast('Cookies uploaded successfully');
|
||||||
checkCookies();
|
checkCookies();
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.error || 'Upload failed');
|
showToast(data.error || 'Upload failed', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error: ' + e.message);
|
showToast('Error: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,20 +596,20 @@
|
|||||||
|
|
||||||
async function uploadWvd() {
|
async function uploadWvd() {
|
||||||
const file = document.getElementById('wvd-file').files[0];
|
const file = document.getElementById('wvd-file').files[0];
|
||||||
if (!file) { alert('Select a file first'); return; }
|
if (!file) { showToast('Select a file first', 'error'); return; }
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/wvd', { method: 'POST', body: form });
|
const res = await fetch('/api/wvd', { method: 'POST', body: form });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('WVD file uploaded successfully');
|
showToast('WVD file uploaded successfully');
|
||||||
checkWvd();
|
checkWvd();
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.error || 'Upload failed');
|
showToast(data.error || 'Upload failed', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error: ' + e.message);
|
showToast('Error: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,6 +631,42 @@
|
|||||||
return str.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');
|
return str.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showToast(msg, type) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
toast.textContent = msg;
|
||||||
|
toast.className = 'show' + (type === 'error' ? ' error' : '');
|
||||||
|
clearTimeout(toast._timer);
|
||||||
|
toast._timer = setTimeout(() => { toast.className = ''; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAdvanced() {
|
||||||
|
const section = document.getElementById('advanced-section');
|
||||||
|
const toggle = document.getElementById('advanced-toggle');
|
||||||
|
const isOpen = section.classList.toggle('open');
|
||||||
|
toggle.classList.toggle('open', isOpen);
|
||||||
|
localStorage.setItem('votify-advanced-open', isOpen ? '1' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag-and-drop Spotify URLs onto the page
|
||||||
|
document.addEventListener('dragover', e => e.preventDefault());
|
||||||
|
document.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = e.dataTransfer.getData('text') || e.dataTransfer.getData('text/uri-list') || '';
|
||||||
|
const lines = text.split(/[\r\n\s]+/).filter(l => l.includes('open.spotify.com'));
|
||||||
|
if (!lines.length) return;
|
||||||
|
const ta = document.getElementById('urls');
|
||||||
|
ta.value = (ta.value.trim() ? ta.value.trim() + '\n' : '') + lines.join('\n');
|
||||||
|
ta.classList.remove('drop-flash');
|
||||||
|
void ta.offsetWidth; // reflow to restart animation
|
||||||
|
ta.classList.add('drop-flash');
|
||||||
|
setTimeout(() => ta.classList.remove('drop-flash'), 650);
|
||||||
|
showPage('download');
|
||||||
|
});
|
||||||
|
const urlTextarea = document.getElementById('urls');
|
||||||
|
urlTextarea.addEventListener('dragover', () => urlTextarea.classList.add('drag-over'));
|
||||||
|
urlTextarea.addEventListener('dragleave', () => urlTextarea.classList.remove('drag-over'));
|
||||||
|
urlTextarea.addEventListener('drop', () => urlTextarea.classList.remove('drag-over'));
|
||||||
|
|
||||||
// Remember settings
|
// Remember settings
|
||||||
const SETTINGS_KEY = 'votify-settings';
|
const SETTINGS_KEY = 'votify-settings';
|
||||||
const SETTING_IDS = ['audio_quality', 'output_format', 'video_format', 'cover_size', 'download_mode', 'save_cover', 'save_playlist', 'overwrite', 'download_music_videos', 'no_lrc'];
|
const SETTING_IDS = ['audio_quality', 'output_format', 'video_format', 'cover_size', 'download_mode', 'save_cover', 'save_playlist', 'overwrite', 'download_music_videos', 'no_lrc'];
|
||||||
@@ -583,6 +694,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
if (localStorage.getItem('votify-advanced-open')) {
|
||||||
|
document.getElementById('advanced-section').classList.add('open');
|
||||||
|
document.getElementById('advanced-toggle').classList.add('open');
|
||||||
|
}
|
||||||
checkCookies();
|
checkCookies();
|
||||||
checkWvd();
|
checkWvd();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user