fix(qbz-qt): local context menus, collapsing search fields, toolbar density
Four things the owner hit in Local Library. The context menu did nothing there — local rows and cards now mount the shared menu the rest of the app uses, with the action set Slint gives local and Plex items. Anything with no seam in this port (add-to-playlist, add-to-mixtape, the tag editor, local favourites) is hidden or visibly disabled rather than a row that silently does nothing. The per-view search fields did not open and collapse the way the header's does. That behaviour moved into the shared QbzLineEdit, so the Local toolbar, the queue and the tree rail all get it from one place instead of four copies. The queue's field also escaped its 300px column; it is width-bound now. Filter, sorting and grouping become icon-only buttons with an explanatory tooltip, which is what the owner asked for and what the crowding called for. Found on the way, and affecting Qobuz cards too: the album card's ⋯ menu had no "Play later" at all, and its "Add to queue" row dispatched play-later — the row lied about what it did. Both fixed, with Slint's icon set for the three. cc
This commit is contained in:
15 files changed
+767
-202
No files matched your search
@@ -97,6 +97,8 @@ fn main() {
|
||||
"qml/views/PlaylistView.qml",
|
||||
"qml/views/SearchView.qml",
|
||||
"qml/views/SectionRail.qml",
|
||||
"qml/views/local/LocalTip.qml",
|
||||
"qml/views/local/LocalIconSelect.qml",
|
||||
"qml/shell/FavToggle.qml",
|
||||
"qml/shell/InfoCreditCell.qml",
|
||||
"qml/shell/InfoMetaCell.qml",
|
||||
|
||||
@@ -216,7 +216,8 @@ Rectangle {
|
||||
var m = [
|
||||
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-plus", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
if (!root.localMode) {
|
||||
@@ -264,13 +265,15 @@ Rectangle {
|
||||
if (a === "open") root.openRequested()
|
||||
else if (a === "play") root.playRequested()
|
||||
else if (a === "next") root.enqueueRequested("next")
|
||||
else if (a === "queue") root.enqueueRequested("later")
|
||||
else if (a === "later") root.enqueueRequested("later")
|
||||
else if (a === "queue") root.enqueueRequested("queue")
|
||||
return
|
||||
}
|
||||
if (a === "open") QbzAlbum.openAlbum(root.albumId)
|
||||
else if (a === "play") QbzPlayer.playAlbum(root.albumId)
|
||||
else if (a === "next") QbzPlayer.enqueueAlbum(root.albumId, "next")
|
||||
else if (a === "queue") QbzPlayer.enqueueAlbum(root.albumId, "later")
|
||||
else if (a === "later") QbzPlayer.enqueueAlbum(root.albumId, "later")
|
||||
else if (a === "queue") QbzPlayer.enqueueAlbum(root.albumId, "queue")
|
||||
else if (a === "favorite") root.toggleFavorite()
|
||||
// "block" (no blacklist store): inert —
|
||||
// POC-NOTE.
|
||||
|
||||
@@ -1,58 +1,215 @@
|
||||
// QbzLineEdit (std-widgets LineEdit, settings styling) — extracted from
|
||||
// SettingsView.qml in phase 19. 240px / 34px elevated r8 bordered input;
|
||||
// QbzLineEdit — THE shared text input. Two arms over ONE control, so there
|
||||
// is never a second search-box implementation in the tree:
|
||||
//
|
||||
// * PLAIN (default, unchanged): 240x34 elevated r8 bordered input that
|
||||
// commits on Enter AND on focus loss (the Tauri onchange semantics —
|
||||
// PlaybackSettings.slint).
|
||||
// PlaybackSettings.slint). Settings panels use this arm.
|
||||
//
|
||||
// * SEARCH (`searchMode`): leading magnifier, LIVE `edited(text)` and a
|
||||
// trailing X. With `expandable` it is primitives/ExpandableSearch.slint
|
||||
// 1:1 — a 34px (30px when `sm`) magnifier slot that, on click, opens a
|
||||
// RIGHT-ANCHORED field growing LEFT over whatever sits beside it (200ms
|
||||
// ease-in-out), the magnifier cross-fading out (120ms); focus is grabbed
|
||||
// one tick later because focusing synchronously inside the open races the
|
||||
// open animation (the Slint comment at ExpandableSearch.slint:26); the X
|
||||
// clears AND closes; Esc closes. The closed footprint is fixed, so
|
||||
// nothing to the control's right ever moves.
|
||||
//
|
||||
// Every per-view search box (Local Library toolbars, tree rail, folder
|
||||
// detail, queue) is this control.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// --- plain arm (API unchanged) ---------------------------------------
|
||||
property string text: ""
|
||||
property string placeholder: ""
|
||||
property bool isPassword: false
|
||||
signal committed(string value)
|
||||
|
||||
// --- search arm -------------------------------------------------------
|
||||
/// Leading magnifier + live `edited` + trailing X.
|
||||
property bool searchMode: false
|
||||
/// Collapse to the magnifier slot (implies searchMode semantics).
|
||||
property bool expandable: false
|
||||
/// Bootstrap-style small variant (ExpandableSearch `sm`): 30px, not 34px.
|
||||
property bool sm: false
|
||||
/// Open width of the expandable field (ExpandableSearch max-open-width).
|
||||
property int openWidth: 196
|
||||
/// surface-elevated fill (toolbar variant) vs surface-card (rail/overlay).
|
||||
property bool elevated: true
|
||||
property bool open: false
|
||||
signal edited(string value)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
width: 240
|
||||
height: 34
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
color: theme.surfaceElevated
|
||||
readonly property int collapsedSize: sm ? 30 : 34
|
||||
readonly property int glyph: sm ? 13 : 14
|
||||
|
||||
// The ROOT keeps the closed footprint; only the inner field animates, so
|
||||
// a positioner never reflows while the search opens.
|
||||
width: expandable ? collapsedSize : 240
|
||||
height: (searchMode || expandable) ? collapsedSize : 34
|
||||
color: "transparent"
|
||||
|
||||
function clearSearch() {
|
||||
input.text = ""
|
||||
root.text = ""
|
||||
root.edited("")
|
||||
}
|
||||
function closeSearch() {
|
||||
clearSearch()
|
||||
root.open = false
|
||||
input.focus = false
|
||||
}
|
||||
|
||||
// Deferred focus — see the header note.
|
||||
onOpenChanged: if (open) focusDefer.restart()
|
||||
Timer {
|
||||
id: focusDefer
|
||||
interval: 30
|
||||
repeat: false
|
||||
onTriggered: input.forceActiveFocus()
|
||||
}
|
||||
|
||||
// --- Closed: the magnifier toggle (fades, so both directions are smooth)
|
||||
Rectangle {
|
||||
visible: root.expandable
|
||||
x: root.width - width
|
||||
width: root.collapsedSize
|
||||
height: root.collapsedSize
|
||||
radius: 6
|
||||
opacity: root.open ? 0.0 : 1.0
|
||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||
color: openArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
QbzIcon {
|
||||
name: "search"
|
||||
width: root.sm ? 14 : 16
|
||||
height: root.sm ? 14 : 16
|
||||
anchors.centerIn: parent
|
||||
tintName: openArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: openArea
|
||||
anchors.fill: parent
|
||||
enabled: !root.open
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.open = true
|
||||
}
|
||||
}
|
||||
|
||||
// --- The field. Right-anchored, so an expandable one grows LEFT. ------
|
||||
Rectangle {
|
||||
id: box
|
||||
x: root.width - width
|
||||
width: root.expandable ? (root.open ? root.openWidth : 0) : root.width
|
||||
height: root.height
|
||||
radius: (root.searchMode || root.expandable) ? 6 : theme.radiusSm
|
||||
color: root.elevated ? theme.surfaceElevated : theme.surfaceCard
|
||||
border.width: (root.expandable && !root.open) ? 0 : 1
|
||||
border.color: input.activeFocus ? theme.accent : theme.borderSubtle
|
||||
clip: true
|
||||
Behavior on width {
|
||||
enabled: root.expandable
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.searchMode ? (root.expandable ? 11 : 9) : 12
|
||||
anchors.rightMargin: root.searchMode ? 5 : 12
|
||||
spacing: root.searchMode ? 7 : 0
|
||||
|
||||
QbzIcon {
|
||||
visible: root.searchMode
|
||||
name: "search"
|
||||
width: root.glyph
|
||||
height: root.glyph
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "muted"
|
||||
}
|
||||
Item {
|
||||
// Clamped: the open animation passes through width 0 and a
|
||||
// negative width is a silent layout trap.
|
||||
width: Math.max(0, parent.width
|
||||
- (root.searchMode ? root.glyph + 7 : 0)
|
||||
- (clearSlot.visible ? clearSlot.width + 7 : 0))
|
||||
height: parent.height
|
||||
clip: true
|
||||
TextInput {
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.pixelSize: root.searchMode ? 12 : theme.fontBody
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
clip: true
|
||||
echoMode: parent.isPassword ? TextInput.Password : TextInput.Normal
|
||||
text: parent.text
|
||||
onAccepted: parent.committed(text)
|
||||
onActiveFocusChanged: if (!activeFocus) parent.committed(text)
|
||||
// External republish (e.g. Reset) re-seeds the field while it is
|
||||
// not being edited.
|
||||
selectByMouse: true
|
||||
echoMode: root.isPassword ? TextInput.Password : TextInput.Normal
|
||||
text: root.text
|
||||
onAccepted: root.committed(text)
|
||||
onActiveFocusChanged: if (!activeFocus) root.committed(text)
|
||||
onTextEdited: root.edited(text)
|
||||
Keys.onEscapePressed: function (event) {
|
||||
if (root.expandable) {
|
||||
root.closeSearch()
|
||||
event.accepted = true
|
||||
} else {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
// External republish (e.g. Reset) re-seeds the field while
|
||||
// it is not being edited.
|
||||
Binding {
|
||||
target: input
|
||||
property: "text"
|
||||
value: input.parent.text
|
||||
value: root.text
|
||||
when: !input.activeFocus
|
||||
}
|
||||
}
|
||||
Text {
|
||||
visible: input.text === ""
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
text: placeholder
|
||||
text: root.placeholder
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontBody
|
||||
font.pixelSize: root.searchMode ? 12 : theme.fontBody
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
// Trailing X — clears (plain search) or clears AND closes
|
||||
// (expandable, back to the magnifier).
|
||||
Item {
|
||||
id: clearSlot
|
||||
visible: root.searchMode && (root.expandable || input.text !== "")
|
||||
width: visible ? (root.sm ? 22 : 24) : 0
|
||||
height: parent.height
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.sm ? 22 : 24
|
||||
height: root.sm ? 22 : 24
|
||||
radius: 4
|
||||
color: clearArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
QbzIcon {
|
||||
name: "x"
|
||||
width: 12
|
||||
height: 12
|
||||
anchors.centerIn: parent
|
||||
tintName: clearArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expandable ? root.closeSearch() : root.clearSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,6 +631,7 @@ Rectangle {
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Row {
|
||||
id: footerRow
|
||||
width: parent.width
|
||||
leftPadding: theme.spacingMd
|
||||
rightPadding: theme.spacingMd
|
||||
@@ -638,6 +639,12 @@ Rectangle {
|
||||
bottomPadding: 10
|
||||
spacing: theme.spacingXs
|
||||
|
||||
// The row's usable width — `parent.width` here is the PANEL
|
||||
// width, padding included; using it raw is what pushed the
|
||||
// search field out of the 300px column.
|
||||
readonly property int contentWidth:
|
||||
width - leftPadding - rightPadding
|
||||
|
||||
// Action 1 — Clear queue (wired).
|
||||
QbzIconButton { btnSize: 30
|
||||
name: "trash-list"
|
||||
@@ -651,42 +658,24 @@ Rectangle {
|
||||
// Action 4 — sleep timer (INERT engine — POC-NOTE).
|
||||
QbzIconButton { btnSize: 30; name: "clock" }
|
||||
|
||||
// Inline queue search field (wired).
|
||||
Rectangle {
|
||||
width: parent.width - 4 * 30 - 4 * theme.spacingXs
|
||||
height: 30
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 9
|
||||
anchors.rightMargin: 7
|
||||
spacing: 6
|
||||
QbzIcon {
|
||||
name: "search"
|
||||
width: 13
|
||||
height: 13
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
width: parent.width - 20
|
||||
height: parent.height
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
clip: true
|
||||
onTextEdited: QbzQueue.queueSetSearch(text)
|
||||
Text {
|
||||
visible: parent.text === ""
|
||||
anchors.fill: parent
|
||||
text: QbzSession.tr("Search queue", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
// Filler so the collapsed magnifier sits at the right edge;
|
||||
// the field opens LEFT across it (5 gaps of spacingXs).
|
||||
Item {
|
||||
width: Math.max(0, footerRow.contentWidth - 4 * 30 - 30
|
||||
- 5 * theme.spacingXs)
|
||||
height: 1
|
||||
}
|
||||
// Inline queue search (wired) — the shared expandable control.
|
||||
QbzLineEdit {
|
||||
searchMode: true
|
||||
expandable: true
|
||||
sm: true
|
||||
// Bound to the PANEL, never wider: it opens leftward over
|
||||
// the four action buttons, exactly like the Slint
|
||||
// ExpandableSearch does over its neighbours.
|
||||
openWidth: Math.max(90, footerRow.contentWidth)
|
||||
placeholder: QbzSession.tr("Search queue", QbzSession.trRev)
|
||||
onEdited: function (v) { QbzQueue.queueSetSearch(v) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
// border on hover), square cover = card width - 16 with a folder placeholder,
|
||||
// name 13px/medium, recursive track count 11px/muted. Click drills in.
|
||||
|
||||
// Right-click opens the same folder queue menu the tree rows carry (a
|
||||
// deliberate divergence — the Slint card has no menu; see TreeRow.qml).
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../controls"
|
||||
import "../../theme"
|
||||
|
||||
Rectangle {
|
||||
@@ -66,7 +70,29 @@ Rectangle {
|
||||
id: cardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.opened()
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
folderMenu.openAtCursor(cardArea, mouse.x, mouse.y)
|
||||
return
|
||||
}
|
||||
root.opened()
|
||||
}
|
||||
}
|
||||
|
||||
CardMenu {
|
||||
id: folderMenu
|
||||
menuWidth: 200
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
onPicked: function (a) {
|
||||
if (a === "play") QbzLocal.playFolder(root.item.path)
|
||||
else QbzLocal.enqueue("folder", root.item.path, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../cards"
|
||||
import "../../controls"
|
||||
import "../../theme"
|
||||
|
||||
Item {
|
||||
@@ -162,6 +163,30 @@ Item {
|
||||
required property var modelData
|
||||
width: 200
|
||||
height: 246
|
||||
|
||||
// Right-click menu for the card (AlbumCard.slint's
|
||||
// album-menu, local arm: no favourite, no Block —
|
||||
// the Slint hides Block for source local/plex).
|
||||
// The card's own ⋯ overlay button opens AlbumCard's
|
||||
// built-in menu; see the GLUE note about aligning
|
||||
// that one's entries with these.
|
||||
CardMenu {
|
||||
id: cardMenu
|
||||
menuWidth: 196
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
onPicked: function (a) {
|
||||
if (a === "open") root.openRequested(cardCell.modelData.id)
|
||||
else if (a === "play") root.playRequested(cardCell.modelData.id)
|
||||
else root.enqueueRequested(cardCell.modelData.id, a)
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCard {
|
||||
localMode: true
|
||||
albumId: cardCell.modelData.id
|
||||
@@ -182,6 +207,16 @@ Item {
|
||||
root.enqueueRequested(cardCell.modelData.id, m)
|
||||
}
|
||||
}
|
||||
// RIGHT-only, declared after the card so every
|
||||
// left click still reaches the card's own areas.
|
||||
MouseArea {
|
||||
id: cardRc
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: function (mouse) {
|
||||
cardMenu.openAtCursor(cardRc, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
// Multi-select tick — the collection view's
|
||||
// select-mode overlay (top-left of the artwork).
|
||||
Rectangle {
|
||||
@@ -216,6 +251,9 @@ Item {
|
||||
checked: root.selected[modelData.items[0].id] === true
|
||||
onOpened: root.openRequested(modelData.items[0].id)
|
||||
onPlayRequested: root.playRequested(modelData.items[0].id)
|
||||
onEnqueueRequested: function (m) {
|
||||
root.enqueueRequested(modelData.items[0].id, m)
|
||||
}
|
||||
onToggleSelect: root.toggleSelect(modelData.items[0].id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
// surfaces (AlbumCollectionView's list mode, mounted by
|
||||
// LocalLibraryView.slint:1255 with show-source / show-source-badge on).
|
||||
//
|
||||
// 56px: cover, title + artist, year, track count, quality mark and the
|
||||
// source column. Multi-select puts the shared checkbox in front of the
|
||||
// cover, as the collection view does.
|
||||
// 56px: cover, title + artist, year, track count, quality mark, the source
|
||||
// column and the trailing ⋯ overflow. Multi-select puts the shared checkbox
|
||||
// in front of the cover, as the collection view does.
|
||||
//
|
||||
// The ⋯ menu is AlbumListRow.slint:381 minus its two Qobuz-only entries: the
|
||||
// favourite heart (local albums are not catalog favourites) and "Block this
|
||||
// album" (which the Slint itself hides for source local/plex, :435). What is
|
||||
// left is the five entries Slint always shows — Open album, Play, Play next,
|
||||
// Play later, Add to queue — and all five are wired. Right-clicking the row
|
||||
// opens the same menu at the pointer.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
@@ -21,6 +28,7 @@ Rectangle {
|
||||
property bool checked: false
|
||||
signal opened()
|
||||
signal playRequested()
|
||||
signal enqueueRequested(string mode)
|
||||
signal toggleSelect()
|
||||
|
||||
QbzTheme { id: theme }
|
||||
@@ -33,11 +41,36 @@ Rectangle {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.selectMode ? root.toggleSelect() : root.opened()
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
rowMenu.openAtCursor(rowArea, mouse.x, mouse.y)
|
||||
return
|
||||
}
|
||||
if (root.selectMode) root.toggleSelect()
|
||||
else root.opened()
|
||||
}
|
||||
onDoubleClicked: if (!root.selectMode) root.playRequested()
|
||||
}
|
||||
|
||||
CardMenu {
|
||||
id: rowMenu
|
||||
menuWidth: 196
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
onPicked: function (a) {
|
||||
if (a === "open") root.opened()
|
||||
else if (a === "play") root.playRequested()
|
||||
else root.enqueueRequested(a)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
@@ -68,8 +101,9 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent.width - 40 - 70 - 90 - 92 - (root.showSource ? 34 : 0)
|
||||
- (root.selectMode ? 28 : 0) - 5 * 12
|
||||
width: parent.width - 40 - 70 - 90 - 92 - 32
|
||||
- (root.showSource ? 34 : 0)
|
||||
- (root.selectMode ? 28 : 0) - 6 * 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
@@ -125,5 +159,27 @@ Rectangle {
|
||||
tintName: root.item.source === "plex" ? "accent" : "muted"
|
||||
}
|
||||
}
|
||||
// Trailing ⋯ overflow (AlbumListRow.slint:360).
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: moreArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
QbzIcon {
|
||||
name: "ellipsis"
|
||||
width: 18
|
||||
height: 18
|
||||
anchors.centerIn: parent
|
||||
tintName: moreArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: moreArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,9 @@ Item {
|
||||
onClicked: QbzLocal.playFolder(root.view.selectedFolder)
|
||||
}
|
||||
Column {
|
||||
width: parent.width - 40 - 200 - 60 - 3 * 12
|
||||
// The search collapses to a 30px magnifier and opens LEFT
|
||||
// over this column, so the name block owns that space.
|
||||
width: parent.width - 40 - 30 - 60 - 3 * 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
@@ -85,8 +87,6 @@ Item {
|
||||
LocalSearchBox {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
boxWidth: 200
|
||||
iconSize: 14
|
||||
leftPad: 10
|
||||
placeholder: QbzSession.tr("Search albums...", QbzSession.trRev)
|
||||
onEdited: function (v) { root.view.folderDetailSearch = v }
|
||||
}
|
||||
@@ -197,8 +197,30 @@ Item {
|
||||
id: subArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.view.selectFolder(subRow.modelData.path)
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
subMenu.openAtCursor(subArea, mouse.x, mouse.y)
|
||||
return
|
||||
}
|
||||
root.view.selectFolder(subRow.modelData.path)
|
||||
}
|
||||
}
|
||||
// Same folder queue menu as the tree rows.
|
||||
CardMenu {
|
||||
id: subMenu
|
||||
menuWidth: 200
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
onPicked: function (a) {
|
||||
if (a === "play") QbzLocal.playFolder(subRow.modelData.path)
|
||||
else QbzLocal.enqueue("folder", subRow.modelData.path, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Icon-only dropdown for the Local Library toolbars: a 34x30 bordered button
|
||||
// — the SAME chrome as the quality/format filter button beside it
|
||||
// (LocalLibraryView.slint:857) — that opens its option list as the shared
|
||||
// controls/QbzContextMenu.qml, with a check on the current option and a hover
|
||||
// tooltip naming the control AND its current value.
|
||||
//
|
||||
// DELIBERATE DIVERGENCE from the reference: LocalLibraryView.slint spends this
|
||||
// space on full-text QbzSelects (sort :793 / :952 / :1033, group :824 / :988 /
|
||||
// :1063). The owner asked for icon-only + tooltip because the toolbar row was
|
||||
// too crowded. The option lists, their order, the persisted keys and the
|
||||
// wiring are unchanged — only the trigger's presentation is.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../controls"
|
||||
import "../../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
/// What the control IS ("Sort", "Group by") — the tooltip's lead.
|
||||
property string label: ""
|
||||
/// Plain label strings, in the same order the host maps to its keys.
|
||||
property var options: []
|
||||
property int currentIndex: 0
|
||||
property int menuWidth: 190
|
||||
/// Accent chrome while the control is off its default option (the
|
||||
/// filter button's own "active" cue).
|
||||
property bool marked: false
|
||||
signal selected(int index)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
width: 34
|
||||
height: 30
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 6
|
||||
border.width: 1
|
||||
border.color: root.marked ? theme.accent : theme.borderSubtle
|
||||
color: btnArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
QbzIcon {
|
||||
name: root.iconName
|
||||
width: 15
|
||||
height: 15
|
||||
anchors.centerIn: parent
|
||||
tintName: root.marked ? "accent" : "secondary"
|
||||
}
|
||||
MouseArea {
|
||||
id: btnArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: menu.openBelowRight(root)
|
||||
}
|
||||
}
|
||||
|
||||
LocalTip {
|
||||
visible: btnArea.containsMouse && !menu.visible
|
||||
text: root.label
|
||||
+ (root.currentIndex >= 0 && root.currentIndex < root.options.length
|
||||
? ": " + root.options[root.currentIndex] : "")
|
||||
}
|
||||
|
||||
QbzContextMenu {
|
||||
id: menu
|
||||
menuWidth: root.menuWidth
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
delegate: Rectangle {
|
||||
id: optRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent ? parent.width : 0
|
||||
height: 33
|
||||
radius: 5
|
||||
color: optArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
QbzIcon {
|
||||
// "" renders nothing — the slot keeps the labels aligned.
|
||||
name: optRow.index === root.currentIndex ? "check" : ""
|
||||
width: 15
|
||||
height: 15
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "accent"
|
||||
}
|
||||
Text {
|
||||
height: parent.height
|
||||
width: parent.width - 23
|
||||
text: optRow.modelData
|
||||
color: optRow.index === root.currentIndex
|
||||
? theme.accent : theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: optArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
menu.close()
|
||||
root.selected(optRow.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,28 @@
|
||||
// Local Library search input — the ONE box the whole surface uses.
|
||||
// Local Library search input — a NAMING wrapper over the shared
|
||||
// controls/QbzLineEdit.qml search arm, kept only so the six local call sites
|
||||
// read as one thing and tune in one place.
|
||||
//
|
||||
// Slint has three near-identical inputs on this view (LocalLibraryView.slint:
|
||||
// the toolbar ExpandableSearch `sm`, the tree-rail header input at :1724 and
|
||||
// the folder-detail input at :1984). They differ only in width, left padding
|
||||
// and glyph size, so this is one component with those three as properties
|
||||
// instead of three copies.
|
||||
//
|
||||
// Numbers: 30px tall, radius 6, surface-card fill, 1px border that turns
|
||||
// accent on focus, 12px text, muted placeholder (Slint :1726-1771).
|
||||
// the toolbar `ExpandableSearch { sm: true }` at :782, the tree-rail header
|
||||
// input at :1724 and the folder-detail input at :1984). All three are the
|
||||
// EXPANDABLE form here: closed they are a 30px magnifier slot; clicking opens
|
||||
// a `boxWidth` field that grows LEFT over its neighbours, and the X clears and
|
||||
// closes it again (primitives/ExpandableSearch.slint).
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../theme"
|
||||
import "../../controls"
|
||||
|
||||
Rectangle {
|
||||
QbzLineEdit {
|
||||
id: root
|
||||
|
||||
property string placeholder: ""
|
||||
property alias text: input.text
|
||||
/// Width of the OPEN field (the closed footprint is always 30px).
|
||||
property int boxWidth: 200
|
||||
property int iconSize: 13
|
||||
property int leftPad: 9
|
||||
property int rightPad: 8
|
||||
/// Elevated fill for the toolbar variant; the rail/detail inputs use
|
||||
/// surface-card (Slint :1728).
|
||||
property bool elevated: false
|
||||
signal edited(string value)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
width: boxWidth
|
||||
height: 30
|
||||
radius: 6
|
||||
color: elevated ? theme.surfaceElevated : theme.surfaceCard
|
||||
border.width: 1
|
||||
border.color: input.activeFocus ? theme.accent : theme.borderSubtle
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.leftPad
|
||||
anchors.rightMargin: root.rightPad
|
||||
spacing: 6
|
||||
|
||||
QbzIcon {
|
||||
name: "search"
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "muted"
|
||||
}
|
||||
Item {
|
||||
width: parent.width - root.iconSize - 6
|
||||
height: parent.height
|
||||
clip: true
|
||||
TextInput {
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
clip: true
|
||||
selectByMouse: true
|
||||
onTextEdited: root.edited(text)
|
||||
}
|
||||
Text {
|
||||
visible: input.text === ""
|
||||
anchors.fill: parent
|
||||
text: root.placeholder
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
searchMode: true
|
||||
expandable: true
|
||||
sm: true
|
||||
// surface-card — ExpandableSearch.slint's own overlay fill (the port's
|
||||
// toolbar copy used surface-elevated; the reference does not).
|
||||
elevated: false
|
||||
openWidth: boxWidth
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Hover tooltip for the Local Library's icon-only toolbar controls.
|
||||
//
|
||||
// Slint routes these through a shell-level overlay (TooltipState, set by
|
||||
// LocalLibraryView.slint's CircleIconBtn / RailBulkBtn). This port has no
|
||||
// tooltip overlay singleton, so the pattern is the one the quality badge
|
||||
// already uses: a QtQuick.Controls ToolTip (a POPUP, so it survives the
|
||||
// `clip: true` containers these toolbars live in) styled to the Slint
|
||||
// numbers — 24px tall, radius sm, surface-elevated on a 1px border-subtle,
|
||||
// 11px medium secondary text, 9px side padding, 6px above its anchor.
|
||||
//
|
||||
// `delay` is 0 and the caller drives `visible` from its own hover area (the
|
||||
// quality-badge convention), so the tip tracks hover exactly.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "../../theme"
|
||||
|
||||
ToolTip {
|
||||
id: tip
|
||||
|
||||
delay: 0
|
||||
timeout: -1
|
||||
padding: 0
|
||||
implicitHeight: 24
|
||||
x: parent ? Math.round((parent.width - width) / 2) : 0
|
||||
y: -height - 6
|
||||
|
||||
contentItem: Text {
|
||||
text: tip.text
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 9
|
||||
rightPadding: 9
|
||||
}
|
||||
background: Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
// The theme instance lives here because a ToolTip's default property
|
||||
// is its content; ids are component-scoped, so contentItem sees it.
|
||||
QbzTheme { id: theme }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,14 @@
|
||||
// tab has rows OR a live search; the Folders flat-only block hides in tree
|
||||
// mode while the Flat/Tree toggle stays; Artists shows the search only when
|
||||
// there are artists.
|
||||
//
|
||||
// PRESENTATION DIVERGENCE (owner request — the row was too crowded):
|
||||
// * search -> the EXPANDABLE form (Slint already uses ExpandableSearch
|
||||
// here; the port used to mount an always-open 200px box);
|
||||
// * sort -> LocalIconSelect "arrow-up-down" (Slint: text QbzSelect)
|
||||
// * grouping -> LocalIconSelect "layers" (Slint: text QbzSelect)
|
||||
// * filter -> unchanged icon-only button (matches Slint) + a tooltip.
|
||||
// Every option list, key mapping and callback is untouched.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
@@ -38,6 +46,29 @@ Row {
|
||||
QbzSession.tr("Group by letter", QbzSession.trRev),
|
||||
QbzSession.tr("Group by artist", QbzSession.trRev),
|
||||
]
|
||||
// Tooltip leads for the icon-only controls.
|
||||
readonly property string sortTip: QbzSession.tr("Sort", QbzSession.trRev)
|
||||
readonly property string groupTip: QbzSession.tr("Grouping", QbzSession.trRev)
|
||||
|
||||
readonly property var tracksSortIds: ["default", "artist-asc", "artist-desc",
|
||||
"title-asc", "title-desc", "year-desc", "year-asc", "added-desc"]
|
||||
readonly property var tracksSortLabels: [
|
||||
QbzSession.tr("Default", QbzSession.trRev),
|
||||
QbzSession.tr("Artist A-Z", QbzSession.trRev),
|
||||
QbzSession.tr("Artist Z-A", QbzSession.trRev),
|
||||
QbzSession.tr("Title A-Z", QbzSession.trRev),
|
||||
QbzSession.tr("Title Z-A", QbzSession.trRev),
|
||||
QbzSession.tr("Year (newest)", QbzSession.trRev),
|
||||
QbzSession.tr("Year (oldest)", QbzSession.trRev),
|
||||
QbzSession.tr("Date added", QbzSession.trRev),
|
||||
]
|
||||
readonly property var tracksGroupIds: ["off", "album", "artist", "name"]
|
||||
readonly property var tracksGroupLabels: [
|
||||
QbzSession.tr("No grouping", QbzSession.trRev),
|
||||
QbzSession.tr("By album", QbzSession.trRev),
|
||||
QbzSession.tr("By artist", QbzSession.trRev),
|
||||
QbzSession.tr("By name", QbzSession.trRev),
|
||||
]
|
||||
|
||||
height: 30
|
||||
spacing: 8
|
||||
@@ -50,29 +81,33 @@ Row {
|
||||
height: parent.height
|
||||
|
||||
LocalSearchBox {
|
||||
elevated: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
placeholder: QbzSession.tr("Search", QbzSession.trRev)
|
||||
onEdited: function (v) { root.view.albumsSearch = v }
|
||||
}
|
||||
QbzSelect {
|
||||
menuWidth: 180
|
||||
height: 30
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "arrow-up-down"
|
||||
label: root.sortTip
|
||||
menuWidth: 190
|
||||
options: root.sortLabels
|
||||
currentIndex: Math.max(0, root.sortIds.indexOf(root.view.albumsSort))
|
||||
onSelected: function (i) { root.view.albumsSort = root.sortIds[i] }
|
||||
}
|
||||
QbzSelect {
|
||||
menuWidth: 160
|
||||
height: 30
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "layers"
|
||||
label: root.groupTip
|
||||
menuWidth: 180
|
||||
options: root.groupLabels
|
||||
marked: root.view.albumsGroup !== "off"
|
||||
currentIndex: Math.max(0, root.groupIds.indexOf(root.view.albumsGroup))
|
||||
onSelected: function (i) { root.view.albumsGroup = root.groupIds[i] }
|
||||
}
|
||||
// Album identity ("Albums by") — what one album IS. A QUERY change
|
||||
// (the grouping happens in SQL), so it reloads the set; persisted,
|
||||
// shared with the Slint app.
|
||||
// shared with the Slint app. Kept as a LABELLED select: it changes
|
||||
// what the tab shows, not how it is ordered, and no icon says that.
|
||||
QbzSelect {
|
||||
menuWidth: 190
|
||||
height: 30
|
||||
@@ -112,6 +147,13 @@ Row {
|
||||
onClicked: root.view.filterOpen = !root.view.filterOpen
|
||||
}
|
||||
}
|
||||
LocalTip {
|
||||
visible: filtArea.containsMouse
|
||||
text: root.view.filterCount > 0
|
||||
? QbzSession.tr("Filters", QbzSession.trRev)
|
||||
+ " (" + root.view.filterCount + ")"
|
||||
: QbzSession.tr("Quality, format and source filters", QbzSession.trRev)
|
||||
}
|
||||
Rectangle {
|
||||
visible: root.view.filterCount > 0
|
||||
x: parent.width - width + 3
|
||||
@@ -156,7 +198,7 @@ Row {
|
||||
onClicked: root.view.toggleTracksMultiSelect()
|
||||
}
|
||||
LocalSearchBox {
|
||||
elevated: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
placeholder: QbzSession.tr("Search", QbzSession.trRev)
|
||||
onEdited: function (v) {
|
||||
root.view.tracksSearch = v
|
||||
@@ -169,44 +211,24 @@ Row {
|
||||
onTriggered: QbzLocal.tracksSearch(root.view.tracksSearch)
|
||||
}
|
||||
}
|
||||
QbzSelect {
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "arrow-up-down"
|
||||
label: root.sortTip
|
||||
menuWidth: 190
|
||||
options: root.tracksSortLabels
|
||||
currentIndex: Math.max(0, root.tracksSortIds.indexOf(QbzLocal.localTracksSort))
|
||||
onSelected: function (i) { QbzLocal.tracksSetSort(root.tracksSortIds[i]) }
|
||||
}
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "layers"
|
||||
label: root.groupTip
|
||||
menuWidth: 180
|
||||
height: 30
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
options: [
|
||||
QbzSession.tr("Default", QbzSession.trRev),
|
||||
QbzSession.tr("Artist A-Z", QbzSession.trRev),
|
||||
QbzSession.tr("Artist Z-A", QbzSession.trRev),
|
||||
QbzSession.tr("Title A-Z", QbzSession.trRev),
|
||||
QbzSession.tr("Title Z-A", QbzSession.trRev),
|
||||
QbzSession.tr("Year (newest)", QbzSession.trRev),
|
||||
QbzSession.tr("Year (oldest)", QbzSession.trRev),
|
||||
QbzSession.tr("Date added", QbzSession.trRev),
|
||||
]
|
||||
currentIndex: Math.max(0, ["default", "artist-asc", "artist-desc",
|
||||
"title-asc", "title-desc", "year-desc", "year-asc",
|
||||
"added-desc"].indexOf(QbzLocal.localTracksSort))
|
||||
onSelected: function (i) {
|
||||
QbzLocal.tracksSetSort(["default", "artist-asc", "artist-desc",
|
||||
"title-asc", "title-desc", "year-desc", "year-asc",
|
||||
"added-desc"][i])
|
||||
}
|
||||
}
|
||||
QbzSelect {
|
||||
menuWidth: 160
|
||||
height: 30
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
options: [
|
||||
QbzSession.tr("No grouping", QbzSession.trRev),
|
||||
QbzSession.tr("By album", QbzSession.trRev),
|
||||
QbzSession.tr("By artist", QbzSession.trRev),
|
||||
QbzSession.tr("By name", QbzSession.trRev),
|
||||
]
|
||||
currentIndex: Math.max(0, ["off", "album", "artist", "name"]
|
||||
.indexOf(root.view.tracksGroup))
|
||||
onSelected: function (i) {
|
||||
root.view.tracksGroup = ["off", "album", "artist", "name"][i]
|
||||
}
|
||||
options: root.tracksGroupLabels
|
||||
marked: root.view.tracksGroup !== "off"
|
||||
currentIndex: Math.max(0, root.tracksGroupIds.indexOf(root.view.tracksGroup))
|
||||
onSelected: function (i) { root.view.tracksGroup = root.tracksGroupIds[i] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,23 +246,26 @@ Row {
|
||||
spacing: 8
|
||||
height: parent.height
|
||||
LocalSearchBox {
|
||||
elevated: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
placeholder: QbzSession.tr("Search", QbzSession.trRev)
|
||||
onEdited: function (v) { root.view.foldersSearch = v }
|
||||
}
|
||||
QbzSelect {
|
||||
menuWidth: 150
|
||||
height: 30
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "arrow-up-down"
|
||||
label: root.sortTip
|
||||
menuWidth: 170
|
||||
options: root.sortLabels
|
||||
currentIndex: Math.max(0, root.sortIds.indexOf(root.view.foldersSort))
|
||||
onSelected: function (i) { root.view.foldersSort = root.sortIds[i] }
|
||||
}
|
||||
QbzSelect {
|
||||
menuWidth: 150
|
||||
height: 30
|
||||
LocalIconSelect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "layers"
|
||||
label: root.groupTip
|
||||
menuWidth: 180
|
||||
options: root.groupLabels
|
||||
marked: root.view.foldersGroup !== "off"
|
||||
currentIndex: Math.max(0, root.groupIds.indexOf(root.view.foldersGroup))
|
||||
onSelected: function (i) { root.view.foldersGroup = root.groupIds[i] }
|
||||
}
|
||||
@@ -269,7 +294,7 @@ Row {
|
||||
LocalSearchBox {
|
||||
visible: root.view && root.view.activeTab === "artists"
|
||||
&& root.view.artists.length > 0
|
||||
elevated: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
placeholder: QbzSession.tr("Search artists", QbzSession.trRev)
|
||||
onEdited: function (v) { root.view.artistsSearch = v }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
// Local track row — the SHARED rows/TrackRow.qml with the local arms set,
|
||||
// plus the two things the shared row has no arm for:
|
||||
// plus the three things the shared row has no arm for:
|
||||
//
|
||||
// * the always-visible SOURCE glyph the Slint renders from
|
||||
// `show-source: true` (LocalLibraryView.slint:1489) — drawn OUTSIDE the
|
||||
// shared row so TrackRow keeps one column layout for every surface;
|
||||
// * the multi-select checkbox (Slint TrackRow's `multi-select-mode`
|
||||
// swaps the number cell for it) — overlaid on the number cell here for
|
||||
// the same reason: no fork of the shared row, no shared-file edit.
|
||||
// the same reason: no fork of the shared row, no shared-file edit;
|
||||
// * the LOCAL context menu.
|
||||
//
|
||||
// THE MENU (this is the `force-local-menu` arm of the unified Slint menu —
|
||||
// TrackRow.slint:68 -> TrackMenuState -> AppShell.slint:1009-1021 ->
|
||||
// TrackContextMenu.slint). For a local / Plex / offline row on a library
|
||||
// surface Slint resolves to: qobuz-actions OFF, favorite OFF, Track info OFF
|
||||
// (show-track-info rides the Qobuz block), mixtape ON, add-to-playlist ON,
|
||||
// Go to album / Go to artist ON (`local-goto-actions`). So the full local set
|
||||
// is: Play now, Play next, Play later, Add to queue, Add to mixtape, Add to
|
||||
// playlist, Go to album, Go to artist.
|
||||
//
|
||||
// Add to mixtape and Add to playlist are OMITTED here, not disabled: this
|
||||
// port has no Mixtape/Collection store and no playlist picker (the Rust side
|
||||
// says so itself — `local_album_actions.rs` keeps them as LOGGED SEAMS), and
|
||||
// a menu row that silently does nothing is worse than an absent one. Every
|
||||
// row that IS shown is wired.
|
||||
//
|
||||
// The shared row's own ⋯ is turned OFF (`showMenu: false`) because its
|
||||
// entries are the Qobuz set: its Go-to-album/artist call QbzAlbum.openAlbum /
|
||||
// QbzArtist.openArtist with CATALOG ids, and a local row carries a local
|
||||
// group key + a bare artist NAME. The button is re-drawn here, in the exact
|
||||
// same gutter (the shared row is narrowed by precisely the 32px cell + 14px
|
||||
// gap it no longer draws), and both it and a right-click open this menu.
|
||||
//
|
||||
// Artwork follows the Slint gate (AppearanceState.local-library-track-
|
||||
// artwork, default OFF) — decoding a cover per row is exactly what froze
|
||||
@@ -14,6 +37,7 @@
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../controls"
|
||||
import "../../rows"
|
||||
import "../../theme"
|
||||
|
||||
@@ -35,17 +59,24 @@ Item {
|
||||
|
||||
height: 50
|
||||
|
||||
// Local rows have a local album GROUP KEY and a bare artist NAME; both
|
||||
// routes are name/key routes, so each entry is gated on its own datum.
|
||||
readonly property bool canGoAlbum: (item.albumId || "") !== ""
|
||||
readonly property bool canGoArtist: (item.artist || "") !== ""
|
||||
|
||||
TrackRow {
|
||||
id: sharedRow
|
||||
width: root.width - 26
|
||||
// 26px source-glyph gutter + the 46px (32 cell + 14 gap) the shared
|
||||
// row would have spent on its own ⋯ — so every other column lands
|
||||
// exactly where it did before the menu moved out.
|
||||
width: root.width - 26 - 46
|
||||
item: root.item
|
||||
number: root.number
|
||||
showArtwork: root.showArtwork
|
||||
showAlbum: root.showAlbum
|
||||
showFavorite: false
|
||||
showDownload: false
|
||||
menuShowGoTo: false
|
||||
menuShowFavorite: false
|
||||
showMenu: false
|
||||
// Local rows are NOT drag sources: item.id is a local DB row id and
|
||||
// the sidebar drop handler forwards it as a Qobuz catalog id.
|
||||
draggable: false
|
||||
@@ -70,6 +101,32 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// ⋯ menu button — the shared row's own gutter, re-drawn with the local
|
||||
// action set behind it.
|
||||
Rectangle {
|
||||
id: moreBtn
|
||||
x: root.width - 70
|
||||
width: 32
|
||||
height: 32
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: moreArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
QbzIcon {
|
||||
name: "ellipsis"
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: moreArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: moreArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
||||
}
|
||||
}
|
||||
|
||||
// Source glyph (local / offline cache / Plex).
|
||||
QbzIcon {
|
||||
visible: root.item.source === "local" || root.item.source === "plex"
|
||||
@@ -81,4 +138,45 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: root.item.source === "plex" ? "accent" : "muted"
|
||||
}
|
||||
|
||||
// Right-click anywhere on the row opens the SAME menu at the pointer
|
||||
// (TrackRow.slint's `open-track-menu(..., at-pointer: true)`). Declared
|
||||
// last so it sits on top, and RIGHT-only so every left click still falls
|
||||
// through to the row body / checkbox / ⋯ underneath.
|
||||
MouseArea {
|
||||
id: rcArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: function (mouse) { rowMenu.openAtCursor(rcArea, mouse.x, mouse.y) }
|
||||
}
|
||||
|
||||
CardMenu {
|
||||
id: rowMenu
|
||||
menuWidth: 220
|
||||
entries: {
|
||||
var m = [
|
||||
{ "label": QbzSession.tr("Play now", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
if (root.canGoAlbum) m.push({ "label": QbzSession.tr("Go to album", QbzSession.trRev), "icon": "disc-3", "action": "go-album" })
|
||||
if (root.canGoArtist) m.push({ "label": QbzSession.tr("Go to artist", QbzSession.trRev), "icon": "user", "action": "go-artist" })
|
||||
return m
|
||||
}
|
||||
onPicked: function (a) {
|
||||
if (a === "play") root.playRequested()
|
||||
else if (a === "go-album") {
|
||||
QbzLocal.openAlbum(root.item.albumId)
|
||||
QbzShell.navigateTo("localalbum")
|
||||
} else if (a === "go-artist") {
|
||||
// Local/Plex artists have no catalog id — a NAME route into
|
||||
// the Local Library Artists tab (local_album_actions.rs).
|
||||
QbzLocal.openArtistByName(root.item.artist)
|
||||
QbzShell.navigateTo("local")
|
||||
} else {
|
||||
root.enqueueRequested(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,10 +53,14 @@ Item {
|
||||
iconSize: 15
|
||||
onClicked: QbzLocal.ephemeralOpen()
|
||||
}
|
||||
// Filler: the search collapses to a 30px magnifier, so the slot
|
||||
// stays right-aligned and the field opens LEFT over this gap.
|
||||
Item {
|
||||
width: Math.max(0, parent.width - 3 * 30 - 30 - 4 * 4)
|
||||
height: 1
|
||||
}
|
||||
LocalSearchBox {
|
||||
boxWidth: parent.width - 3 * 34
|
||||
leftPad: 8
|
||||
rightPad: 6
|
||||
placeholder: QbzSession.tr("Search folders", QbzSession.trRev)
|
||||
onEdited: function (v) {
|
||||
root.view.treeSearch = v
|
||||
|
||||
@@ -6,8 +6,17 @@
|
||||
// music glyph and the segment name. The selection cue is the 3px accent bar
|
||||
// on the left edge — ADR-008: never a pill.
|
||||
|
||||
// Right-click opens a folder queue menu. DIVERGENCE: LocalLibraryView.slint
|
||||
// gives the tree rail no per-row menu (only the detail pane's Play button and
|
||||
// the bulk bar), but the owner asked for working context menus on the local
|
||||
// rows and every entry here rides a seam that already exists
|
||||
// (QbzLocal.playFolder / enqueue("folder", path, mode) — local_playback.rs
|
||||
// `list_folder_tracks_recursive`). TRACK leaves get no menu: their `path` is
|
||||
// a file path, not a track id, so no track arm accepts it.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../../controls"
|
||||
import "../../theme"
|
||||
|
||||
Rectangle {
|
||||
@@ -32,8 +41,30 @@ Rectangle {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: root.node.isFolder ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.node.isFolder) root.activated()
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (root.node.isFolder) folderMenu.openAtCursor(rowArea, mouse.x, mouse.y)
|
||||
return
|
||||
}
|
||||
if (root.node.isFolder) root.activated()
|
||||
}
|
||||
}
|
||||
|
||||
CardMenu {
|
||||
id: folderMenu
|
||||
menuWidth: 200
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
||||
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
||||
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
||||
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
||||
]
|
||||
onPicked: function (a) {
|
||||
if (a === "play") QbzLocal.playFolder(root.node.path)
|
||||
else QbzLocal.enqueue("folder", root.node.path, a)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
Reference in new issue
Block a user