Files
qbz/crates/qbz-qt/qml/controls/PlaylistPickerModal.qml
T
vicrodh ed7fa588ea fix(qt): elevated controls lift on hover instead of darkening
Buttons that rest on surfaceElevated used surfaceHover for their hover
fill: a 6% white wash meant for rows that rest transparent. Over an
elevated button it lands DARKER than the rest fill — on a modal's dark
ground the Cancel/Save/footer buttons went near-black under the pointer.

QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived
so every palette has it), its ambient half-alpha twin, and the
elevatedFill / elevatedHoverFill pair an ambient-aware control paints.
Every site with the old pairing (65 across modals, toolbars, selects,
circle actions, chips and cards) now hovers to the lift.

cc
2026-09-14 00:49:54 -06:00

904 lines
43 KiB
QML

// "Add to playlist" picker — QML port of
// crates/qbz-ui/ui/primitives/PlaylistPickerModal.slint (+ its
// PlaylistDuplicateConfirmModal sibling, folded in here because the two
// share one document and one carried track set).
//
// A focus-opened dropdown lists the user's playlists; clicking a row only
// SELECTS it (the selection becomes the input's placeholder); the footer
// button fires the actual add. "+ Create new playlist" is always the first
// dropdown row and reveals an inline name field confirmed by the same
// footer button ("Create & Add").
//
// Mounted ONCE in AppShell next to AddToMixtapeModal: every host that opens
// it (the queue footer and row menu today) can be unmounted by the time the
// user picks a target, so the modal must outlive them.
//
// Data: QbzPlaylistPicker.pickerJson (playlist_picker_qt.rs PickerDoc). The
// name filter runs in RUST (the QbzMyQbzAdd.search convention), so the row
// list arrives already filtered; the 8-row window and the overflow line are
// applied here against `total`/`playlists.length`.
//
// LOCAL playlists are listed alongside the Qobuz ones (a hard-drive glyph
// leads the row, `isLocal` in the document) and are the ONLY rows while
// offline — a Qobuz playlist cannot be written to without a session, so
// offering one would be a dead target (D3/D11). The carried payload is
// likewise either Qobuz catalog ids or LocalLibrary refs; `localMode` only
// changes the counter's wording ("Adding {} local track(s)"), never the row
// list — a local selection can legitimately target a Qobuz playlist, which
// Rust routes through the library.db sidecar.
import QtQuick
import QtQuick.Controls
import com.blitzfc.qbz
import "../theme"
Item {
id: root
// An unopened overlay is an invisible, non-interactive Item and costs
// nothing (the AppShell mount comment states the contract).
visible: doc.open === true || dup.open === true
readonly property var doc: JSON.parse(QbzPlaylistPicker.pickerJson)
readonly property var rows: doc.playlists || []
readonly property var dup: doc.dup || ({})
readonly property int trackCount: doc.trackCount || 0
// Session-local pinned target (last successful add); null when unset,
// deleted, unwritable, or filtered out by the current query.
readonly property var lastUsed: doc.lastUsed || null
// Containment strip: writable playlists already holding carried tracks,
// honest per alreadyState ("loading"|"complete"|"updating"|"unavailable").
readonly property var alreadyIn: doc.alreadyIn || []
readonly property string alreadyState: doc.alreadyState || ""
// The column renders only when it can back a claim: complete with
// matches, or updating (where partial matches show under an explicit
// caveat). It is a SIDE column, not an in-flow strip, so the dropdown
// can never cover it.
readonly property bool alreadyShowing:
root.alreadyState === "updating"
|| (root.alreadyState === "complete" && root.alreadyIn.length > 0)
// The carried payload is LocalLibrary refs (counter wording only).
readonly property bool localMode: doc.localMode === true
readonly property bool creatingOpen: doc.creatingOpen === true
readonly property bool creating: doc.creating === true
// Rows past the 8-row window collapse into the "{} more playlist(s)..."
// line, exactly as the .slint's filter-rank window does.
readonly property int windowSize: 8
readonly property int shown: Math.min(rows.length, windowSize)
readonly property int overflow: Math.max(0, rows.length - windowSize)
// --- Local selection state (the .slint keeps these on the `if open:`
// subtree so a remount resets them; here they are reset when `open`
// flips, which is the same lifetime without a conditional Loader). ---
property bool dropdownOpen: false
property string selectedId: ""
property string selectedLabel: ""
property string filterText: ""
property string createName: ""
onVisibleChanged: if (!visible) resetLocals()
function resetLocals() {
dropdownOpen = false
selectedId = ""
selectedLabel = ""
filterText = ""
createName = ""
}
function selectRow(row) {
selectedId = row.id
selectedLabel = (row.tracksLine || "") === ""
? row.name : row.name + " (" + row.tracksLine + ")"
// The reference clears the query on selection (Tauri §2.4).
filterText = ""
QbzPlaylistPicker.search("")
QbzPlaylistPicker.showCreate(false)
dropdownOpen = false
}
function confirm() {
if (root.creating)
return
if (root.creatingOpen) {
if (createName.trim() !== "")
QbzPlaylistPicker.createAndAdd(createName)
} else if (selectedId !== "") {
QbzPlaylistPicker.pick(selectedId)
}
}
QbzTheme { id: theme }
// ======================= The picker ==================================
Item {
anchors.fill: parent
visible: root.doc.open === true && root.dup.open !== true
// Scrim — click outside dismisses. The wheel is eaten too: these
// overlays are plain Items, not Popups, so no modal grab stops the
// page underneath from scrolling (the DiscoverConfigModal contract).
Rectangle {
anchors.fill: parent
color: "#bf000000"
MouseArea {
anchors.fill: parent
onClicked: QbzPlaylistPicker.close()
onWheel: function (wheel) { wheel.accepted = true }
}
}
Rectangle {
id: card
anchors.centerIn: parent
width: Math.min(parent.width - 80, 735)
// Tauri: min-height 400 / max-height 90vh. The body stretches so
// the footer pins to the bottom.
height: Math.min(Math.max(400, panel.implicitHeight), parent.height * 0.9)
radius: theme.radiusLg
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
// NO clip: the dropdown legitimately overflows the card bottom.
// The "Already in" side column's slot. Zero when absent, so the
// main content takes the whole card.
readonly property real rightPanelW: root.alreadyShowing ? 221 : 0
// Swallow clicks so they never reach the scrim; doubles as
// "a click inside the card closes the list" (Tauri parity).
// The wheel stops here too — same reason as the scrim.
MouseArea {
anchors.fill: parent
onClicked: root.dropdownOpen = false
onWheel: function (wheel) { wheel.accepted = true }
}
Column {
id: panel
width: parent.width
// --- Header -------------------------------------------
Item {
width: parent.width
height: 68
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.right: closeX.left
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Add to Playlist", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightSemibold
elide: Text.ElideRight
}
Item {
id: closeX
anchors.right: parent.right
anchors.rightMargin: 24
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 17
height: 17
tintName: closeArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzPlaylistPicker.close()
}
}
}
Rectangle { width: parent.width; height: 1; color: theme.borderSubtle }
// --- Body (the MAIN column; the "Already in" side column
// takes the right slot when present) -------------------
Column {
id: body
width: parent.width - card.rightPanelW
padding: 24
spacing: 16
// Loading — the playlists are still on the wire.
Item {
width: parent.width - 48
height: visible ? 30 : 0
visible: root.doc.loading === true
QbzSpinner {
anchors.horizontalCenter: parent.horizontalCenter
size: 30
}
}
Column {
width: parent.width - 48
spacing: 8
Item {
width: parent.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Choose playlist", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: theme.fontLegal
font.weight: theme.weightMedium
}
// Two msgids, not one with an inserted word: the
// .slint counter switches the whole string on
// `local-mode` so every locale can order it its
// own way.
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: (root.localMode
? QbzSession.tr("Adding {} local track(s)", QbzSession.trRev)
: QbzSession.tr("Adding {} track(s)", QbzSession.trRev))
.replace("{}", root.trackCount)
color: theme.textMuted
font.pixelSize: theme.fontLink
}
}
// The search / selection input (43px, the Tauri metric).
Rectangle {
id: inputBox
width: parent.width
height: 43
radius: theme.radiusSm
color: theme.surfaceCard
border.width: 1
border.color: searchInput.activeFocus ? theme.accent : theme.borderSubtle
TextInput {
QbzTextEditMenu { }
id: searchInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 36 // room for the clear chip
color: theme.textPrimary
font.pixelSize: theme.fontLink
verticalAlignment: Text.AlignVCenter
clip: true
selectByMouse: true
enabled: root.doc.loading !== true
text: root.filterText
onTextEdited: {
root.filterText = text
root.dropdownOpen = true // typing re-opens
QbzPlaylistPicker.search(text)
}
onAccepted: root.confirm()
// Tauri: the list opens on focus.
onActiveFocusChanged: if (activeFocus) root.dropdownOpen = true
// Re-seed while not being edited (the
// AddToMixtapeModal Binding shape).
Binding {
target: searchInput
property: "text"
value: root.filterText
when: !searchInput.activeFocus
}
}
// Placeholder, doubling as the SELECTION label —
// the filter is cleared on select, so the field is
// empty exactly when the label should show.
Text {
visible: searchInput.text === ""
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 36
verticalAlignment: Text.AlignVCenter
text: root.creatingOpen
? QbzSession.tr("+ Create new playlist", QbzSession.trRev)
: (root.selectedLabel !== ""
? root.selectedLabel
: QbzSession.tr("Search playlists...", QbzSession.trRev))
color: theme.textMuted
font.pixelSize: theme.fontLink
elide: Text.ElideRight
}
// Clear-selection chip — only with a selection (or
// the inline-create state) and the list closed.
Rectangle {
visible: (root.selectedId !== "" || root.creatingOpen)
&& !root.dropdownOpen
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: 20
height: 20
radius: 10
color: chipArea.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 12
height: 12
tintName: chipArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: chipArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedId = ""
root.selectedLabel = ""
root.createName = ""
QbzPlaylistPicker.showCreate(false)
}
}
}
}
}
// Inline create — a second form group BELOW the picker
// (Tauri §1.4.6); the footer button confirms it.
Column {
width: parent.width - 48
spacing: 8
visible: root.creatingOpen
Text {
text: QbzSession.tr("Playlist Name", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: theme.fontLegal
font.weight: theme.weightMedium
}
Rectangle {
width: parent.width
height: 43
radius: theme.radiusSm
color: theme.surfaceCard
border.width: 1
border.color: nameInput.activeFocus ? theme.accent : theme.borderSubtle
TextInput {
QbzTextEditMenu { }
id: nameInput
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
color: theme.textPrimary
font.pixelSize: theme.fontLink
verticalAlignment: Text.AlignVCenter
clip: true
selectByMouse: true
enabled: !root.creating
text: root.createName
onTextEdited: root.createName = text
onAccepted: root.confirm()
Binding {
target: nameInput
property: "text"
value: root.createName
when: !nameInput.activeFocus
}
}
Text {
visible: nameInput.text === ""
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
verticalAlignment: Text.AlignVCenter
text: QbzSession.tr("My Playlist", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLink
elide: Text.ElideRight
}
}
}
}
}
// --- "Already in" side column -----------------------------
// The containment answer as a right-hand column instead of an
// in-flow strip: the dropdown overlays the MAIN column only, so
// this list stays readable while the playlist list is open.
// Shown if and only if the index can back a claim (complete with
// matches, or updating under the explicit caveat) — an empty
// answer from an incomplete index renders nothing.
Item {
visible: root.alreadyShowing
anchors.top: parent.top
anchors.topMargin: 69
anchors.bottom: footerDivider.top
anchors.right: parent.right
width: card.rightPanelW
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 1
color: theme.borderSubtle
}
Column {
anchors.fill: parent
anchors.leftMargin: 17
anchors.rightMargin: 16
anchors.topMargin: 16
spacing: 8
Text {
text: QbzSession.tr("Already in", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: theme.fontLegal
font.weight: theme.weightMedium
}
Text {
visible: root.alreadyState === "updating"
width: parent.width
text: QbzSession.tr("Playlist index updating...", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.italic: true
wrapMode: Text.WordWrap
}
ListView {
width: parent.width
height: Math.max(0, parent.height - y - 12)
clip: true
spacing: 2
boundsBehavior: Flickable.StopAtBounds
model: root.alreadyIn
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 34
radius: theme.radiusSm
color: alreadyArea.containsMouse
? theme.surfaceHover : "transparent"
QbzIcon {
id: alreadyGlyph
visible: modelData.isLocal === true
anchors.left: parent.left
anchors.leftMargin: 6
anchors.verticalCenter: parent.verticalCenter
name: "hard-drive"
width: 12
height: 12
tintName: "muted"
}
Text {
anchors.left: alreadyGlyph.visible
? alreadyGlyph.right : parent.left
anchors.leftMargin: 6
anchors.right: alreadyCount.visible
? alreadyCount.left : parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: theme.textPrimary
font.pixelSize: theme.fontLegal
elide: Text.ElideRight
}
// The multi-track answer: "2 of 5". A full or
// single-track containment says it by presence.
Text {
id: alreadyCount
visible: root.trackCount > 1
&& modelData.contained < root.trackCount
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("{} of {}", QbzSession.trRev)
.replace("{}", modelData.contained)
.replace("{}", root.trackCount)
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
MouseArea {
id: alreadyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// Selecting it is the useful action: "add the
// missing 3 of 5" is one click plus the
// dup-confirm's only-new answer.
onClicked: root.selectRow(modelData)
}
}
}
}
}
// --- Footer, pinned to the card bottom --------------------
// Keep a real footer band around the modal-standard 36px
// controls. The old divider was anchored directly to the Row's
// top edge, so both buttons physically touched the separator.
Item {
id: footerBar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 69
Row {
id: footerRow
anchors.right: parent.right
anchors.rightMargin: 24
anchors.verticalCenter: parent.verticalCenter
spacing: 10
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Cancel", QbzSession.trRev)
btnHeight: 36
minWidth: 0
onClicked: QbzPlaylistPicker.close()
}
// The single accent confirm (ADR-008). Disabled until
// valid; QbzPrimaryButton supplies the active theme's
// primary fill and matching on-accent label colour.
QbzPrimaryButton {
anchors.verticalCenter: parent.verticalCenter
btnHeight: 36
labelSize: 15
label: root.creating
? QbzSession.tr("Saving...", QbzSession.trRev)
: (root.creatingOpen
? QbzSession.tr("Create & Add", QbzSession.trRev)
: QbzSession.tr("Add", QbzSession.trRev))
btnEnabled: !root.creating
&& (root.creatingOpen
? root.createName.trim() !== ""
: root.selectedId !== "")
onClicked: root.confirm()
}
}
}
Rectangle {
id: footerDivider
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: footerBar.top
height: 1
color: theme.borderSubtle
}
// --- Dropdown overlay -------------------------------------
// Declared LAST inside the card so it paints above the body and
// the footer (declaration order IS z-order in QML), and it
// legitimately overflows the unclipped card bottom.
Rectangle {
id: dropdown
visible: root.dropdownOpen && root.doc.loading !== true
x: 24
// Anchored below the input by static arithmetic over fixed
// metrics — never an absolute-position binding: header 68 +
// 1 divider + 24 body pad + 20 label row + 8 spacing +
// 43 input + 4 gap.
y: 68 + 1 + 24 + 20 + 8 + 43 + 4
// MAIN column only — the "Already in" side column stays
// readable while the list is open (that is its whole point).
width: parent.width - card.rightPanelW - 48
// create row 41 + 1 divider, pinned last-used 41 + 1, 41 per
// shown row, 41 no-results, 1 + 34 overflow. Tauri max-height
// 320 + border.
height: Math.min(2 + 42 + (root.lastUsed !== null ? 42 : 0)
+ root.shown * 41
+ ((root.rows.length === 0 && root.filterText !== "") ? 41 : 0)
+ (root.overflow > 0 ? 35 : 0), 322)
radius: theme.radiusSm
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
clip: true
// Keep clicks on the list chrome from reaching the card
// swallower, which would close the list under the pointer.
MouseArea { anchors.fill: parent }
Flickable {
anchors.fill: parent
anchors.margins: 1
contentWidth: width
contentHeight: dropInner.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: dropInner
width: parent.width
// "+ Create new playlist" — ALWAYS first, never
// filtered out (Tauri parity).
Rectangle {
width: parent.width
height: 41
color: createArea.containsMouse ? theme.surfaceHover : "transparent"
Text {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
verticalAlignment: Text.AlignVCenter
text: QbzSession.tr("+ Create new playlist", QbzSession.trRev)
color: theme.accent
font.pixelSize: theme.fontLink
font.weight: theme.weightMedium
elide: Text.ElideRight
}
MouseArea {
id: createArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedId = ""
root.selectedLabel = ""
root.filterText = ""
QbzPlaylistPicker.search("")
QbzPlaylistPicker.showCreate(true)
root.dropdownOpen = false
}
}
}
Rectangle { width: parent.width; height: 1; color: theme.borderSubtle }
// Session's last successful target, pinned above the
// results. Rust already validated it (still listed,
// still writable) and de-duplicated it from `rows`;
// under a search it only survives a matching query.
Rectangle {
visible: root.lastUsed !== null
width: parent.width
height: visible ? 41 : 0
color: lastUsedArea.containsMouse ? theme.surfaceHover : "transparent"
QbzIcon {
id: lastUsedGlyph
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
name: "clock"
width: 14
height: 14
tintName: "muted"
}
Text {
anchors.left: lastUsedGlyph.right
anchors.leftMargin: 8
anchors.right: lastUsedCaption.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: root.lastUsed ? root.lastUsed.name : ""
color: theme.textPrimary
font.pixelSize: theme.fontLink
elide: Text.ElideRight
}
Text {
id: lastUsedCaption
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Last playlist", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.italic: true
}
MouseArea {
id: lastUsedArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.selectRow(root.lastUsed)
}
}
Rectangle {
visible: root.lastUsed !== null
width: parent.width
height: visible ? 1 : 0
color: theme.borderSubtle
}
Repeater {
model: root.rows.slice(0, root.windowSize)
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 41
color: rowArea.containsMouse ? theme.surfaceHover : "transparent"
// LOCAL playlists (library.db) keep a small
// hard-drive glyph LEADING the name, muted —
// PlaylistPickerModal.slint:62-69. It is what
// tells a local target apart from a Qobuz one
// in a list that mixes both.
QbzIcon {
id: localGlyph
visible: modelData.isLocal === true
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
name: "hard-drive"
width: 14
height: 14
tintName: "muted"
}
Text {
anchors.left: localGlyph.visible ? localGlyph.right : parent.left
anchors.leftMargin: localGlyph.visible ? 8 : 12
anchors.right: countText.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: theme.textPrimary
font.pixelSize: theme.fontLink
elide: Text.ElideRight
}
Text {
id: countText
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.tracksLine || ""
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.selectRow(modelData)
}
}
}
Text {
visible: root.rows.length === 0 && root.filterText !== ""
width: parent.width
height: visible ? 41 : 0
text: QbzSession.tr("No playlists found", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontLegal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
// Overflow line — the rows past the 8-row window.
Rectangle {
visible: root.overflow > 0
width: parent.width
height: visible ? 1 : 0
color: theme.borderSubtle
}
Text {
visible: root.overflow > 0
width: parent.width
height: visible ? 34 : 0
text: QbzSession.tr("{} more playlist(s)...", QbzSession.trRev)
.replace("{}", root.overflow)
color: theme.textMuted
font.pixelSize: theme.fontLegal
font.italic: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
// ============= Duplicate-confirm sub-modal ===========================
// Opens INSTEAD of adding when the chosen playlist already holds some of
// the carried tracks. It replaces the picker rather than stacking on it
// (the picker's own state is already parked in Rust).
Item {
anchors.fill: parent
visible: root.dup.open === true
Rectangle {
anchors.fill: parent
color: "#bf000000"
MouseArea {
anchors.fill: parent
onClicked: QbzPlaylistPicker.cancelDuplicates()
onWheel: function (wheel) { wheel.accepted = true }
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 80, 460)
height: dupPanel.implicitHeight
radius: theme.radiusLg
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
MouseArea {
anchors.fill: parent
onWheel: function (wheel) { wheel.accepted = true }
}
Column {
id: dupPanel
width: parent.width
padding: 24
spacing: 16
Item {
width: parent.width - 48
height: 28
Text {
anchors.left: parent.left
anchors.right: dupClose.left
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Some of these tracks were already added", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightSemibold
elide: Text.ElideRight
}
Item {
id: dupClose
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 17
height: 17
tintName: dupCloseArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: dupCloseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzPlaylistPicker.cancelDuplicates()
}
}
}
Text {
width: parent.width - 48
text: QbzSession.tr("This playlist already contains {} of the {} tracks you're adding. Add only the new ones, or add everything including duplicates.", QbzSession.trRev)
.replace("{}", root.dup.duplicateCount || 0)
.replace("{}", root.trackCount)
color: theme.textSecondary
font.pixelSize: theme.fontLink
wrapMode: Text.WordWrap
}
Row {
anchors.right: parent.right
anchors.rightMargin: 24
spacing: 12
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: root.dup.busy === true
? QbzSession.tr("Adding...", QbzSession.trRev)
: QbzSession.tr("Add all tracks", QbzSession.trRev)
btnHeight: 34
minWidth: 0
enabled: root.dup.busy !== true
onClicked: QbzPlaylistPicker.confirmDuplicates(true)
}
QbzPrimaryButton {
anchors.verticalCenter: parent.verticalCenter
btnHeight: 34
label: root.dup.busy === true
? QbzSession.tr("Adding...", QbzSession.trRev)
: QbzSession.tr("Add only new tracks", QbzSession.trRev)
btnEnabled: root.dup.busy !== true
onClicked: QbzPlaylistPicker.confirmDuplicates(false)
}
}
}
}
}
}