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,
|
"no_warnings": True,
|
||||||
"noprogress": True,
|
"noprogress": True,
|
||||||
"noplaylist": True, # Individual jobs — don't re-expand playlists
|
"noplaylist": True, # Individual jobs — don't re-expand playlists
|
||||||
|
"overwrites": True, # Allow re-downloading same URL with different format
|
||||||
}
|
}
|
||||||
if job_create.format_id:
|
if job_create.format_id:
|
||||||
opts["format"] = job_create.format_id
|
opts["format"] = job_create.format_id
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,17 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="app-root">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<router-view />
|
<router-view />
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAdminStore } from '@/stores/admin'
|
import { useAdminStore } from '@/stores/admin'
|
||||||
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import AdminLogin from './AdminLogin.vue'
|
import AdminLogin from './AdminLogin.vue'
|
||||||
|
|
||||||
const store = useAdminStore()
|
const store = useAdminStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const activeTab = ref<'sessions' | 'storage' | 'purge' | 'settings'>('sessions')
|
const activeTab = ref<'sessions' | 'storage' | 'purge' | 'settings'>('sessions')
|
||||||
|
|
||||||
|
|
@ -52,6 +54,8 @@ async function saveSettings() {
|
||||||
default_audio_format: defaultAudioFormat.value,
|
default_audio_format: defaultAudioFormat.value,
|
||||||
})
|
})
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
// Reload public config so main page picks up new defaults
|
||||||
|
await configStore.loadConfig()
|
||||||
settingsSaved.value = true
|
settingsSaved.value = true
|
||||||
setTimeout(() => { settingsSaved.value = false }, 3000)
|
setTimeout(() => { settingsSaved.value = false }, 3000)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ const activeTab = ref<MobileTab>('submit')
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: calc(100vh - var(--header-height));
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-main {
|
.layout-main {
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ function handleClear(): void {
|
||||||
|
|
||||||
.btn-clear {
|
.btn-clear {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
|
min-width: 70px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
padding: var(--space-xs) var(--space-md);
|
padding: var(--space-xs) var(--space-md);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
|
|
|
||||||
|
|
@ -441,11 +441,13 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -454,6 +456,8 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover {
|
.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
|
// Unified loading state for URL check + format extraction
|
||||||
const isAnalyzing = ref(false)
|
const isAnalyzing = ref(false)
|
||||||
const analyzePhase = ref<string>('')
|
const analyzePhase = ref<string>('')
|
||||||
|
const analyzeError = ref<string | null>(null)
|
||||||
const phaseMessages = [
|
const phaseMessages = [
|
||||||
'Peeking at the URL…',
|
'Peeking at the URL…',
|
||||||
'Interrogating the server…',
|
'Interrogating the server…',
|
||||||
|
|
@ -95,6 +96,7 @@ watch(url, (newUrl) => {
|
||||||
formats.value = []
|
formats.value = []
|
||||||
selectedFormatId.value = null
|
selectedFormatId.value = null
|
||||||
extractError.value = null
|
extractError.value = null
|
||||||
|
analyzeError.value = null
|
||||||
audioLocked.value = false
|
audioLocked.value = false
|
||||||
showOptions.value = false
|
showOptions.value = false
|
||||||
selectedEntries.value = new Set()
|
selectedEntries.value = new Set()
|
||||||
|
|
@ -104,6 +106,7 @@ watch(url, (newUrl) => {
|
||||||
formats.value = []
|
formats.value = []
|
||||||
selectedFormatId.value = null
|
selectedFormatId.value = null
|
||||||
extractError.value = null
|
extractError.value = null
|
||||||
|
analyzeError.value = null
|
||||||
audioLocked.value = false
|
audioLocked.value = false
|
||||||
selectedEntries.value = new Set()
|
selectedEntries.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +118,11 @@ const selectedEntries = ref<Set<number>>(new Set())
|
||||||
/** Whether formats have been fetched for the current URL. */
|
/** Whether formats have been fetched for the current URL. */
|
||||||
const formatsReady = computed(() => formats.value.length > 0)
|
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> {
|
async function extractFormats(): Promise<void> {
|
||||||
const trimmed = url.value.trim()
|
const trimmed = url.value.trim()
|
||||||
if (!trimmed) return
|
if (!trimmed) return
|
||||||
|
|
@ -180,9 +188,16 @@ function handlePaste(): void {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (url.value.trim()) {
|
if (url.value.trim()) {
|
||||||
isAnalyzing.value = true
|
isAnalyzing.value = true
|
||||||
|
analyzeError.value = null
|
||||||
startAnalyzePhase()
|
startAnalyzePhase()
|
||||||
try {
|
try {
|
||||||
await Promise.all([extractFormats(), fetchUrlInfo()])
|
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 {
|
} finally {
|
||||||
isAnalyzing.value = false
|
isAnalyzing.value = false
|
||||||
stopAnalyzePhase()
|
stopAnalyzePhase()
|
||||||
|
|
@ -325,7 +340,7 @@ function formatTooltip(fmt: string): string {
|
||||||
<button
|
<button
|
||||||
class="btn-download"
|
class="btn-download"
|
||||||
@click="submitDownload"
|
@click="submitDownload"
|
||||||
:disabled="!url.trim() || store.isSubmitting"
|
:disabled="!urlReady || isAnalyzing || store.isSubmitting"
|
||||||
>
|
>
|
||||||
{{ store.isSubmitting ? 'Submitting…' : 'Download' }}
|
{{ store.isSubmitting ? 'Submitting…' : 'Download' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -378,6 +393,10 @@ function formatTooltip(fmt: string): string {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="analyzeError" class="extract-error">
|
||||||
|
{{ analyzeError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="extractError" class="extract-error">
|
<div v-if="extractError" class="extract-error">
|
||||||
{{ extractError }}
|
{{ extractError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue