fix(library): restore offline visibility and unify filters and sorting

Recover missing offline library entries, recognize cached-only libraries, and refresh availability after cache changes. Unify per-tab filters, compact the toolbar, and add reversible sort criteria with complete available purchase metadata.

cx
This commit is contained in:
vicrodh committed 2026-09-09 17:08:33 -06:00
1 parent 9504139dec
commit 17e08e93de
33 files changed
+1938 -547

No files matched your search

+363 -428
View File
@@ -1,7 +1,6 @@
// LibraryToolbar — the Library view's fixed chrome row: the six-tab menu on
// the left, the per-tab ACTION cluster beside it, and the per-tab controls on
// the right (FavoritesView.slint:444-942, rows 1 and 2 folded into one 56px
// band because this port's header carries no "Library" title).
// LibraryToolbar — two rows: navigation/actions/selects above, an always-open
// search and independent opt-in filter switches below. Narrow panes scroll
// each control row horizontally instead of covering neighbouring controls.
//
// EXTRACTED from views/LibraryView.qml (track rule 2 — the file was 1,881
// lines). Everything it reads and writes goes through `view`, the LibraryView
@@ -14,20 +13,45 @@ import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
import "../../assets/release-sort.js" as ReleaseSort
Item {
id: root
readonly property var sortOptions: {
var tab = view.activeTab
var options = []
function add(value, label) { options.push({value: value, label: label}) }
if (tab !== "all") add("default", QbzSession.tr("Default", QbzSession.trRev))
add("date", QbzSession.tr("Date added", QbzSession.trRev))
if (tab === "playlists") add("updated", QbzSession.tr("Date updated", QbzSession.trRev))
add("title", QbzSession.tr("Alphabetical", QbzSession.trRev))
if (tab === "tracks") add("release", QbzSession.tr("Release", QbzSession.trRev))
if (["all", "tracks", "albums"].indexOf(tab) >= 0)
add("artist", QbzSession.tr("Artist", QbzSession.trRev))
if (tab === "tracks" || tab === "albums") {
add("label", QbzSession.tr("Label", QbzSession.trRev))
add("genre", QbzSession.tr("Genre", QbzSession.trRev))
add("release-date", QbzSession.tr("Release date", QbzSession.trRev))
}
if (tab === "playlists") add("track-count", QbzSession.tr("Track count", QbzSession.trRev))
if (["tracks", "albums", "playlists"].indexOf(tab) >= 0)
add("duration", QbzSession.tr("Duration", QbzSession.trRev))
return options
}
/// The LibraryView root.
property var view: null
QbzTheme { id: theme }
height: 56
// The width here is the REAL content pane after the navigation and
// Queue/Lyrics columns have taken their space. At this breakpoint the
// six-tab strip and the All controls fit in one row without either side
// eating the other.
readonly property bool singleRow: view.activeTab === "labels" || view.activeTab === "playlists"
|| view.activeTab === "artists"
readonly property real rowGap: 8
readonly property real secondRowY: 25 + Math.max(tabRow.height, 30) / 2 + rowGap
readonly property real fullSearchWidth: Math.min(240, Math.max(140, root.width * 0.22))
height: singleRow ? 50 : secondRowY + searchBox.height + rowGap
// Compact only the count badges; genre and sort retain their labels.
readonly property bool compactChrome: width < 1040
// Small toolbar toggle button (ToggleButton sm): 30px, active = accent.
@@ -62,7 +86,7 @@ Item {
component GenreToolButton: Rectangle {
id: gtb
readonly property bool active: root.view.genreCount > 0
width: root.compactChrome ? 30 : gtbRow.implicitWidth
width: gtbRow.implicitWidth
height: 30
radius: 6
color: gtb.active ? theme.accent
@@ -71,9 +95,9 @@ Item {
id: gtbRow
anchors.centerIn: parent
height: parent.height
leftPadding: root.compactChrome ? 0 : 10
rightPadding: root.compactChrome ? 0 : 12
spacing: root.compactChrome ? 0 : 7
leftPadding: 10
rightPadding: 12
spacing: 7
QbzIcon {
name: "list-filter"
width: 13
@@ -91,7 +115,7 @@ Item {
tintName: gtb.active ? theme.accentGlyphTint : "secondary"
}
Text {
visible: !root.compactChrome
visible: true
anchors.verticalCenter: parent.verticalCenter
text: root.view.genreCount === 0
? QbzSession.tr("Filter by genre", QbzSession.trRev)
@@ -127,7 +151,7 @@ Item {
anchor: gtb
groups: {
var out = []
var names = (root.view.genreDoc.names || {})["library-all"] || []
var names = (root.view.genreDoc.names || {})[root.view.genreContext] || []
if (names.length > 0)
out.push({ group: QbzSession.tr("Genre", QbzSession.trRev),
values: names })
@@ -220,7 +244,17 @@ Item {
}
}
// FavTabMenu (SegmentedTabBar with count badges).
// Navigation and labelled selects share the first row.
Flickable {
id: topChrome
width: parent.width
height: 52
contentWidth: Math.max(width, topControls.x + topControls.width + 32)
contentHeight: height
flickableDirection: Flickable.HorizontalFlick
boundsBehavior: Flickable.StopAtBounds
clip: true
Rectangle {
x: 32
y: 25 - height / 2
@@ -239,9 +273,9 @@ Item {
tabs: [
{ "id": "all", "label": QbzSession.tr("All", QbzSession.trRev), "count": root.view.counts.all || 0 },
{ "id": "tracks", "label": QbzSession.tr("Tracks", QbzSession.trRev), "count": root.view.tabTotals.tracks || 0 },
{ "id": "albums", "label": QbzSession.tr("Albums", QbzSession.trRev), "count": root.view.tabTotals.albums || 0 },
{ "id": "albums", "label": QbzSession.tr("Releases", QbzSession.trRev), "count": root.view.tabTotals.albums || 0 },
{ "id": "artists", "label": QbzSession.tr("Artists", QbzSession.trRev), "count": root.view.counts.artists || 0 },
{ "id": "playlists", "label": QbzSession.tr("Playlists", QbzSession.trRev), "count": root.view.counts.playlists || 0 },
{ "id": "playlists", "label": QbzSession.tr("Playlists", QbzSession.trRev), "count": root.view.tabTotals.playlists || 0 },
{ "id": "labels", "label": QbzSession.tr("Labels", QbzSession.trRev), "count": root.view.counts.labels || 0 },
]
onSelected: function (id) { root.view.activeTab = id }
@@ -249,292 +283,23 @@ Item {
}
}
// --- Per-tab ACTION cluster (FavoritesView.slint:462-565) -------------
// The reference draws these in its own row, to the right of a "Library"
// title this port's header does not have; they sit beside the tab bar
// instead. FUNCTION is 1:1 — this is issue #554 ("users reported the
// feature as removed"), so what matters is that Play all / Shuffle / the
// per-tab random / the multi-select toggles exist and work.
//
// Each gates on its tab having something to act on, exactly as the
// reference's `*-visible.length > 0` guards do: an action that would play
// nothing is not drawn.
Row {
id: actionRow
x: 32 + tabRow.width + 12
y: 25 - height / 2
height: 30
spacing: 8
function randomVisibleId(kind) {
var pool = []
var rows = root.view.visibleRows
for (var i = 0; i < rows.length; i++)
if (rows[i].kind === kind) pool.push(rows[i])
if (pool.length === 0) return null
return pool[Math.floor(Math.random() * pool.length)]
}
// Tracks — play all / shuffle all / select multiple.
QbzIconButton {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
btnSize: 30
name: "play-fill"
onClicked: QbzLibrary.libraryPlayAll(
JSON.stringify(root.view.visibleTrackIds()), false)
}
QbzIconButton {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: QbzLibrary.libraryPlayAll(
JSON.stringify(root.view.visibleTrackIds()), true)
}
ToolToggle {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
name: "square-check-big"
active: root.view.tracksMultiSelect
onClicked: root.view.setTracksMultiSelect(!root.view.tracksMultiSelect)
}
// Albums — play a RANDOM album (the reference's `albums_shuffle`,
// which picks one visible album, not a shuffled play of every album).
QbzIconButton {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("album")
if (pick) QbzPlayer.playAlbum(pick.id)
}
}
// Albums multi-select — LIST mode ONLY (owner 2026-07-24,
// FavoritesView.slint:512-523): the grid card has no checkbox slot.
ToolToggle {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
&& root.view.albumsView === "list"
name: "square-check-big"
active: root.view.albumsMultiSelect
onClicked: root.view.setAlbumsMultiSelect(!root.view.albumsMultiSelect)
}
// Artists — open a random artist.
QbzIconButton {
visible: root.view.activeTab === "artists" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("artist")
if (pick) QbzArtist.openArtist(pick.id)
}
}
// Playlists — play a random playlist.
QbzIconButton {
visible: root.view.activeTab === "playlists" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("playlist")
if (pick) QbzBridge.openPlaylist(pick.id)
}
}
// Labels — open a random label's landing.
QbzIconButton {
visible: root.view.activeTab === "labels" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("label")
if (pick) QbzHome.openLabel(pick.id)
}
}
}
// Per-tab controls.
Row {
x: parent.width - width - 32
id: topControls
x: Math.max(32 + tabRow.width + 16, topChrome.width - width - 32)
y: 25 - height / 2
height: 30
spacing: 8
// ===== All toolbar =====
Row {
visible: root.view.activeTab === "all"
spacing: 8
height: parent.height
// FavoritesView.slint:801-813 — collapsed magnifier that
// opens LEFT over the tab bar; the All tab is the only
// one that debounces after the keystroke.
QbzLineEdit {
searchMode: true
expandable: true
sm: true
elevated: false // ExpandableSearch fill = surface-card
openWidth: 196 // = max-open-width
placeholder: QbzSession.tr("Search your library", QbzSession.trRev)
onEdited: function (v) { root.view.setAllSearch(v) }
}
// Source switches (tooltips are the Slint copies).
ToolToggle { name: "shopping-bag"; active: root.view.showPurchases; onClicked: root.view.setShowPurchases(!root.view.showPurchases) }
ToolToggle { name: "heart"; active: root.view.showFavorites; onClicked: root.view.setShowFavorites(!root.view.showFavorites) }
ToolToggle { name: "user-plus"; active: root.view.showFollowing; onClicked: root.view.setShowFollowing(!root.view.showFollowing) }
// The ONE source switch the reference persists (favorites_prefs.rs
// `all_show_local`) — the other three are session-local there too.
ToolToggle {
name: "hard-drive"
active: root.view.showLocal
onClicked: root.view.setShowLocal(!root.view.showLocal)
}
// Filter by genre — shared popup, own "library-all"
// context (FavoritesView.slint:864).
GenreToolButton { }
// Grid / list toggle.
ToolToggle {
name: root.view.viewMode === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.viewMode = root.view.viewMode === "list" ? "grid" : "list"
}
// Sort menu (PlaylistView-style: field + direction caret).
Rectangle {
width: root.compactChrome ? 40 : allSortRow.implicitWidth
height: 30
radius: 6
color: allSortArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
Row {
id: allSortRow
anchors.centerIn: parent
height: parent.height
leftPadding: root.compactChrome ? 8 : 10
rightPadding: root.compactChrome ? 8 : 10
spacing: root.compactChrome ? 0 : 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.compactChrome ? "A-Z"
: QbzSession.tr("Sort", QbzSession.trRev) + ": " + (
root.view.sortBy === "title" ? QbzSession.tr("Title", QbzSession.trRev)
: root.view.sortBy === "artist" ? QbzSession.tr("Artist", QbzSession.trRev)
: QbzSession.tr("Date added", QbzSession.trRev))
color: theme.textSecondary
font.pixelSize: 12
}
QbzIcon {
visible: !root.compactChrome
name: root.view.sortAsc ? "chevron-up" : "chevron-down"
width: 12
height: 12
anchors.verticalCenter: parent.verticalCenter
tintName: "secondary"
}
}
MouseArea {
id: allSortArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: allSortMenu.openBelowRight(allSortArea)
}
QbzContextMenu {
id: allSortMenu
menuWidth: 172
Repeater {
model: [
{ "field": "date", "label": QbzSession.tr("Date added", QbzSession.trRev) },
{ "field": "title", "label": QbzSession.tr("Title", QbzSession.trRev) },
{ "field": "artist", "label": QbzSession.tr("Artist", QbzSession.trRev) },
]
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 33
radius: 5
color: sortOptArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 6
Text {
width: parent.width - 26
height: parent.height
text: modelData.label
color: theme.textSecondary
font.pixelSize: 13
font.weight: root.view.sortBy === modelData.field ? theme.weightSemibold : theme.weightRegular
verticalAlignment: Text.AlignVCenter
}
QbzIcon {
visible: root.view.sortBy === modelData.field
name: root.view.sortAsc ? "chevron-up" : "chevron-down"
width: 12
height: 12
anchors.verticalCenter: parent.verticalCenter
tintName: "accent"
}
}
MouseArea {
id: sortOptArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Re-pick flips direction; a new field
// resets to its natural default.
if (root.view.sortBy === modelData.field) {
root.view.sortAsc = !root.view.sortAsc
} else {
root.view.sortBy = modelData.field
root.view.sortAsc = modelData.field !== "date"
}
allSortMenu.close()
}
}
}
}
}
}
Item {
id: firstRowSearchSlot
visible: root.singleRow
width: searchBox.width
height: 30
}
// ===== Other-tab toolbars =====
// ONE search instance for the five tabs: the reference clears the
// query on every tab entry (see LibraryView.onActiveTabChanged), so
// per-tab instances would only implement a persistence that does not
// exist. A Row skips `visible: false` children, so the 30px slot
// appears/disappears cleanly and, because the ROOT width never leaves
// collapsedSize while the field opens, nothing to its right ever moves.
QbzLineEdit {
id: tabSearchBox
objectName: "libraryTabSearch"
visible: root.view.activeTab !== "all" && root.view.tabHasItems
searchMode: true
expandable: true
sm: true
elevated: false
openWidth: 196
placeholder: QbzSession.tr("Search", QbzSession.trRev)
onEdited: function (v) { root.view.tabSearch = v }
}
// Tracks / Albums source switches. They deliberately sit between the
// fixed 30px search slot and Filter by genre: ExpandableSearch grows
// left, so opening it can never cover or push either toggle.
ToolToggle {
visible: (root.view.activeTab === "tracks" || root.view.activeTab === "albums")
&& root.view.tabHasItems
name: "shopping-bag"
active: root.view.showPurchases
onClicked: root.view.setShowPurchases(!root.view.showPurchases)
}
ToolToggle {
visible: (root.view.activeTab === "tracks" || root.view.activeTab === "albums")
&& root.view.tabHasItems
name: "heart"
active: root.view.showFavorites
onClicked: root.view.setShowFavorites(!root.view.showFavorites)
}
// Filter by genre — the SAME shared popup the All toolbar uses. The
// reference draws it on Tracks and Albums too (FavoritesView.slint:
// 609, :652); this port had it on All only, so the two biggest tabs
// could not be filtered at all.
GenreToolButton {
visible: (root.view.activeTab === "tracks" || root.view.activeTab === "albums")
visible: ["all", "tracks", "albums"].indexOf(root.view.activeTab) >= 0
&& root.view.tabHasItems
}
GroupSelect {
@@ -560,9 +325,7 @@ Item {
]
onPicked: function (v) { root.view.setPref("albumsGroup", v) }
}
// Artists group (A-Z) — GRID mode only, like the reference
// (FavoritesView.slint:763: "Tauri hides it in sidepanel"; the
// sidepanel's left rail is ALWAYS A-Z grouped).
// Grid grouping is independent of the chosen sort.
GroupSelect {
visible: root.view.activeTab === "artists" && root.view.tabHasItems
&& root.view.artistsView === "grid"
@@ -574,152 +337,324 @@ Item {
onPicked: function (v) { root.view.setPref("artistsGroup", v) }
}
// Albums sort popup (real, JS-side).
QbzSelect {
id: sortSelect
sm: true
menuWidth: 190
popupWidth: 200
options: root.sortOptions
searchable: options.length > 8
currentIndex: {
var field = ReleaseSort.fieldOf(root.view.activeSort)
for (var i = 0; i < options.length; ++i)
if (options[i].value === field) return i
return 0
}
sortDirection: ReleaseSort.ascending(root.view.activeSort) ? "asc" : "desc"
onSelected: function(index) { root.view.pickSort(options[index].value) }
}
QbzSelect {
visible: root.view.activeTab === "playlists"
sm: true
menuWidth: 180
options: [QbzSession.tr("All", QbzSession.trRev),
QbzSession.tr("By you", QbzSession.trRev),
QbzSession.tr("By Qobuz", QbzSession.trRev),
QbzSession.tr("By others", QbzSession.trRev)]
currentIndex: Math.max(0, ["all", "you", "qobuz", "others"].indexOf(root.view.playlistsSubTab))
onSelected: function (index) {
root.view.playlistsSubTab = ["all", "you", "qobuz", "others"][index]
}
}
Rectangle {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
width: albumsSortRow.width
visible: rightButtons.width > 0
width: 1
height: 20
anchors.verticalCenter: parent.verticalCenter
color: theme.borderSubtle
}
Row {
id: rightButtons
height: 30
radius: 6
color: albumsSortArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
spacing: 8
Row {
id: albumsSortRow
height: parent.height
leftPadding: 10
rightPadding: 10
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Sort", QbzSession.trRev) + ": " + (
root.view.albumsSort === "title-asc" ? QbzSession.tr("Title A-Z", QbzSession.trRev)
: root.view.albumsSort === "title-desc" ? QbzSession.tr("Title Z-A", QbzSession.trRev)
: root.view.albumsSort === "artist-asc" ? QbzSession.tr("Artist A-Z", QbzSession.trRev)
: root.view.albumsSort === "newest" ? QbzSession.tr("Newest", QbzSession.trRev)
: root.view.albumsSort === "oldest" ? QbzSession.tr("Oldest", QbzSession.trRev)
: QbzSession.tr("Default", QbzSession.trRev))
color: theme.textSecondary
font.pixelSize: 12
id: actionRow
height: 30
spacing: 8
function randomVisibleId(kind) {
var pool = []
var rows = root.view.visibleRows
for (var i = 0; i < rows.length; i++)
if (rows[i].kind === kind) pool.push(rows[i])
if (pool.length === 0) return null
return pool[Math.floor(Math.random() * pool.length)]
}
QbzIcon { name: "chevron-down"; width: 12; height: 12; anchors.verticalCenter: parent.verticalCenter; tintName: "secondary" }
}
MouseArea {
id: albumsSortArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: albumsSortMenu.openBelowRight(albumsSortArea)
}
QbzContextMenu {
id: albumsSortMenu
menuWidth: 172
Repeater {
model: [
{ "value": "default", "label": QbzSession.tr("Default", QbzSession.trRev) },
{ "value": "newest", "label": QbzSession.tr("Newest", QbzSession.trRev) },
{ "value": "oldest", "label": QbzSession.tr("Oldest", QbzSession.trRev) },
{ "value": "title-asc", "label": QbzSession.tr("Title A-Z", QbzSession.trRev) },
{ "value": "title-desc", "label": QbzSession.tr("Title Z-A", QbzSession.trRev) },
{ "value": "artist-asc", "label": QbzSession.tr("Artist A-Z", QbzSession.trRev) },
]
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 33
radius: 5
color: abSortOptArea.containsMouse ? theme.surfaceHover : "transparent"
Text {
anchors.fill: parent
anchors.leftMargin: 8
text: modelData.label
color: theme.textSecondary
font.pixelSize: 13
font.weight: root.view.albumsSort === modelData.value ? theme.weightSemibold : theme.weightRegular
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: abSortOptArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.view.setPref("albumsSort", modelData.value)
albumsSortMenu.close()
}
}
// Tracks — play all / shuffle all / select multiple.
QbzIconButton {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
btnSize: 30
name: "play-fill"
onClicked: QbzLibrary.libraryPlayAll(
JSON.stringify(root.view.visibleTrackIds()), false)
}
QbzIconButton {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: QbzLibrary.libraryPlayAll(
JSON.stringify(root.view.visibleTrackIds()), true)
}
ToolToggle {
visible: root.view.activeTab === "tracks" && root.view.tabHasItems
name: "square-check-big"
active: root.view.tracksMultiSelect
onClicked: root.view.setTracksMultiSelect(!root.view.tracksMultiSelect)
}
// Albums — play a RANDOM album (the reference's `albums_shuffle`,
// which picks one visible album, not a shuffled play of every album).
QbzIconButton {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("album")
if (pick) QbzPlayer.playAlbum(pick.id)
}
}
// Albums multi-select — LIST mode ONLY (owner 2026-07-24,
// FavoritesView.slint:512-523): the grid card has no checkbox slot.
ToolToggle {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
&& root.view.albumsView === "list"
name: "square-check-big"
active: root.view.albumsMultiSelect
onClicked: root.view.setAlbumsMultiSelect(!root.view.albumsMultiSelect)
}
// Artists — open a random artist.
QbzIconButton {
visible: root.view.activeTab === "artists" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("artist")
if (pick) QbzArtist.openArtist(pick.id)
}
}
// Playlists — play a random playlist.
QbzIconButton {
visible: root.view.activeTab === "playlists" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("playlist")
if (pick) QbzBridge.openPlaylist(pick.id)
}
}
// Labels — open a random label's landing.
QbzIconButton {
visible: root.view.activeTab === "labels" && root.view.tabHasItems
btnSize: 30
name: "shuffle"
onClicked: {
var pick = actionRow.randomVisibleId("label")
if (pick) QbzHome.openLabel(pick.id)
}
}
}
ToolToggle {
visible: root.view.activeTab === "all"
name: root.view.viewMode === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.viewMode = root.view.viewMode === "list" ? "grid" : "list"
}
// Albums grid / list toggle (FavoritesView.slint:697-705 ViewToggle).
ToolToggle {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
name: root.view.albumsView === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("albumsView",
root.view.albumsView === "list" ? "grid" : "list")
}
// Playlists grid / list toggle (FavoritesView.slint:737-745).
ToolToggle {
visible: root.view.activeTab === "playlists" && root.view.tabHasItems
name: root.view.playlistsView === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("playlistsView",
root.view.playlistsView === "list" ? "grid" : "list")
}
// Artists grid / sidepanel toggle (FavoritesView.slint:780-793 —
// `active: false` there too, "keeps the two icon states visually
// identical").
ToolToggle {
visible: root.view.activeTab === "artists" && root.view.tabHasItems
name: root.view.artistsView === "sidepanel" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("artistsView",
root.view.artistsView === "sidepanel" ? "grid" : "sidepanel")
}
}
// Albums grid / list toggle (FavoritesView.slint:697-705 ViewToggle).
ToolToggle {
visible: root.view.activeTab === "albums" && root.view.tabHasItems
name: root.view.albumsView === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("albumsView",
root.view.albumsView === "list" ? "grid" : "list")
}
} // topChrome
// Search remains visible, including when a restriction produces no rows.
QbzLineEdit {
id: searchBox
objectName: "librarySearch"
parent: root.singleRow ? firstRowSearchSlot : root
x: root.singleRow ? 0 : 34
y: root.singleRow ? 0 : root.secondRowY
width: root.fullSearchWidth * (root.singleRow ? 0.6 : 1)
searchMode: true
expandable: false
sm: true
text: root.view.activeTab === "all" ? root.view.search : root.view.tabSearch
placeholder: root.view.activeTab === "tracks" ? QbzSession.tr("Search your tracks", QbzSession.trRev)
: root.view.activeTab === "albums" ? QbzSession.tr("Search your releases", QbzSession.trRev)
: root.view.activeTab === "playlists" ? QbzSession.tr("Search your playlists", QbzSession.trRev)
: root.view.activeTab === "artists" ? QbzSession.tr("Search your artists", QbzSession.trRev)
: root.view.activeTab === "labels" ? QbzSession.tr("Search your labels", QbzSession.trRev)
: QbzSession.tr("Search your library", QbzSession.trRev)
onEdited: function (value) {
if (root.view.activeTab === "all") root.view.setAllSearch(value)
else root.view.tabSearch = value
}
// Playlists grid / list toggle (FavoritesView.slint:737-745).
ToolToggle {
visible: root.view.activeTab === "playlists" && root.view.tabHasItems
name: root.view.playlistsView === "list" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("playlistsView",
root.view.playlistsView === "list" ? "grid" : "list")
}
// Compact Library-only switch: glyph travels inside the thumb.
component SourceSwitch: Rectangle {
id: sourceSwitch
property string label: ""
property string explanation: ""
property string glyph: ""
property bool checked: false
signal toggled(bool value)
width: 46
height: 26
anchors.verticalCenter: parent.verticalCenter
radius: 13
color: checked ? theme.accent : theme.surfaceHover
activeFocusOnTab: true
border.width: activeFocus ? 2 : 0
border.color: theme.accent
Accessible.role: Accessible.CheckBox
Accessible.name: label
Accessible.description: explanation
Accessible.checked: checked
Accessible.onToggleAction: sourceSwitch.toggled(!checked)
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Space || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (!event.isAutoRepeat) sourceSwitch.toggled(!checked)
event.accepted = true
}
}
// Artists grid / sidepanel toggle (FavoritesView.slint:780-793 —
// `active: false` there too, "keeps the two icon states visually
// identical").
ToolToggle {
visible: root.view.activeTab === "artists" && root.view.tabHasItems
name: root.view.artistsView === "sidepanel" ? "layout-grid" : "list"
active: false
onClicked: root.view.setPref("artistsView",
root.view.artistsView === "sidepanel" ? "grid" : "sidepanel")
Rectangle {
width: 20
height: 20
radius: 10
x: sourceSwitch.checked ? sourceSwitch.width - width - 3 : 3
y: 3
color: theme.textPrimary
QbzIcon {
anchors.centerIn: parent
width: 15
height: 15
visible: sourceSwitch.glyph !== ""
name: sourceSwitch.glyph
tintName: theme.isDark ? "black" : "white"
}
Text {
anchors.centerIn: parent
visible: sourceSwitch.glyph === ""
text: "Hi-Res"
font.pixelSize: 7
font.weight: Font.DemiBold
color: theme.surfaceMain
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: sourceSwitch.forceActiveFocus()
onClicked: sourceSwitch.toggled(!sourceSwitch.checked)
}
// Playlists sub-tab (Library / Following).
Rectangle {
visible: root.view.activeTab === "playlists" && root.view.tabHasItems
width: plSubRow.width
HoverHandler { onHoveredChanged: hovered ? switchTip.enter() : switchTip.exit() }
QbzFilterTip {
id: switchTip
ownerKey: "library-switch-" + sourceSwitch.label
anchor: sourceSwitch
groups: [{group: sourceSwitch.label, values: [sourceSwitch.explanation]}]
}
}
Flickable {
id: filterChrome
x: 34 + searchBox.width + 20
width: Math.max(0, root.width - x - 32)
y: root.secondRowY
visible: !root.singleRow
height: 30
clip: true
contentWidth: Math.max(width, switches.width)
contentHeight: height
flickableDirection: Flickable.HorizontalFlick
boundsBehavior: Flickable.StopAtBounds
Row {
id: switches
x: Math.max(0, filterChrome.width - width)
height: 30
radius: 6
color: theme.surfaceElevated
Row {
id: plSubRow
padding: 3
spacing: 4
Repeater {
model: [
{ "id": "favorites", "label": QbzSession.tr("Library", QbzSession.trRev) },
{ "id": "following", "label": QbzSession.tr("Following", QbzSession.trRev) },
]
delegate: Rectangle {
required property var modelData
property bool active: root.view.playlistsSubTab === modelData.id
width: plSubText.implicitWidth + 20
height: 24
radius: 4
color: active ? theme.surfaceMain : "transparent"
Text {
id: plSubText
anchors.centerIn: parent
text: modelData.label
color: parent.active ? theme.textPrimary : theme.textMuted
font.pixelSize: 12
font.weight: theme.weightMedium
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.view.playlistsSubTab = modelData.id
}
}
}
spacing: 12
readonly property bool sourceTab: root.view.activeTab === "all"
|| root.view.activeTab === "tracks" || root.view.activeTab === "albums"
SourceSwitch {
visible: switches.sourceTab
glyph: "shopping-bag"
label: QbzSession.tr("Purchases only", QbzSession.trRev)
explanation: QbzSession.tr("Show only purchased items. Combines with the other enabled filters.", QbzSession.trRev)
checked: root.view.showPurchases
onToggled: function (value) { root.view.setShowPurchases(value) }
}
SourceSwitch {
visible: switches.sourceTab
glyph: "heart"
label: QbzSession.tr("Favorites only", QbzSession.trRev)
explanation: QbzSession.tr("Show only favorites. Combines with the other enabled filters.", QbzSession.trRev)
checked: root.view.showFavorites
onToggled: function (value) { root.view.setShowFavorites(value) }
}
SourceSwitch {
visible: root.view.activeTab === "all"
glyph: "user-plus"
label: QbzSession.tr("Following only", QbzSession.trRev)
explanation: QbzSession.tr("Show only followed items. Combines with the other enabled filters.", QbzSession.trRev)
checked: root.view.showFollowing
onToggled: function (value) { root.view.setShowFollowing(value) }
}
SourceSwitch {
visible: root.view.activeTab === "tracks" || root.view.activeTab === "albums"
label: QbzSession.tr("Hi-Res only", QbzSession.trRev)
explanation: QbzSession.tr("Show only Hi-Res items. Combines with the other enabled filters.", QbzSession.trRev)
checked: root.view.hiresOnly
onToggled: function (value) { root.view.setHiresOnly(value) }
}
SourceSwitch {
visible: root.view.activeTab === "all"
glyph: "hard-drive"
label: QbzSession.tr("Hide local albums from library", QbzSession.trRev)
explanation: QbzSession.tr("Hide local albums here. They remain available in Local Library.", QbzSession.trRev)
checked: !root.view.showLocal
onToggled: function (value) { root.view.setShowLocal(!value) }
}
}
}
/// Collapse the shared per-tab search field (LibraryView calls this on
/// every tab change — the reference destroys and re-creates it collapsed).
function closeTabSearch() {
if (tabSearchBox) tabSearchBox.closeSearch()
searchBox.text = Qt.binding(function () {
return root.view.activeTab === "all" ? root.view.search : root.view.tabSearch
})
}
}