Merge branch 'features/1.1.14'

This commit is contained in:
Victor RH committed 2026-02-13 07:58:56 -06:00
commit 7e80279427
10 files changed
+185 -30

No files matched your search

-3
View File
@@ -40,9 +40,6 @@ finish-args:
- --talk-name=org.freedesktop.portal.FileChooser
- --talk-name=org.mpris.MediaPlayer2.*
# Own MPRIS name to register as media player
- --own-name=org.mpris.MediaPlayer2.qbz
# OAuth browser authentication
- --talk-name=org.freedesktop.portal.OpenURI
+1 -1
View File
@@ -52,7 +52,7 @@ impl MediaControlsManager {
// (souvlaki requires a window handle on some platforms)
thread::spawn(move || {
let config = PlatformConfig {
dbus_name: "qbz",
dbus_name: "com.blitzfc.qbz",
display_name: "QBZ",
hwnd: None, // Not needed on Linux
};
+24 -4
View File
@@ -9,6 +9,7 @@
Repeat1,
Heart,
Plus,
Minus,
Volume2,
VolumeX,
Volume1,
@@ -440,10 +441,23 @@
</div>
<div class="volume-thumb" style="left: {volume}%"></div>
</div>
</div>
<!-- Separator -->
<div class="section-separator"></div>
<button
class="control-btn volume-step-btn"
onclick={() => onVolumeChange?.(Math.max(0, volume - 5))}
title={$t('player.volumeDown')}
>
<Minus size={14} />
</button>
<button
class="control-btn volume-step-btn"
onclick={() => onVolumeChange?.(Math.min(100, volume + 5))}
title={$t('player.volumeUp')}
>
<Plus size={14} />
</button>
</div>
<!-- Queue Button (far right) -->
<button
@@ -721,7 +735,7 @@
padding: 2px;
background: var(--bg-tertiary);
border-radius: 8px;
min-width: 508px;
min-width: 436px;
flex: 1;
max-width: 800px;
}
@@ -915,6 +929,12 @@
opacity: 1;
}
.volume-step-btn {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.volume-value {
position: absolute;
right: 0;
+30 -12
View File
@@ -64,10 +64,11 @@
<img src={uniqueArtworks[1]} alt="" class="cover quarter top-right" />
<img src={uniqueArtworks[2]} alt="" class="cover quarter bottom-right" />
{:else}
<img src={uniqueArtworks[0]} alt="" class="cover quarter top-left" />
<img src={uniqueArtworks[1]} alt="" class="cover quarter top-right" />
<img src={uniqueArtworks[2]} alt="" class="cover quarter bottom-left" />
<img src={uniqueArtworks[3]} alt="" class="cover quarter bottom-right" />
<!-- 4 covers: 3 small stacked left, 1 large right -->
<img src={uniqueArtworks[0]} alt="" class="cover small-top" />
<img src={uniqueArtworks[1]} alt="" class="cover small-mid" />
<img src={uniqueArtworks[2]} alt="" class="cover small-bot" />
<img src={uniqueArtworks[3]} alt="" class="cover large-right" />
{/if}
</div>
@@ -142,17 +143,34 @@
height: 100%;
}
/* 4 covers - 2x2 grid */
/* 4 covers - 3 small left stacked, 1 large right */
.collage.quad {
grid-template: 1fr 1fr / 1fr 1fr;
gap: 1px;
grid-template-rows: 1fr 1fr 1fr;
grid-template-columns: 1fr 2fr;
gap: 2px;
}
.cover.quarter {
.small-top {
grid-row: 1;
grid-column: 1;
width: 100%;
height: 100%;
}
.small-mid {
grid-row: 2;
grid-column: 1;
width: 100%;
height: 100%;
}
.small-bot {
grid-row: 3;
grid-column: 1;
width: 100%;
height: 100%;
}
.large-right {
grid-row: 1 / 4;
grid-column: 2;
width: 100%;
height: 100%;
}
.quarter.top-left { grid-area: 1 / 1; }
.quarter.top-right { grid-area: 1 / 2; }
.quarter.bottom-left { grid-area: 2 / 1; }
.quarter.bottom-right { grid-area: 2 / 2; }
</style>
+114 -2
View File
@@ -130,6 +130,11 @@
let showCreateFolderModal = $state(false);
let newFolderName = $state('');
// Playlist search state
let showPlaylistSearch = $state(false);
let playlistSearchQuery = $state('');
let playlistSearchInput = $state<HTMLInputElement | null>(null);
// Virtual scroll state for playlists
type VirtualPlaylistItem =
| { type: 'folder-header'; folder: PlaylistFolder; folderId: string; top: number; height: number }
@@ -378,6 +383,12 @@
return !settings?.hidden;
});
// Filter by playlist search query
if (playlistSearchQuery.trim()) {
const query = playlistSearchQuery.toLowerCase();
visible = visible.filter(p => p.name.toLowerCase().includes(query));
}
// Filter by local content when offline
if (offlineStatus.isOffline) {
visible = visible.filter(p => {
@@ -613,6 +624,7 @@
let virtualPlaylistItems = $derived.by(() => {
const items: VirtualPlaylistItem[] = [];
let currentTop = 0;
const isSearching = playlistSearchQuery.trim().length > 0;
// Reference folderExpandState to trigger re-derivation when folders are toggled
// (isFolderExpanded reads from a non-reactive Set, so we need this dependency)
@@ -622,7 +634,12 @@
// Expanded sidebar: folders with headers + playlists, then root playlists
for (const folder of folders) {
const folderPlaylists = getPlaylistsInFolder(folder.id);
const expanded = isFolderExpanded(folder.id);
// When searching, skip folders with no matching playlists
if (isSearching && folderPlaylists.length === 0) continue;
// When searching, force expand; otherwise use normal expand state
const expanded = isSearching || isFolderExpanded(folder.id);
// Folder header
items.push({
@@ -1363,8 +1380,49 @@
<div class="section playlists-section">
{#if isExpanded}
<div class="playlists-header">
<div class="section-header">{$t('nav.playlists')}</div>
{#if showPlaylistSearch}
<div class="playlist-search-container">
<Search size={12} />
<input
type="text"
class="playlist-search-input"
placeholder={$t('playlist.searchPlaylists')}
bind:value={playlistSearchQuery}
bind:this={playlistSearchInput}
onkeydown={(e) => {
if (e.key === 'Escape') {
showPlaylistSearch = false;
playlistSearchQuery = '';
}
}}
/>
<button
class="playlist-search-close"
onclick={() => { showPlaylistSearch = false; playlistSearchQuery = ''; }}
title={$t('actions.close')}
>
<X size={12} />
</button>
</div>
{:else}
<div class="section-header">{$t('nav.playlists')}</div>
{/if}
<div class="header-actions" bind:this={menuRef}>
<button
class="icon-btn"
class:active={showPlaylistSearch}
onclick={() => {
showPlaylistSearch = !showPlaylistSearch;
if (showPlaylistSearch) {
setTimeout(() => playlistSearchInput?.focus(), 0);
} else {
playlistSearchQuery = '';
}
}}
title={$t('playlist.searchPlaylists')}
>
<Search size={14} />
</button>
<button class="icon-btn" onclick={onCreatePlaylist} title={$t('playlist.createNew')}>
<Plus size={14} />
</button>
@@ -1942,6 +2000,60 @@
padding: 0;
}
.playlist-search-container {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
height: 24px;
background: var(--bg-tertiary);
border-radius: 4px;
padding: 0 8px;
margin-right: 4px;
margin-top: -2px;
margin-bottom: -2px;
color: var(--text-muted);
}
.playlist-search-input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
outline: none;
font-size: 11px;
color: var(--text-primary);
padding: 0;
}
.playlist-search-input::placeholder {
color: var(--text-muted);
}
.playlist-search-close {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
color: var(--text-muted);
cursor: pointer;
transition: color 150ms ease;
}
.playlist-search-close:hover {
color: var(--text-primary);
}
.icon-btn.active {
color: var(--accent-primary);
}
.header-actions {
display: flex;
align-items: center;
@@ -1950,14 +1950,10 @@
</div>
{:else}
<div class="collage-wrapper">
{#if playlist.images && playlist.images.length > 0}
<img class="playlist-api-image" src={playlist.images[0]} alt={playlist.name} width="200" height="200" />
{:else}
<PlaylistCollage
artworks={tracks.slice(0, 4).map(trk => trk.albumArt).filter((a): a is string => !!a)}
size={200}
/>
{/if}
<PlaylistCollage
artworks={tracks.slice(0, 4).map(trk => trk.albumArt).filter((a): a is string => !!a)}
size={200}
/>
<div class="artwork-overlay">
<button class="artwork-btn" onclick={selectCustomArtwork} title="Set custom artwork">
<ImagePlus size={24} />
+3
View File
@@ -258,6 +258,7 @@
"private": "Privat",
"import": "Importieren",
"manage": "Playlists verwalten",
"searchPlaylists": "Playlists durchsuchen",
"allHidden": "Alle Playlists sind verborgen",
"findReplacement": "Verfügbare Version finden",
"searchingFor": "Suche nach",
@@ -406,6 +407,8 @@
"volume": "Lautstärke",
"mute": "Stumm",
"unmute": "Stummschaltung aufheben",
"volumeUp": "Lauter",
"volumeDown": "Leiser",
"clearQueue": "Warteschlange leeren",
"queueEmpty": "Warteschlange ist leer",
"queueEmptyDescription": "Suche nach Musik oder durchsuchen Sie Ihre Bibliothek, um Titel hinzuzufügen.",
+3
View File
@@ -267,6 +267,7 @@
"private": "Private",
"import": "Import",
"manage": "Manage playlists",
"searchPlaylists": "Search playlists",
"allHidden": "All playlists are hidden",
"findReplacement": "Find Available Version",
"searchingFor": "Searching for",
@@ -414,6 +415,8 @@
"volume": "Volume",
"mute": "Mute",
"unmute": "Unmute",
"volumeUp": "Increase volume",
"volumeDown": "Decrease volume",
"clearQueue": "Clear Queue",
"queueEmpty": "Queue is empty",
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
+3
View File
@@ -267,6 +267,7 @@
"private": "Privada",
"import": "Importar",
"manage": "Administrar listas",
"searchPlaylists": "Buscar playlists",
"allHidden": "Todas las listas están ocultas",
"findReplacement": "Buscar Versión Disponible",
"searchingFor": "Buscando",
@@ -414,6 +415,8 @@
"volume": "Volumen",
"mute": "Silenciar",
"unmute": "Activar Sonido",
"volumeUp": "Subir volumen",
"volumeDown": "Bajar volumen",
"clearQueue": "Limpiar Cola",
"queueEmpty": "La cola está vacía",
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
+3
View File
@@ -258,6 +258,7 @@
"private": "Privé",
"import": "Importer",
"manage": "Gérer les playlists",
"searchPlaylists": "Rechercher des playlists",
"allHidden": "Toutes les playlists sont masquées",
"findReplacement": "Trouver la version disponible",
"searchingFor": "Recherche de",
@@ -406,6 +407,8 @@
"volume": "Volume",
"mute": "Muet",
"unmute": "Réactiver le son",
"volumeUp": "Augmenter le volume",
"volumeDown": "Baisser le volume",
"clearQueue": "Vider la file d'attente",
"queueEmpty": "La file d'attente est vide",
"queueEmptyDescription": "Recherchez de la musique ou parcourez votre bibliothèque pour ajouter des pistes.",