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:
33 files changed
+1938
-547
No files matched your search
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user