Merge branch 'features/1.1.14'
This commit is contained in:
commit
7e80279427
10 files changed
+185
-30
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user