can be read RIP WIZARD, three steps, each answering something the first version got wrong by not asking. What will happen — which disc, how many tracks, and that the output is FLAC, stated rather than implied. The tracks, editable, because a CD-DA carries no titles at all so "named wrong" is the ordinary case and once files are on disk it is a rename job. And what happens afterwards: a ripped album the library cannot see is a folder full of files, so the destination is compared against the registered folders — by path COMPONENTS, not by string prefix, since /music-backup starts with /music and is not inside it — and the right follow-up is offered, naming the folder it would re-scan. "Do nothing" is an explicit choice rather than the silent default. The follow-up runs only on success. Tracks can be deselected. Everything is ticked on arrival and the user goes from all to fewer; a wizard that starts empty makes the common case the one that costs the most clicks. On the Rust side an absent `selected` means yes, so an older form cannot silently rip nothing. A PROGRESS PANEL behind the ripping chip, which is a door rather than a label: it is the only thing on screen during a rip and the two questions it could not answer are the ones that matter. A soft lock says not to eject — soft because nothing here can hold a tray shut, so the honest move is to say it and keep saying it. Per-track state with a bar on the row that is actually moving. Stopping is offered, deletes nothing, and says so; a cancelled job does not jump to 100 %, which is the one lie a progress bar must not tell. CORRECT DISC DETAILS, for a CD and a SACD image and for neither on a folder, which has no identity to write a correction under. The list shows what distinguishes four pressings of one album rather than four identical titles, and flags a track count that disagrees with the disc. Selecting is not applying: the candidate is fetched in full and its track list shown, because that list is the only thing proving a plausible title is the right pressing. Applying renames the live session without renumbering ids — a re-open would orphan a playing track — and writes a user row. Only MusicBrainz re-resolves the cover; Discogs ids are not Cover Art Archive keys and a wrong cover is worse than the old one. The correction can be dropped, because without an escape a bad apply is permanent and that makes people afraid of the button. THE PANE SPLITS BY DISC. A box set scans as one album — measured on a 34-track set whose album_group_key is the box root for every file while the disc number sits in the data unused — so the pane was a flat list 247 rows long. The key carries the disc, unconditionally, because it round-trips through the play-album lookup. Its own client-side filter over what is already rendered, since finding one track meant scrolling past the other 246. EPHEMERAL TRACKS LOSE WHAT CANNOT OUTLIVE THEM. queue_qt has published isEphemeral on every row for a while and the queue panel respected it; the bar never read it. The heart goes inert, the add menu keeps only its queue actions, and the row menu comes back with those three and nothing else. Absent rather than rendered-and-inert. Also: a spinner on Open that blocks a second click onto a drive already being read, held by a counting RAII guard because opening is a relay and the chip must not blink off in the seam; the now-playing card routes a disc to the pane it came from instead of to an album id no catalogue can open; the file pickers use rfd, the platform's own dialog, which the POC-NOTE said was a five-line change once the dependency existed — it existed, Settings had been calling it from this crate for weeks, and on macOS the four Linux choosers do not exist so Open folder and Open SACD image opened nothing at all, in silence; the empty state stands down for the Open pane, which on a machine with no library drew both fully painted; tooltips on every disc control through one hover() helper, since two call sites had already mismatched their key, which does not fail — the bubble simply never goes away. cc
593 lines
28 KiB
QML
593 lines
28 KiB
QML
// Ephemeral-folder pane (LocalLibraryView.slint:114 EphemeralPane) — shown
|
|
// in the Folders tab while an ad-hoc folder is open. Album-grouped (a folder
|
|
// can hold several albums): each block is a 56px cover + title/artist/meta,
|
|
// with a per-album play button ONLY for multi-album sessions, then the
|
|
// block's tracks.
|
|
//
|
|
// Metadata-bound actions (favorite / playlist / album+artist links) are off:
|
|
// ephemeral tracks have no DB row. The CUE badge marks a single-file rip.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../../controls"
|
|
import "../../rows"
|
|
import "../../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var view: null
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property var eph: view ? view.ephemeral : null
|
|
readonly property var allAlbums: eph && eph.albums ? eph.albums : []
|
|
|
|
/// A CLIENT-side filter over the session that is already on screen.
|
|
///
|
|
/// It exists because a box set is not a browsable list: the owner's Saint
|
|
/// Seiya Eternal CD-Box is 247 tracks, and reaching one of them meant
|
|
/// scrolling past the other 246. Nothing here asks Rust for anything — the
|
|
/// whole session is already in this document, so filtering it is a pass
|
|
/// over an array the pane is holding anyway.
|
|
property string query: ""
|
|
readonly property var albums: {
|
|
var q = root.query.trim().toLowerCase()
|
|
if (q === "")
|
|
return root.allAlbums
|
|
var out = []
|
|
for (var i = 0; i < root.allAlbums.length; i++) {
|
|
var a = root.allAlbums[i]
|
|
var src = a.tracks || []
|
|
// A block whose ALBUM matches keeps ALL of its tracks: searching
|
|
// for a record means wanting the record, not only the rows whose
|
|
// titles happen to repeat its name.
|
|
var albumHit = (a.title || "").toLowerCase().indexOf(q) !== -1
|
|
|| (a.artist || "").toLowerCase().indexOf(q) !== -1
|
|
var hits = []
|
|
if (albumHit) {
|
|
hits = src
|
|
} else {
|
|
for (var j = 0; j < src.length; j++) {
|
|
var t = src[j]
|
|
if ((t.title || "").toLowerCase().indexOf(q) !== -1
|
|
|| (t.artist || "").toLowerCase().indexOf(q) !== -1)
|
|
hits.push(t)
|
|
}
|
|
}
|
|
if (hits.length === 0)
|
|
continue
|
|
out.push({ "groupKey": a.groupKey, "title": a.title, "artist": a.artist,
|
|
"meta": a.meta, "isCue": a.isCue, "artKey": a.artKey,
|
|
"tracks": hits })
|
|
}
|
|
return out
|
|
}
|
|
readonly property int matchCount: {
|
|
var n = 0
|
|
for (var i = 0; i < root.albums.length; i++)
|
|
n += (root.albums[i].tracks || []).length
|
|
return n
|
|
}
|
|
|
|
/// Art for the 224px header cell: the FIRST block's key. For the common
|
|
/// one-album session that is simply "the cover"; for a multi-album folder
|
|
/// it is a representative, and each block keeps drawing its own 56px art
|
|
/// below. Empty when the session has no art at all, which collapses the
|
|
/// cell instead of reserving an empty square.
|
|
readonly property string headerArtKey:
|
|
allAlbums.length > 0 && allAlbums[0].artKey ? allAlbums[0].artKey : ""
|
|
|
|
// Same rule as LocalFolderDetail: the host reports this pane's covers as
|
|
// one window when the document changes, so re-opening the pane on an
|
|
// unchanged document (leaving and returning to the Folders tab) needs its
|
|
// own trigger, or the evicted covers never come back.
|
|
Component.onCompleted: if (view && visible) view.reportEphemeralWindow()
|
|
Component.onDestruction: if (view) view.releaseWindow("ephemeral")
|
|
onVisibleChanged: {
|
|
if (!view) return
|
|
if (visible) view.reportEphemeralWindow()
|
|
else view.releaseWindow("ephemeral")
|
|
}
|
|
Connections {
|
|
target: root.view
|
|
function onArtworkRefresh() {
|
|
if (root.visible) root.view.reportEphemeralWindow()
|
|
else root.view.releaseWindow("ephemeral")
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
id: pane
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.height + 100
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: page
|
|
x: 32
|
|
y: 16
|
|
width: parent.width - 64
|
|
spacing: 14
|
|
|
|
// ---- Header: cover + name / count · duration, and the actions ----
|
|
//
|
|
// Shaped after LocalAlbumHeader, because for the common case this
|
|
// pane IS an album page: one folder, one album, one cover. The art
|
|
// is 224px there (`LocalAlbumHeader.qml:45`) and 224 here, so an
|
|
// opened folder and an indexed album do not present the same record
|
|
// at two different sizes.
|
|
//
|
|
// The cover cell collapses to zero width when the session has no
|
|
// art at all, rather than reserving a 224px hole — a CUE rip with
|
|
// no cover.jpg would otherwise open on a mostly empty header.
|
|
Row {
|
|
width: parent.width
|
|
spacing: root.headerArtKey === "" ? 0 : 20
|
|
|
|
readonly property int actionsW: 3 * 40 + 2 * 12
|
|
|
|
Rectangle {
|
|
id: heroArt
|
|
visible: root.headerArtKey !== ""
|
|
width: visible ? 224 : 0
|
|
height: visible ? 224 : 0
|
|
radius: 8
|
|
color: theme.surfaceHover
|
|
clip: true
|
|
QbzIcon {
|
|
name: "disc-3"
|
|
width: 64
|
|
height: 64
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
RoundedImage {
|
|
id: heroImg
|
|
anchors.fill: parent
|
|
source: root.view ? root.view.artPathOf(root.headerArtKey) : ""
|
|
radius: 8
|
|
}
|
|
QbzSkeleton {
|
|
variant: "art"
|
|
anchors.fill: parent
|
|
blockRadius: 8
|
|
pending: root.view ? root.view.artWanted(root.headerArtKey) : false
|
|
coverReady: heroImg.ready
|
|
phase: root.view ? root.view.skelPhase : false
|
|
settleMs: root.view ? root.view.artSettleMs : 0
|
|
settleHold: root.view ? root.view.artPulse : false
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - heroArt.width
|
|
- (root.headerArtKey === "" ? 0 : 20)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 8
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.eph ? (root.eph.name || "") : ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontSection
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
// The artist, in LocalAlbumHeader's own shape (fontHeading,
|
|
// bold, text-secondary) — for a one-album session this pane
|
|
// IS an album page, and the two must not present the same
|
|
// record differently.
|
|
//
|
|
// NOT clickable, unlike the album header's: that one routes
|
|
// into the Artists tab, and the act on a CD the library has
|
|
// never seen is a route to an empty page. A disc names its
|
|
// artist; it does not promise the library knows them.
|
|
//
|
|
// Hidden rather than blank when there is none: an
|
|
// unidentified disc has no artist to state, and an empty
|
|
// bold line reads as a loading failure.
|
|
Text {
|
|
width: parent.width
|
|
visible: text !== ""
|
|
text: root.allAlbums.length > 0 ? (root.allAlbums[0].artist || "") : ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontHeading
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
// "10 tracks · 42 min". The PATH used to live here and no
|
|
// longer does: the medium's name is already the line above
|
|
// and the tab, and the duration is what a listener actually
|
|
// wants before pressing play.
|
|
Text {
|
|
width: parent.width
|
|
text: {
|
|
var n = root.eph ? (root.eph.trackCount || 0) : 0
|
|
var d = root.eph ? (root.eph.totalDuration || "") : ""
|
|
var t = n + " " + QbzSession.tr("tracks", QbzSession.trRev)
|
|
return d === "" ? t : t + " · " + d
|
|
}
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
elide: Text.ElideRight
|
|
}
|
|
// Every control here is a bare glyph, and three of them do
|
|
// things that are not guessable from a circle with an icon
|
|
// in it — so each one says what it does on hover.
|
|
Row {
|
|
spacing: 12
|
|
topPadding: 4
|
|
QbzCircleAction {
|
|
id: playAllBtn
|
|
name: "play-fill"
|
|
primary: true
|
|
onClicked: QbzLocal.ephemeralPlayAll(false)
|
|
HoverHandler {
|
|
onHoveredChanged: tips.hover(hovered, playAllBtn, "eph-play",
|
|
QbzSession.tr("Play the whole disc from the first track",
|
|
QbzSession.trRev))
|
|
}
|
|
}
|
|
QbzCircleAction {
|
|
id: shuffleBtn
|
|
name: "shuffle"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzLocal.ephemeralPlayAll(true)
|
|
HoverHandler {
|
|
onHoveredChanged: tips.hover(hovered, shuffleBtn, "eph-shuffle",
|
|
QbzSession.tr("Play it in a shuffled order", QbzSession.trRev))
|
|
}
|
|
}
|
|
// Fix the names. Offered for both DISC media — a
|
|
// CD-DA carries no titles at all, and a SACD's Master
|
|
// TOC can carry the wrong ones ("names itself" and
|
|
// "names itself correctly" are different claims) — and
|
|
// for NEITHER on an opened folder, which has no disc
|
|
// identity to write a correction under.
|
|
//
|
|
// `tags`, not a magnifier: the button edits this
|
|
// record's metadata. The search is only how it gets
|
|
// there, and a magnifier next to a search field that
|
|
// filters tracks reads as the same control twice.
|
|
QbzCircleAction {
|
|
id: metaBtn
|
|
visible: QbzLocal.localSessionIsDisc && !QbzLocal.localRipActive
|
|
name: "tags"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzDiscMeta.open()
|
|
HoverHandler {
|
|
onHoveredChanged: tips.hover(hovered, metaBtn, "eph-meta",
|
|
QbzSession.tr("Look this disc up and correct its details",
|
|
QbzSession.trRev))
|
|
}
|
|
}
|
|
// Rip — a PHYSICAL disc only. An image is already a
|
|
// file, so offering it there would be offering to copy
|
|
// something the user already has.
|
|
QbzCircleAction {
|
|
id: ripBtn
|
|
visible: QbzLocal.localEphemeralIsCd && !QbzLocal.localRipActive
|
|
name: "disc-folder"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzLocal.ripWizardOpen()
|
|
HoverHandler {
|
|
onHoveredChanged: tips.hover(hovered, ripBtn, "eph-rip",
|
|
QbzSession.tr("Copy this CD to your computer as FLAC files",
|
|
QbzSession.trRev))
|
|
}
|
|
}
|
|
// While it runs the button gives way to its progress —
|
|
// one control, one state, rather than a button that
|
|
// looks pressable during a job it cannot start twice.
|
|
//
|
|
// And the indicator is a DOOR, not a label: it is the
|
|
// only thing on screen while a rip runs, and the two
|
|
// questions it cannot answer — which track, and is it
|
|
// safe to eject — are the ones worth opening a panel
|
|
// for.
|
|
Rectangle {
|
|
id: ripChip
|
|
visible: QbzLocal.localRipActive
|
|
width: ripRow.width + 20
|
|
height: 32
|
|
radius: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: ripArea.containsMouse
|
|
? theme.surfaceHover
|
|
: (theme.ambientOn ? theme.surfaceElevatedA50 : theme.surfaceElevated)
|
|
Row {
|
|
id: ripRow
|
|
anchors.centerIn: parent
|
|
spacing: 8
|
|
QbzSpinner {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
size: 15
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Ripping", QbzSession.trRev)
|
|
+ " " + QbzLocal.localRipProgress
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: ripArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzLocal.ripPanel(true)
|
|
onContainsMouseChanged: tips.hover(containsMouse, ripChip,
|
|
"rip-open",
|
|
QbzSession.tr("See what the rip is doing, or stop it",
|
|
QbzSession.trRev))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scanning — the shape of the album blocks that are coming
|
|
// (56px cover + two text bars per block), not a spinner.
|
|
QbzSkeleton {
|
|
visible: QbzLocal.localEphemeralLoading
|
|
variant: "rowList"
|
|
width: parent.width
|
|
height: visible ? 3 * 68 : 0
|
|
rowH: 56
|
|
rowGap: 12
|
|
rowArtSize: 56
|
|
phase: root.view ? root.view.skelPhase : false
|
|
}
|
|
|
|
// Empty (no playable tracks).
|
|
Text {
|
|
visible: !QbzLocal.localEphemeralLoading && root.allAlbums.length === 0
|
|
width: parent.width
|
|
topPadding: 24
|
|
text: QbzSession.tr("No playable tracks in this folder.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontBody
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
// ---- Filter ----
|
|
//
|
|
// Above the blocks rather than in the header: it acts on the LIST,
|
|
// and a control that sits with the play buttons reads as another
|
|
// thing you do to the album. Only drawn once there is enough to be
|
|
// worth filtering — on a seven-track CD it would be furniture.
|
|
Item {
|
|
visible: !QbzLocal.localEphemeralLoading && root.eph
|
|
&& (root.eph.trackCount || 0) > 12
|
|
width: parent.width
|
|
height: visible ? 40 : 0
|
|
|
|
QbzLineEdit {
|
|
id: filterBox
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 260
|
|
sm: true
|
|
searchMode: true
|
|
placeholder: QbzSession.tr("Filter tracks", QbzSession.trRev)
|
|
text: root.query
|
|
onEdited: function (v) { root.query = v }
|
|
}
|
|
Text {
|
|
anchors.right: filterBox.left
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: root.query.trim() !== ""
|
|
text: root.matchCount + " / " + (root.eph ? (root.eph.trackCount || 0) : 0)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
}
|
|
}
|
|
|
|
// Nothing matched. Distinct from "no playable tracks": the session
|
|
// is fine, the filter is what is empty, and saying so is what
|
|
// stops it reading as a folder that failed to scan.
|
|
Text {
|
|
visible: !QbzLocal.localEphemeralLoading
|
|
&& root.allAlbums.length > 0 && root.albums.length === 0
|
|
width: parent.width
|
|
topPadding: 24
|
|
text: QbzSession.tr("No track matches that.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontBody
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
// ---- Album blocks ----
|
|
Column {
|
|
visible: !QbzLocal.localEphemeralLoading
|
|
width: parent.width
|
|
spacing: 22
|
|
Repeater {
|
|
model: root.albums
|
|
delegate: Column {
|
|
id: block
|
|
required property var modelData
|
|
width: page.width
|
|
spacing: 6
|
|
|
|
// The per-block header is for MULTI-album sessions,
|
|
// where it is the only thing separating one record
|
|
// from the next. A single-album session already has
|
|
// all of it — cover, title, artist, count — in the
|
|
// 224px header above, and drawing it twice made the
|
|
// pane read like a page with a stutter.
|
|
Row {
|
|
visible: root.eph && root.eph.multiAlbum === true
|
|
height: visible ? implicitHeight : 0
|
|
width: parent.width
|
|
spacing: 12
|
|
Rectangle {
|
|
width: 56
|
|
height: 56
|
|
radius: 6
|
|
color: theme.surfaceHover
|
|
clip: true
|
|
QbzIcon {
|
|
name: "disc-3"
|
|
width: 24
|
|
height: 24
|
|
anchors.centerIn: parent
|
|
tintName: "muted"
|
|
}
|
|
RoundedImage {
|
|
id: blockArt
|
|
anchors.fill: parent
|
|
source: root.view
|
|
? root.view.artPathOf(block.modelData.artKey) : ""
|
|
radius: 6
|
|
}
|
|
// Per-item: hands over when THIS album's
|
|
// cover is actually painted, settles out when
|
|
// it has none.
|
|
QbzSkeleton {
|
|
variant: "art"
|
|
anchors.fill: parent
|
|
blockRadius: 6
|
|
pending: root.view
|
|
? root.view.artWanted(block.modelData.artKey) : false
|
|
coverReady: blockArt.ready
|
|
phase: root.view ? root.view.skelPhase : false
|
|
settleMs: root.view ? root.view.artSettleMs : 0
|
|
settleHold: root.view ? root.view.artPulse : false
|
|
}
|
|
}
|
|
Column {
|
|
width: parent.width - 56 - 12
|
|
- (root.eph && root.eph.multiAlbum ? 46 : 0)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Row {
|
|
width: parent.width
|
|
spacing: 6
|
|
Text {
|
|
width: parent.width - (block.modelData.isCue ? 44 : 0)
|
|
text: block.modelData.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightSemibold
|
|
elide: Text.ElideRight
|
|
}
|
|
// CUE badge (single-file rip) — a 16px
|
|
// rounded rect, not a pill (ADR-008).
|
|
Rectangle {
|
|
visible: block.modelData.isCue === true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: cueText.implicitWidth + 12
|
|
height: 16
|
|
radius: 3
|
|
color: theme.surfaceElevated
|
|
Text {
|
|
id: cueText
|
|
anchors.centerIn: parent
|
|
text: "CUE"
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
font.weight: theme.weightSemibold
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: block.modelData.artist || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
visible: (block.modelData.meta || "") !== ""
|
|
width: parent.width
|
|
text: block.modelData.meta || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
QbzCircleAction {
|
|
visible: root.eph && root.eph.multiAlbum === true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: "play-fill"
|
|
onClicked: QbzLocal.ephemeralPlayAlbum(block.modelData.groupKey)
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: block.modelData.tracks || []
|
|
delegate: TrackRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: page.width
|
|
item: modelData
|
|
number: index + 1
|
|
showArtwork: false
|
|
showAlbum: false
|
|
showFavorite: false
|
|
showDownload: false
|
|
// The menu is BACK, but only its queue block.
|
|
// A disc track can be queued, played next and
|
|
// played later; it cannot be favourited, put
|
|
// in a playlist or cached, because none of
|
|
// those references survive the eject.
|
|
showMenu: true
|
|
queueOnly: true
|
|
draggable: false
|
|
zebra: true
|
|
onPlayRequested: QbzLocal.ephemeralPlayTrack(modelData.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Close, pinned to the pane's top-right corner.
|
|
//
|
|
// It used to sit in the action row between Shuffle and the rest, which is
|
|
// where a hand goes without looking — a destructive action one pixel from
|
|
// "play". Up here it is deliberate, and it is the same place every other
|
|
// dismissable surface in this app puts its close.
|
|
//
|
|
// OUTSIDE the Flickable on purpose: scrolling a long disc must not carry
|
|
// the only way to close the session off the top of the screen.
|
|
QbzCircleAction {
|
|
id: closeSession
|
|
name: "x"
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.rightMargin: 32
|
|
anchors.topMargin: 16
|
|
onClicked: QbzLocal.ephemeralClear()
|
|
HoverHandler {
|
|
onHoveredChanged: tips.hover(hovered, closeSession, "eph-close",
|
|
QbzSession.tr("Close this disc and go back to your library",
|
|
QbzSession.trRev))
|
|
}
|
|
}
|
|
|
|
// The pane's own tooltip overlay. It takes no pointer and owns no
|
|
// animator, so an idle one costs nothing (QbzTooltip's header).
|
|
QbzTooltip {
|
|
id: tips
|
|
anchors.fill: parent
|
|
z: 900
|
|
}
|
|
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: pane
|
|
visible: pane.contentHeight > pane.height
|
|
}
|
|
}
|