From 02bfc7ba914528b529bc8a759449a7d40d6ce4f7 Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Sun, 22 Feb 2026 17:57:05 -0600
Subject: [PATCH 001/525] fix(ui): suppress WebKit default context menu
globally
---
src/routes/+page.svelte | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index a96020802..c913ecaff 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2898,6 +2898,11 @@
// Keyboard navigation
document.addEventListener('keydown', handleKeydown);
+ // Suppress WebKit default context menu globally
+ // Custom menus (TrackRow, sidebar, etc.) call e.stopPropagation() so they are unaffected
+ const handleGlobalContextMenu = (e: MouseEvent) => e.preventDefault();
+ document.addEventListener('contextmenu', handleGlobalContextMenu);
+
// Register keybinding actions
registerAction('playback.toggle', togglePlay);
registerAction('playback.next', handleSkipForward);
@@ -3392,6 +3397,7 @@
saveSessionBeforeClose();
cleanupBootstrap();
document.removeEventListener('keydown', handleKeydown);
+ document.removeEventListener('contextmenu', handleGlobalContextMenu);
unregisterAll(); // Cleanup keybinding actions
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange);
From 66d25b773bc494f4ba9b65f44fb2db98ab924d01 Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Sun, 22 Feb 2026 17:57:46 -0600
Subject: [PATCH 002/525] fix(ui): keybindings modal z-index and 2-column
layout
---
src/lib/components/KeyboardShortcutsModal.svelte | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/lib/components/KeyboardShortcutsModal.svelte b/src/lib/components/KeyboardShortcutsModal.svelte
index 7d652ca03..6d556291e 100644
--- a/src/lib/components/KeyboardShortcutsModal.svelte
+++ b/src/lib/components/KeyboardShortcutsModal.svelte
@@ -148,7 +148,7 @@
display: flex;
align-items: center;
justify-content: center;
- z-index: 1000;
+ z-index: 3000;
backdrop-filter: blur(4px);
animation: fadeIn 150ms ease-out;
}
@@ -162,7 +162,7 @@
background: var(--bg-secondary);
border-radius: 12px;
width: 90%;
- max-width: 500px;
+ max-width: 660px;
max-height: 80vh;
display: flex;
flex-direction: column;
@@ -225,10 +225,14 @@
flex: 1;
overflow-y: auto;
padding: 20px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0 24px;
}
.category-section {
margin-bottom: 24px;
+ break-inside: avoid;
}
.category-section:last-child {
@@ -315,6 +319,7 @@
.modal-content {
padding: 16px;
+ grid-template-columns: 1fr;
}
.shortcut-row {
From 37cca5dccf325055be36b03c40bae4bf51150b69 Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Sun, 22 Feb 2026 17:58:41 -0600
Subject: [PATCH 003/525] fix(ui): render sanitized HTML in playlist
descriptions
---
.../views/PlaylistDetailView.svelte | 12 +++++-
src/lib/utils/sanitize.ts | 37 +++++++++++++++++++
2 files changed, 48 insertions(+), 1 deletion(-)
create mode 100644 src/lib/utils/sanitize.ts
diff --git a/src/lib/components/views/PlaylistDetailView.svelte b/src/lib/components/views/PlaylistDetailView.svelte
index ac3434aee..fd0364c42 100644
--- a/src/lib/components/views/PlaylistDetailView.svelte
+++ b/src/lib/components/views/PlaylistDetailView.svelte
@@ -22,6 +22,7 @@
import { isTrackUnavailable, clearTrackUnavailable, subscribe as subscribeUnavailable } from '$lib/stores/unavailableTracksStore';
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
import { showToast } from '$lib/stores/toastStore';
+ import { sanitizeHtml } from '$lib/utils/sanitize';
import { get } from 'svelte/store';
import { t } from '$lib/i18n';
import { onMount, tick } from 'svelte';
@@ -2020,7 +2021,7 @@
Playlist
{playlist.name}
{#if playlist.description}
- {playlist.description}
+ {@html sanitizeHtml(playlist.description)}
{/if}
{playlist.owner.name}
@@ -2603,6 +2604,15 @@
line-height: 1.4;
}
+ .playlist-description :global(a) {
+ color: var(--accent-primary);
+ text-decoration: none;
+ }
+
+ .playlist-description :global(a:hover) {
+ text-decoration: underline;
+ }
+
.playlist-info {
display: flex;
align-items: center;
diff --git a/src/lib/utils/sanitize.ts b/src/lib/utils/sanitize.ts
new file mode 100644
index 000000000..b73e4b351
--- /dev/null
+++ b/src/lib/utils/sanitize.ts
@@ -0,0 +1,37 @@
+/**
+ * Lightweight HTML sanitizer for rendering API content (e.g. Qobuz playlist descriptions).
+ * Allows safe tags and strips dangerous elements/attributes.
+ */
+
+const ALLOWED_TAGS = new Set([
+ 'b', 'i', 'em', 'strong', 'br', 'a', 'p', 'span', 'ul', 'ol', 'li',
+]);
+
+/** Strip event handler attributes and javascript: URLs */
+function stripDangerousAttrs(tag: string): string {
+ // Remove on* event attributes
+ tag = tag.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
+ // Remove javascript: URLs from href/src
+ tag = tag.replace(/(href|src)\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, '$1=""');
+ return tag;
+}
+
+export function sanitizeHtml(html: string): string {
+ if (!html) return '';
+
+ // Remove
-
+
{#snippet children()}
-
-
-
-
-
Icon
+
+
+
{$t('favorites.tabOrderSectionTitle')}
-
- {#each presetIcons as preset}
-
- {/each}
-
-
-
-
- {#if useCustomImage && customIconPath}
- {customIconPath.split('/').pop()}
- {/if}
-
-
-
-
-
Background
-
-
-
-
- {#each solidColors as color}
-
- {/each}
-
-
-
-
-
- {#each gradients as gradient}
-
- {/each}
-
-
-
-
- {#if !showCustomGradientPicker}
-
- {:else}
-
- {#each customGradientColors as color, i}
-
- updateGradientColor(i, e.currentTarget.value)}
- class="color-input"
- />
- {#if customGradientColors.length > 2}
-
- {/if}
-
- {/each}
-
- {#if customGradientColors.length < 3}
-
- {/if}
-
-
-
+
+ {#each tabOrder as tab, index}
+
-
-
-
-
-
-
-
Tab Order
-
-
- {#each tabOrder as tab, index}
-
-
-
-
-
{tabLabels[tab] || tab}
-
-
-
-
+
{getTabLabel(tab)}
+
+
+
- {/each}
-
+
+ {/each}
@@ -318,174 +116,27 @@
{#snippet footer()}
{/snippet}
diff --git a/src/lib/components/views/FavoritesView.svelte b/src/lib/components/views/FavoritesView.svelte
index 53f7abe48..06cb4b5b2 100644
--- a/src/lib/components/views/FavoritesView.svelte
+++ b/src/lib/components/views/FavoritesView.svelte
@@ -1,9 +1,9 @@
+
+
+
+{#if isOpen}
+
+
+
![]()
+
+
+
+{/if}
+
+
From b8f8ca4e3d22f6ddf97da6f2ad5039959ea678fa Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Mon, 23 Feb 2026 02:16:45 -0600
Subject: [PATCH 031/525] feat(ui): click artist image to view full size
---
.../components/views/ArtistDetailView.svelte | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/lib/components/views/ArtistDetailView.svelte b/src/lib/components/views/ArtistDetailView.svelte
index 95d2f1f78..a1374fc98 100644
--- a/src/lib/components/views/ArtistDetailView.svelte
+++ b/src/lib/components/views/ArtistDetailView.svelte
@@ -25,6 +25,7 @@
toggleTrackFavorite
} from '$lib/stores/favoritesStore';
import { tick, onMount, onDestroy } from 'svelte';
+ import ImageLightbox from '../ImageLightbox.svelte';
interface Track {
id: number;
@@ -135,6 +136,7 @@
let bioExpanded = $state(false);
let imageError = $state(false);
+ let lightboxOpen = $state(false);
let topTracks = $state