chore: improved web layout
This commit is contained in:
@@ -104,10 +104,51 @@
|
||||
|
||||
.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) {
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
.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>
|
||||
</head>
|
||||
@@ -119,7 +160,6 @@
|
||||
<button class="tab" onclick="showPage('jobs')">Jobs</button>
|
||||
<button class="tab" onclick="showPage('files')">Files</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>
|
||||
|
||||
@@ -151,37 +191,44 @@
|
||||
<option value="mp3" selected>MP3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="video_format">Video Format</label>
|
||||
<select id="video_format">
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cover_size">Cover Size</label>
|
||||
<select id="cover_size">
|
||||
<option value="small">Small</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large" selected>Large</option>
|
||||
<option value="extra-large">Extra Large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="download_mode">Download Mode</label>
|
||||
<select id="download_mode">
|
||||
<option value="ytdlp">yt-dlp</option>
|
||||
<option value="aria2c">aria2c</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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_playlist"> Save Playlist File</label>
|
||||
<label class="checkbox-label"><input type="checkbox" id="overwrite"> Overwrite Existing</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>
|
||||
<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>
|
||||
<label for="video_format">Video Format</label>
|
||||
<select id="video_format">
|
||||
<option value="mp4">MP4</option>
|
||||
<option value="webm">WebM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cover_size">Cover Size</label>
|
||||
<select id="cover_size">
|
||||
<option value="small">Small</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large" selected>Large</option>
|
||||
<option value="extra-large">Extra Large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="download_mode">Download Mode</label>
|
||||
<select id="download_mode">
|
||||
<option value="ytdlp">yt-dlp</option>
|
||||
<option value="aria2c">aria2c</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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_playlist"> Save Playlist File</label>
|
||||
<label class="checkbox-label"><input type="checkbox" id="overwrite"> Overwrite Existing</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn" id="btn-download" onclick="startDownload()">Start Download</button>
|
||||
@@ -207,17 +254,23 @@
|
||||
|
||||
<!-- SETTINGS 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">
|
||||
<h2>Cookies</h2>
|
||||
<div class="cookie-status" id="cookie-status">
|
||||
<div class="dot dot-red"></div>
|
||||
<span>Checking...</span>
|
||||
</div>
|
||||
<br>
|
||||
<label>Upload cookies.txt (Netscape format)</label>
|
||||
<input type="file" id="cookie-file" accept=".txt" style="margin-top:8px">
|
||||
<br><br>
|
||||
<button class="btn btn-sm btn-secondary" onclick="uploadCookies()">Upload</button>
|
||||
<div style="margin-top:16px">
|
||||
<label>Upload cookies.txt (Netscape format)</label>
|
||||
<input type="file" id="cookie-file" accept=".txt" style="margin-top:8px; display:block">
|
||||
<button class="btn btn-sm btn-secondary" style="margin-top:12px" onclick="uploadCookies()">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Widevine Device</h2>
|
||||
@@ -225,15 +278,36 @@
|
||||
<div class="dot dot-red"></div>
|
||||
<span>Checking...</span>
|
||||
</div>
|
||||
<br>
|
||||
<label>Upload device.wvd (required for AAC quality)</label>
|
||||
<input type="file" id="wvd-file" accept=".wvd" style="margin-top:8px">
|
||||
<br><br>
|
||||
<button class="btn btn-sm btn-secondary" onclick="uploadWvd()">Upload</button>
|
||||
<div style="margin-top:16px">
|
||||
<label>Upload device.wvd (required for AAC quality)</label>
|
||||
<input type="file" id="wvd-file" accept=".wvd" style="margin-top:8px; display:block">
|
||||
<button class="btn btn-sm btn-secondary" style="margin-top:12px" onclick="uploadWvd()">Upload</button>
|
||||
</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>
|
||||
let currentPath = "";
|
||||
let jobPollInterval = null;
|
||||
@@ -245,6 +319,7 @@
|
||||
document.getElementById('page-' + name).classList.add('active');
|
||||
const matchingTab = document.querySelector(`.tab[onclick="showPage('${name}')"]`);
|
||||
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 === 'files') loadFiles("");
|
||||
@@ -285,10 +360,10 @@
|
||||
document.getElementById('urls').value = '';
|
||||
document.querySelector('[onclick="showPage(\'jobs\')"]').click();
|
||||
} else {
|
||||
alert(result.error || 'Failed to start download');
|
||||
showToast(result.error || 'Failed to start download', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
showToast('Error: ' + e.message, 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -429,7 +504,7 @@
|
||||
const encodedPath = encodeURIComponent(f.path);
|
||||
if (f.is_dir) {
|
||||
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>
|
||||
<div class="file-actions">
|
||||
<button class="btn-icon" onclick="window.location='/api/files/download-folder?path=${encodedPath}'" title="Download as ZIP">⬇</button>
|
||||
@@ -439,7 +514,7 @@
|
||||
} else {
|
||||
const size = formatSize(f.size);
|
||||
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-size">${size}</span>
|
||||
<div class="file-actions">
|
||||
@@ -487,20 +562,20 @@
|
||||
|
||||
async function uploadCookies() {
|
||||
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();
|
||||
form.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/cookies', { method: 'POST', body: form });
|
||||
if (res.ok) {
|
||||
alert('Cookies uploaded successfully');
|
||||
showToast('Cookies uploaded successfully');
|
||||
checkCookies();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Upload failed');
|
||||
showToast(data.error || 'Upload failed', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
showToast('Error: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,20 +596,20 @@
|
||||
|
||||
async function uploadWvd() {
|
||||
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();
|
||||
form.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/wvd', { method: 'POST', body: form });
|
||||
if (res.ok) {
|
||||
alert('WVD file uploaded successfully');
|
||||
showToast('WVD file uploaded successfully');
|
||||
checkWvd();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Upload failed');
|
||||
showToast(data.error || 'Upload failed', 'error');
|
||||
}
|
||||
} 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,'>');
|
||||
}
|
||||
|
||||
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
|
||||
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'];
|
||||
@@ -583,6 +694,10 @@
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
if (localStorage.getItem('votify-advanced-open')) {
|
||||
document.getElementById('advanced-section').classList.add('open');
|
||||
document.getElementById('advanced-toggle').classList.add('open');
|
||||
}
|
||||
checkCookies();
|
||||
checkWvd();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user