diff --git a/js/app.js b/js/app.js
index b8f6991..4ad3fff 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1265,6 +1265,7 @@ document.addEventListener('DOMContentLoaded', async () => {
let name = document.getElementById('playlist-name-input').value.trim();
let description = document.getElementById('playlist-description-input').value.trim();
const isPublic = document.getElementById('playlist-public-toggle')?.checked;
+ const isStrictAlbumMatch = document.getElementById('strict-album-match-toggle')?.checked;
if (name) {
const modal = document.getElementById('playlist-modal');
@@ -1317,7 +1318,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const xmlFileInput = document.getElementById('xml-file-input');
const m3uFileInput = document.getElementById('m3u-file-input');
- const importOptions = { strictArtistMatch: true, albumMatch: true };
+ const importOptions = { strictArtistMatch: true, strictAlbumMatch: isStrictAlbumMatch };
let tracks = [];
let importSource = 'manual';
diff --git a/js/playlist-importer.js b/js/playlist-importer.js
index f55f7e0..08332c6 100644
--- a/js/playlist-importer.js
+++ b/js/playlist-importer.js
@@ -5,21 +5,21 @@ function isFuzzyMatch(str1, str2) {
return s1.includes(s2) || s2.includes(s1);
}
-function findBestMatch(items, targetArtist, targetAlbum, options) {
+function findBestMatch(items, targetArtist, targetAlbum, importOptions) {
if (!items || items.length === 0) return null;
- if (!options?.strictArtistMatch && !options?.albumMatch) return items[0];
+ if (!importOptions?.strictArtistMatch && !importOptions?.strictAlbumMatch) return items[0];
return (
items.find((item) => {
let artistOk = true;
let albumOk = true;
- if (options.strictArtistMatch && targetArtist) {
+ if (importOptions.strictArtistMatch && targetArtist) {
const itemArtist = item.artist?.name || item.artists?.[0]?.name;
if (!isFuzzyMatch(itemArtist, targetArtist)) artistOk = false;
}
- if (options.albumMatch && targetAlbum) {
+ if (importOptions.strictAlbumMatch && targetAlbum) {
const itemAlbum = item.album?.title;
if (itemAlbum && !isFuzzyMatch(itemAlbum, targetAlbum)) albumOk = false;
}
@@ -294,8 +294,7 @@ export async function parseDynamicCSV(csvText, api, onProgress, options = {}) {
const albumName = mappedHeaders.album !== undefined ? values[mappedHeaders.album] : '';
const isrc = mappedHeaders.isrc !== undefined ? values[mappedHeaders.isrc] : '';
const playlistName = mappedHeaders.playlistName !== undefined ? values[mappedHeaders.playlistName] : '';
- const typeValue =
- mappedHeaders.type !== undefined ? values[mappedHeaders.type]?.toLowerCase().trim() : '';
+ const typeValue = mappedHeaders.type !== undefined ? values[mappedHeaders.type]?.toLowerCase().trim() : '';
const isFavorite = typeValue.includes('favorite');
if (onProgress) {
@@ -497,7 +496,7 @@ export async function importToLibrary(csvResult, db, onProgress, options = {}) {
return results;
}
-export async function parseCSV(csvText, api, onProgress, options = {}) {
+export async function parseCSV(csvText, api, onProgress, importOptions = {}) {
const lines = csvText.trim().split('\n');
if (lines.length < 2) return { tracks: [], missingTracks: [] };
@@ -583,7 +582,7 @@ export async function parseCSV(csvText, api, onProgress, options = {}) {
const searchResult = await api.searchTracks(searchQuery);
if (searchResult.items && searchResult.items.length > 0) {
- const match = findBestMatch(searchResult.items, artistNames, albumName, options);
+ const match = findBestMatch(searchResult.items, artistNames, albumName, importOptions);
if (match) tracks.push(match);
else missingTracks.push({ title: trackTitle, artist: artistNames, album: albumName });
} else {
From 677f515e4d25c34ed7613fdafd59569e80ae19bf Mon Sep 17 00:00:00 2001
From: Srihari NT
Date: Sun, 15 Mar 2026 15:41:32 +0530
Subject: [PATCH 11/16] 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);
}
From 518c930eb2d9254efc050aa86a26a92851e8dcd7 Mon Sep 17 00:00:00 2001
From: edideaur
Date: Sun, 15 Mar 2026 13:43:37 +0200
Subject: [PATCH 12/16] undo linting changes
---
.github/workflows/lint.yml | 81 +++++++++++++++++---------------------
1 file changed, 37 insertions(+), 44 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 59e8396..4e32e03 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,60 +1,53 @@
name: Lint Codebase
on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
permissions:
- contents: write
+ contents: write
jobs:
- lint:
- runs-on: ubuntu-latest
+ lint:
+ runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
+ steps:
+ - uses: actions/checkout@v4
+ if: github.event_name == 'pull_request'
+ with:
+ ref: ${{ github.head_ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
- - name: Setup Bun
- uses: oven-sh/setup-bun@v1
- with:
- bun-version: latest
+ - uses: actions/checkout@v4
+ if: github.event_name != 'pull_request'
- - name: Cache dependencies
- uses: actions/cache@v3
- with:
- path: |
- ./bun_modules
- ./node_modules
- ./bun.lock
- key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
- - name: Install dependencies
- run: bun install --frozen-lockfile
+ - name: Install dependencies
+ run: npm ci
- - name: Run JS Lint
- run: bun run lint:js -- --fix
- continue-on-error: true
+ - name: Fix JS Lint
+ run: npm run lint:js -- --fix
+ continue-on-error: true
- - name: Run CSS Lint
- run: bun run lint:css -- --fix
- continue-on-error: true
+ - name: Fix CSS Lint
+ run: npm run lint:css -- --fix
+ continue-on-error: true
- - name: Format with Prettier
- run: bun run format
- continue-on-error: true
+ - name: Format with Prettier
+ run: npm run format
+ continue-on-error: true
- - name: Commit and Push lint fixes
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- commit_message: 'style: auto-fix linting issues'
- commit_user_name: 'github-actions[bot]'
- commit_user_email: 'github-actions[bot]@users.noreply.github.com'
- only_if_changed: true
+ - name: Commit and Push changes
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: 'style: auto-fix linting issues'
- - name: Run HTML Lint
- run: bun run lint:html
+ - name: Run HTML Lint
+ run: npm run lint:html
From 5bd9674e901c3ca0a3c0fbff72ef8fd0a6d5ce7b Mon Sep 17 00:00:00 2001
From: edideaur
Date: Sun, 15 Mar 2026 13:47:00 +0200
Subject: [PATCH 13/16] pls work
---
.github/workflows/lint.yml | 82 +++++++++++++++++++++-----------------
1 file changed, 45 insertions(+), 37 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 4e32e03..17177ef 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,53 +1,61 @@
name: Lint Codebase
on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
permissions:
- contents: write
+ contents: write
+ workflows: write
jobs:
- lint:
- runs-on: ubuntu-latest
+ lint:
+ runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- if: github.event_name == 'pull_request'
- with:
- ref: ${{ github.head_ref }}
- repository: ${{ github.event.pull_request.head.repo.full_name }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
- - uses: actions/checkout@v4
- if: github.event_name != 'pull_request'
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
- - name: Use Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ./bun_modules
+ ./node_modules
+ ./bun.lock
+ key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
- - name: Install dependencies
- run: npm ci
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
- - name: Fix JS Lint
- run: npm run lint:js -- --fix
- continue-on-error: true
+ - name: Run JS Lint
+ run: bun run lint:js -- --fix
+ continue-on-error: true
- - name: Fix CSS Lint
- run: npm run lint:css -- --fix
- continue-on-error: true
+ - name: Run CSS Lint
+ run: bun run lint:css -- --fix
+ continue-on-error: true
- - name: Format with Prettier
- run: npm run format
- continue-on-error: true
+ - name: Format with Prettier
+ run: bun run format
+ continue-on-error: true
- - name: Commit and Push changes
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- commit_message: 'style: auto-fix linting issues'
+ - name: Commit and Push lint fixes
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: 'style: auto-fix linting issues'
+ commit_user_name: 'github-actions[bot]'
+ commit_user_email: 'github-actions[bot]@users.noreply.github.com'
+ only_if_changed: true
- - name: Run HTML Lint
- run: npm run lint:html
+ - name: Run HTML Lint
+ run: bun run lint:html
From abcc33ba380a755122c49733b86118579adc7850 Mon Sep 17 00:00:00 2001
From: itsmo-ks
Date: Sun, 15 Mar 2026 14:00:44 +0000
Subject: [PATCH 14/16] Fix Clear Recently played
---
js/accounts/pocketbase.js | 7 +++++++
js/ui.js | 1 +
2 files changed, 8 insertions(+)
diff --git a/js/accounts/pocketbase.js b/js/accounts/pocketbase.js
index e70b860..d9d4671 100644
--- a/js/accounts/pocketbase.js
+++ b/js/accounts/pocketbase.js
@@ -303,6 +303,13 @@ const syncManager = {
await this._updateUserJSON(user.$id, 'history', newHistory);
},
+ async clearHistory() {
+ const user = authManager.user;
+ if (!user) return;
+
+ await this._updateUserJSON(user.$id, 'history', []);
+ },
+
async syncUserPlaylist(playlist, action) {
const user = authManager.user;
if (!user) return;
diff --git a/js/ui.js b/js/ui.js
index 1998434..87e21c3 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -4451,6 +4451,7 @@ export class UIRenderer {
if (confirm('Clear all recently played tracks? This cannot be undone.')) {
try {
await db.clearHistory();
+ await syncManager.clearHistory();
container.innerHTML = createPlaceholder("You haven't played any tracks yet.");
clearBtn.style.display = 'none';
} catch (err) {
From 7162a021c71d5fc13153f6dbbf06511a28d27d42 Mon Sep 17 00:00:00 2001
From: Samidy
Date: Mon, 16 Mar 2026 06:23:10 +0300
Subject: [PATCH 15/16] alr bra im not tryna get fucked
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index ccba62b..1cc0c76 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@
## What is Monochrome?
-**Monochrome** is an open-source, privacy-respecting, ad-free [TIDAL](https://tidal.com) web UI, built on top of [Hi-Fi](https://github.com/binimum/hifi-api). It provides a beautiful, minimalist interface for streaming high-quality music without the clutter of traditional streaming platforms.
+**Monochrome** is an open-source, privacy-respecting, ad-free [TIDAL](https://tidal.com) web UI, built on top of Hi-Fi. It provides a beautiful, minimalist interface for streaming high-quality music without the clutter of traditional streaming platforms.
From a2fd69223ea0ca95973f701541e5a2ee56124962 Mon Sep 17 00:00:00 2001
From: Samidy
Date: Mon, 16 Mar 2026 06:46:11 +0300
Subject: [PATCH 16/16] fix link here lol
---
.github/pull_request_template.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 3ecdb37..a7b1919 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -6,7 +6,7 @@
- [ ] Docs only
### Checklist
-- [ ] **I have read the [Contributing Guidelines](../CONTRIBUTING.md).**
+- [ ] **I have read the [Contributing Guidelines](https://github.com/monochrome-music/monochrome/blob/main/CONTRIBUTING.md).**
- [ ] **I understand every line of code I am submitting.**
- [ ] I have tested these changes locally, and they work as expected.