From 87f7996d5d002d0e82174106bf6f76cd5ceea7a4 Mon Sep 17 00:00:00 2001 From: xpltd Date: Thu, 19 Mar 2026 04:50:52 -0500 Subject: [PATCH] Download button gating, format defaults fix, layout/UX polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Download button: - Disabled until URL analysis confirms downloadable content - Shows error message for invalid URLs or pages with no media - analyzeError state resets when URL is cleared or changed Admin format defaults fix: - AdminPanel now reloads configStore after saving settings - Previously the main page kept stale config until full page refresh - Config store import added to AdminPanel Re-download same URL: - Added overwrites: true to yt-dlp opts so re-downloading the same URL with different format options works correctly - Previously yt-dlp would skip if intermediate file existed UI polish: - Clear button fixed-width (min-width: 70px) — no shift between 'Clear' and 'Sure?' states - Action buttons uniform sizing (inline-flex, min-width/height, box-sizing: border-box) — download/copy/clear all same size - Footer no longer pushes below viewport — App.vue uses flex column layout, AppLayout uses flex:1 instead of min-height:100vh - Page only scrolls when content exceeds viewport --- backend/app/services/download.py | 1 + frontend/src/App.vue | 16 +++++++++++++--- frontend/src/components/AdminPanel.vue | 4 ++++ frontend/src/components/AppLayout.vue | 2 +- frontend/src/components/DownloadQueue.vue | 1 + frontend/src/components/DownloadTable.vue | 6 +++++- frontend/src/components/UrlInput.vue | 21 ++++++++++++++++++++- 7 files changed, 45 insertions(+), 6 deletions(-) diff --git a/backend/app/services/download.py b/backend/app/services/download.py index ed12622..4828b3b 100644 --- a/backend/app/services/download.py +++ b/backend/app/services/download.py @@ -159,6 +159,7 @@ class DownloadService: "no_warnings": True, "noprogress": True, "noplaylist": True, # Individual jobs — don't re-expand playlists + "overwrites": True, # Allow re-downloading same URL with different format } if job_create.format_id: opts["format"] = job_create.format_id diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8979e75..9ea3a13 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -22,7 +22,17 @@ onMounted(async () => { + + diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index 9e492b3..2344c5d 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -2,10 +2,12 @@ import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { useAdminStore } from '@/stores/admin' +import { useConfigStore } from '@/stores/config' import { api } from '@/api/client' import AdminLogin from './AdminLogin.vue' const store = useAdminStore() +const configStore = useConfigStore() const router = useRouter() const activeTab = ref<'sessions' | 'storage' | 'purge' | 'settings'>('sessions') @@ -52,6 +54,8 @@ async function saveSettings() { default_audio_format: defaultAudioFormat.value, }) if (ok) { + // Reload public config so main page picks up new defaults + await configStore.loadConfig() settingsSaved.value = true setTimeout(() => { settingsSaved.value = false }, 3000) } diff --git a/frontend/src/components/AppLayout.vue b/frontend/src/components/AppLayout.vue index 715ca4a..6a5bd60 100644 --- a/frontend/src/components/AppLayout.vue +++ b/frontend/src/components/AppLayout.vue @@ -52,7 +52,7 @@ const activeTab = ref('submit') .app-layout { display: flex; flex-direction: column; - min-height: calc(100vh - var(--header-height)); + flex: 1; } .layout-main { diff --git a/frontend/src/components/DownloadQueue.vue b/frontend/src/components/DownloadQueue.vue index be1b334..e429fd0 100644 --- a/frontend/src/components/DownloadQueue.vue +++ b/frontend/src/components/DownloadQueue.vue @@ -195,6 +195,7 @@ function handleClear(): void { .btn-clear { min-height: 36px; + min-width: 70px; font-size: var(--font-size-sm); padding: var(--space-xs) var(--space-md); background: var(--color-surface); diff --git a/frontend/src/components/DownloadTable.vue b/frontend/src/components/DownloadTable.vue index 07baa8c..0244717 100644 --- a/frontend/src/components/DownloadTable.vue +++ b/frontend/src/components/DownloadTable.vue @@ -441,11 +441,13 @@ async function clearJob(jobId: string): Promise { } .action-btn { - display: flex; + display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; + min-width: 30px; + min-height: 30px; padding: 0; background: transparent; border: 1px solid var(--color-border); @@ -454,6 +456,8 @@ async function clearJob(jobId: string): Promise { cursor: pointer; transition: all 0.15s ease; text-decoration: none; + box-sizing: border-box; + line-height: 1; } .action-btn:hover { diff --git a/frontend/src/components/UrlInput.vue b/frontend/src/components/UrlInput.vue index b0e1d7d..f83a679 100644 --- a/frontend/src/components/UrlInput.vue +++ b/frontend/src/components/UrlInput.vue @@ -22,6 +22,7 @@ const audioLocked = ref(false) // true when source is audio-only // Unified loading state for URL check + format extraction const isAnalyzing = ref(false) const analyzePhase = ref('') +const analyzeError = ref(null) const phaseMessages = [ 'Peeking at the URL…', 'Interrogating the server…', @@ -95,6 +96,7 @@ watch(url, (newUrl) => { formats.value = [] selectedFormatId.value = null extractError.value = null + analyzeError.value = null audioLocked.value = false showOptions.value = false selectedEntries.value = new Set() @@ -104,6 +106,7 @@ watch(url, (newUrl) => { formats.value = [] selectedFormatId.value = null extractError.value = null + analyzeError.value = null audioLocked.value = false selectedEntries.value = new Set() } @@ -115,6 +118,11 @@ const selectedEntries = ref>(new Set()) /** Whether formats have been fetched for the current URL. */ const formatsReady = computed(() => formats.value.length > 0) +/** URL has been analyzed and content was found. */ +const urlReady = computed(() => + !!urlInfo.value && urlInfo.value.type !== 'unknown' +) + async function extractFormats(): Promise { const trimmed = url.value.trim() if (!trimmed) return @@ -180,9 +188,16 @@ function handlePaste(): void { setTimeout(async () => { if (url.value.trim()) { isAnalyzing.value = true + analyzeError.value = null startAnalyzePhase() try { await Promise.all([extractFormats(), fetchUrlInfo()]) + // Check if URL yielded anything useful + if (urlInfo.value?.type === 'unknown') { + analyzeError.value = 'No downloadable media found at this URL.' + } else if (!urlInfo.value && !extractError.value) { + analyzeError.value = 'Could not reach this URL. Check the address and try again.' + } } finally { isAnalyzing.value = false stopAnalyzePhase() @@ -325,7 +340,7 @@ function formatTooltip(fmt: string): string { @@ -378,6 +393,10 @@ function formatTooltip(fmt: string): string { +
+ {{ analyzeError }} +
+
{{ extractError }}