Mobile queue badge + fix paste-then-download race condition

Mobile:
- Queue tab shows badge with active job count (queued/downloading)
- Badge hidden when user is already on Queue tab
- Styled as accent-colored pill with count (caps at 9+)

Paste race fix:
- Set isAnalyzing=true immediately on paste event, not after 50ms timeout
- Prevents Download button from being briefly clickable between paste
  and analysis start
- Handles edge case where URL is cleared before timeout fires
This commit is contained in:
xpltd 2026-03-22 16:37:03 -05:00
parent 1b5f24f796
commit f72b649acf
2 changed files with 44 additions and 2 deletions

View file

@ -1,13 +1,26 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
import { useDownloadsStore } from '@/stores/downloads'
import WireframeBackground from './WireframeBackground.vue'
const themeStore = useThemeStore()
const downloadsStore = useDownloadsStore()
const showWireframe = computed(() => themeStore.currentTheme === 'cyberpunk')
type MobileTab = 'submit' | 'queue'
const activeTab = ref<MobileTab>('submit')
/** Number of active (non-terminal) jobs — shown as badge on Queue tab */
const queueBadge = computed(() => {
let count = 0
for (const job of downloadsStore.jobs.values()) {
if (job.status === 'queued' || job.status === 'downloading' || job.status === 'extracting') {
count++
}
}
return count
})
</script>
<template>
@ -41,7 +54,10 @@ const activeTab = ref<MobileTab>('submit')
:class="{ active: activeTab === 'queue' }"
@click="activeTab = 'queue'"
>
<span class="nav-icon"></span>
<span class="nav-icon-wrap">
<span class="nav-icon"></span>
<span v-if="queueBadge > 0 && activeTab !== 'queue'" class="nav-badge">{{ queueBadge > 9 ? '9+' : queueBadge }}</span>
</span>
<span class="nav-label">Queue</span>
</button>
</nav>
@ -136,5 +152,26 @@ const activeTab = ref<MobileTab>('submit')
text-transform: uppercase;
letter-spacing: 0.05em;
}
.nav-icon-wrap {
position: relative;
display: inline-flex;
}
.nav-badge {
position: absolute;
top: -6px;
right: -10px;
background: var(--color-accent);
color: var(--color-bg);
font-size: 0.6rem;
font-weight: 700;
min-width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: var(--radius-full);
padding: 0 3px;
}
}
</style>

View file

@ -184,10 +184,12 @@ function onFormatSelect(formatId: string | null): void {
}
function handlePaste(): void {
// Immediately signal that analysis is starting prevents the Download
// button from being briefly clickable between paste and analysis.
isAnalyzing.value = true
// Auto-extract on paste unified loading state
setTimeout(async () => {
if (url.value.trim()) {
isAnalyzing.value = true
analyzeError.value = null
startAnalyzePhase()
try {
@ -204,6 +206,9 @@ function handlePaste(): void {
isAnalyzing.value = false
stopAnalyzePhase()
}
} else {
// URL was cleared before timeout cancel analysis state
isAnalyzing.value = false
}
}, 50)
}