From 677f515e4d25c34ed7613fdafd59569e80ae19bf Mon Sep 17 00:00:00 2001 From: Srihari NT Date: Sun, 15 Mar 2026 15:41:32 +0530 Subject: [PATCH] fix(ui): fullscreen volume above taskbar, settings overflow, download progress - #322: Fullscreen overlay padding and main-view scrollable so volume stays above taskbar when Up next is shown - #313: Settings tab content constrained on small displays with max-width, min-width, overflow-x - #278: HEAD request before GET for download to get Content-Length for progress bar; resolveDownloadTotalBytes in downloadProgressUtils.js --- js/api.js | 22 ++++++++++++++++++++-- js/downloadProgressUtils.js | 14 ++++++++++++++ styles.css | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 js/downloadProgressUtils.js diff --git a/js/api.js b/js/api.js index b5b95a6..725b066 100644 --- a/js/api.js +++ b/js/api.js @@ -20,8 +20,10 @@ import { loadFfmpeg, FfmpegError } from './ffmpeg.js'; import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts'; import { isCustomFormat } from './ffmpegFormats.ts'; import { DownloadProgress } from './progressEvents.js'; +import { resolveDownloadTotalBytes } from './downloadProgressUtils.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; +export { resolveDownloadTotalBytes }; const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; export class LosslessAPI { @@ -1398,6 +1400,22 @@ export class LosslessAPI { throw hlsError; } } else { + // Try HEAD first to get Content-Length when GET uses chunked encoding (fixes #278) + let headContentLength = null; + try { + const headResponse = await fetch(streamUrl, { + method: 'HEAD', + cache: 'no-store', + signal: options.signal, + }); + if (headResponse.ok) { + const cl = headResponse.headers.get('Content-Length'); + if (cl) headContentLength = parseInt(cl, 10); + } + } catch (_) { + /* ignore HEAD failure; proceed with GET */ + } + const response = await fetch(streamUrl, { cache: 'no-store', signal: options.signal, @@ -1407,8 +1425,8 @@ export class LosslessAPI { throw new Error(`Fetch failed: ${response.status}`); } - const contentLength = response.headers.get('Content-Length'); - const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; + const contentLengthHeader = response.headers.get('Content-Length'); + const totalBytes = resolveDownloadTotalBytes(contentLengthHeader, headContentLength); let receivedBytes = 0; diff --git a/js/downloadProgressUtils.js b/js/downloadProgressUtils.js new file mode 100644 index 0000000..6360a53 --- /dev/null +++ b/js/downloadProgressUtils.js @@ -0,0 +1,14 @@ +/** + * Helpers for download progress. Extracted for testability (fixes #278). + * Resolve total byte count from GET and optional HEAD Content-Length. + */ + +/** + * @param {string | null} contentLengthFromGet - Content-Length header from GET response + * @param {number | null} headContentLength - Content-Length from prior HEAD request + * @returns {number} + */ +export function resolveDownloadTotalBytes(contentLengthFromGet, headContentLength) { + const fromGet = contentLengthFromGet ? parseInt(contentLengthFromGet, 10) : null; + return fromGet ?? headContentLength ?? 0; +} diff --git a/styles.css b/styles.css index c0ef4be..7b892ee 100644 --- a/styles.css +++ b/styles.css @@ -1733,6 +1733,14 @@ input[type='search']::-webkit-search-cancel-button { animation: fade-in-slide-up 0.4s var(--ease-out-back); } +/* Prevent settings tab content from overflowing on small displays (fixes #313) */ +.settings-tab-content { + max-width: 100%; + min-width: 0; + overflow-x: auto; + overflow-y: visible; +} + .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); @@ -2571,6 +2579,9 @@ input[type='search']::-webkit-search-cancel-button { .settings-list { max-width: 800px; + width: 100%; + min-width: 0; + box-sizing: border-box; } .settings-group { @@ -2615,6 +2626,7 @@ input[type='search']::-webkit-search-cancel-button { padding: var(--spacing-lg) 0; border-bottom: 1px solid var(--border); gap: var(--spacing-lg); + min-width: 0; } .setting-item.sidebar-setting-item { @@ -2654,6 +2666,7 @@ input[type='search']::-webkit-search-cancel-button { .setting-item .info { display: flex; flex-direction: column; + min-width: 0; } .setting-item .label { @@ -3641,7 +3654,8 @@ input:checked + .slider::before { /* Use a CSS variable for the image so we can set it in JS */ --bg-image: none; - padding-bottom: 0; + /* Reserve space above taskbar / system UI so volume controls stay visible (fixes #322) */ + padding-bottom: max(env(safe-area-inset-bottom), 1.5rem); } #fullscreen-cover-overlay::before { @@ -5390,11 +5404,13 @@ img[src=''] { .fullscreen-main-view { flex: 1; + min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 6rem 2rem 2rem; + overflow-y: auto; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }