Best quality format, password UX, mobile columns

Best quality format:
- Synthetic 'bestvideo+bestaudio/best' entry added at top of format
  list when the best separate video stream exceeds the best pre-muxed
  format. Shows as 'Best quality (1920x1080)' in Video+Audio group.
- YouTube typically only has 360p pre-muxed but 1080p+ as separate
  streams — users can now select full quality with auto-merge.
- Only appears when there's actually a quality advantage vs pre-muxed.

Password change UX:
- Enter key on confirm password field submits the change
- Auto-logout 1.5s after successful password change
- User sees '✓ Password changed' before being redirected home

Mobile table:
- Status column hidden on mobile (<640px) alongside Progress
- Only Name + Actions columns shown — clean two-column layout
- Removed mobile-specific status badge font tweaks (column gone)
This commit is contained in:
xpltd 2026-03-19 05:29:41 -05:00
parent 5da223c5f8
commit 3d778246ca
3 changed files with 50 additions and 11 deletions

View file

@ -229,6 +229,47 @@ class DownloadService:
key=lambda fi: _parse_resolution_height(fi.resolution),
reverse=True,
)
# Add synthetic "best quality" entries at the top.
# yt-dlp can merge separate video+audio streams for best quality,
# but those don't appear as pre-muxed formats in the format list.
best_video = None
best_audio = None
for f in formats_raw:
vcodec = f.get("vcodec", "none")
acodec = f.get("acodec", "none")
height = f.get("height") or 0
if vcodec and vcodec != "none" and height > 0:
if best_video is None or height > (best_video.get("height") or 0):
best_video = f
if acodec and acodec != "none" and (vcodec == "none" or not vcodec):
if best_audio is None:
best_audio = f
if best_video:
bv_height = best_video.get("height", 0)
bv_res = f"{best_video.get('width', '?')}x{bv_height}"
# Only add if the best separate video exceeds the best pre-muxed
best_premuxed_height = 0
for f in formats_raw:
vc = f.get("vcodec", "none")
ac = f.get("acodec", "none")
if vc and vc != "none" and ac and ac != "none":
h = f.get("height") or 0
if h > best_premuxed_height:
best_premuxed_height = h
if bv_height > best_premuxed_height:
result.insert(0, FormatInfo(
format_id="bestvideo+bestaudio/best",
ext=best_video.get("ext", "webm"),
resolution=bv_res,
codec=best_video.get("vcodec"),
format_note=f"Best quality ({bv_res})",
vcodec=best_video.get("vcodec"),
acodec="merged",
))
return result
async def cancel(self, job_id: str) -> None:

View file

@ -95,13 +95,15 @@ async function changePassword() {
})
if (res.ok) {
// Update stored credentials to use new password
store.password = newPassword.value
passwordChanged.value = true
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
passwordChanged.value = true
setTimeout(() => { passwordChanged.value = false }, 3000)
// Auto-logout after 1.5s so user sees the success message
setTimeout(() => {
store.logout()
router.push('/')
}, 1500)
} else {
const data = await res.json()
passwordError.value = data.detail || 'Failed to change password'
@ -335,6 +337,7 @@ function formatFilesize(bytes: number | null): string {
placeholder="Confirm new password"
autocomplete="new-password"
class="settings-input"
@keydown.enter="changePassword"
/>
</div>
<div class="settings-actions" style="margin-top: var(--space-sm);">

View file

@ -515,7 +515,7 @@ async function clearJob(jobId: string): Promise<void> {
opacity: 0;
}
/* Mobile: hide speed, ETA, and progress columns */
/* Mobile: hide speed, ETA, progress, and status columns */
@media (max-width: 639px) {
.hide-mobile {
display: none;
@ -527,7 +527,7 @@ async function clearJob(jobId: string): Promise<void> {
}
.col-status {
width: 75px;
display: none;
}
.col-progress {
@ -553,11 +553,6 @@ async function clearJob(jobId: string): Promise<void> {
.action-group {
gap: 2px;
}
.status-badge {
font-size: 0.625rem;
padding: 1px 4px;
}
}
/* Toast notification */