mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Download button gating, format defaults fix, layout/UX polish
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
This commit is contained in:
parent
635da2be82
commit
87f7996d5d
7 changed files with 45 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,7 +22,17 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<AppHeader />
|
||||
<router-view />
|
||||
<AppFooter />
|
||||
<div class="app-root">
|
||||
<AppHeader />
|
||||
<router-view />
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.app-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const activeTab = ref<MobileTab>('submit')
|
|||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -441,11 +441,13 @@ async function clearJob(jobId: string): Promise<void> {
|
|||
}
|
||||
|
||||
.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<void> {
|
|||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
|
|
|
|||
|
|
@ -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<string>('')
|
||||
const analyzeError = ref<string | null>(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<Set<number>>(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<void> {
|
||||
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 {
|
|||
<button
|
||||
class="btn-download"
|
||||
@click="submitDownload"
|
||||
:disabled="!url.trim() || store.isSubmitting"
|
||||
:disabled="!urlReady || isAnalyzing || store.isSubmitting"
|
||||
>
|
||||
{{ store.isSubmitting ? 'Submitting…' : 'Download' }}
|
||||
</button>
|
||||
|
|
@ -378,6 +393,10 @@ function formatTooltip(fmt: string): string {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="analyzeError" class="extract-error">
|
||||
{{ analyzeError }}
|
||||
</div>
|
||||
|
||||
<div v-if="extractError" class="extract-error">
|
||||
{{ extractError }}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue