The AppShell hotkey seam duck-types selectAll, exitMultiSelectMode and multiSelectOn on the mounted view. The offline manager exposed none of them, so Ctrl+A did nothing there although the Slint ticked every track, and MixView lacked multiSelectOn, so Escape never left its select mode. The seam's comment, which still listed Artist, Playlist, Mix and Label as views without multi-select, now names the views that implement it. cc
518 lines
25 KiB
QML
518 lines
25 KiB
QML
// Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ) — QML port of
|
|
// mix/MixView.slint. Route "mix", opened from the Qobuz Mixes tiles on
|
|
// Discover > For You.
|
|
//
|
|
// ONE JSON document (QbzHome.mixJson, src/foryou_qt.rs MixDoc): kind, title,
|
|
// subtitle, trackCount, totalDuration, loading, tracks[]. The gradient is
|
|
// keyed off `kind` through the shared cards/MixArtwork.qml, so the page's
|
|
// artwork is literally the tile the user clicked, enlarged.
|
|
//
|
|
// SCROLLING-PAGE CONVENTION (AlbumView / LabelView / PlaylistView): one
|
|
// Flickable whose Column pads 32 / 32 / top 11 / bottom 100. The .slint's
|
|
// NavButtons row is dropped (back/forward live in the shell header,
|
|
// HeaderBar.qml:230) but its 22px spacer is KEPT, so the header sits where
|
|
// every other detail page's does.
|
|
//
|
|
// Numbers from MixView.slint: art 224 radius Radius.md · header gap 32 ·
|
|
// eyebrow 11px semibold letter-spacing 1.5 · title Typography.section bold ·
|
|
// subtitle Typography.body wrapped at `root.width - 320` · info line
|
|
// Typography.legal · spacers 6/8/10/20/28 · then the column header, a 3px
|
|
// gap, a 1px border-subtle rule, 6px, then the rows.
|
|
//
|
|
// The column header is rows/TrackListHeader.qml — NOT the .slint's numbers.
|
|
// MixView.slint:246-284 lays its header out with 16px gaps, a 40px "#", a
|
|
// 64px Duration and NO artwork reserve, while its rows are the shared
|
|
// TrackRow (14px gaps, 32px number, 36px artwork, 70px Duration): the labels
|
|
// there sit right of the columns they name, which is exactly the defect
|
|
// being fixed here. The port takes the ROW's geometry from
|
|
// rows/TrackCols.qml for both.
|
|
//
|
|
// POC-NOTEs, each deliberate and each with a precedent in this port:
|
|
// - NO multi-select bar — and the ORIGINAL reason is now STALE on both
|
|
// halves: rows/TrackRow.qml:116 DOES have the `selectMode` arm, and
|
|
// add-to-playlist DOES have a bridge seam (QbzPlaylistPicker.openForTracks,
|
|
// src/playlist_picker_bridge.rs). What is still missing here is only the
|
|
// selection STATE and the bar itself — LibraryView.qml's `tracksSelected`
|
|
// map + qml/controls/MultiSelectBar-style host is the pattern to copy.
|
|
// Until then the .slint header's select disc renders DIMMED and inert here
|
|
// (the ArtistView / LabelView precedent). Per-ROW add-to-playlist and
|
|
// add-to-mixtape are both LIVE on this page already (the shared TrackRow
|
|
// menu).
|
|
// - The "Add to playlist" disc (list-plus) is inert in the .slint too — it
|
|
// carries no `clicked` there (MixView.slint:182-188) — so it is inert
|
|
// here, dimmed the same way, rather than silently absent.
|
|
// - Back/forward scroll restore is handled by ScrollMemory below.
|
|
// - CIRCLE ACTION SIZE: the .slint MixView declares a LOCAL CircleAction
|
|
// fixed at 38px, predating the shared primitives/CircleAction.slint
|
|
// (44 primary / 32 secondary). This port mounts the shared
|
|
// controls/QbzCircleAction.qml — the port of the SHARED primitive — so the
|
|
// mix page's discs match every other detail page in this build.
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
import "../cards"
|
|
import "../controls"
|
|
import "../rows"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
property bool kioskHost: false
|
|
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: theme.ambientOn
|
|
radius: 12
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property var doc: {
|
|
try {
|
|
return JSON.parse(QbzHome.mixJson)
|
|
} catch (e) {
|
|
return ({})
|
|
}
|
|
}
|
|
readonly property var tracks: root.doc.tracks || []
|
|
readonly property bool loading: root.doc.loading === true
|
|
readonly property string kind: root.doc.kind || "daily"
|
|
|
|
// --- Multi-select (parity with PlaylistView) --------------------------
|
|
// The mix rows are Qobuz catalog tracks (see the delegate's SOURCE note),
|
|
// so the selection rides the shared bulk seam exactly like a playlist:
|
|
// selection lives in QML, select-all/clear never reach Rust, and every
|
|
// other action goes down as a JSON id array through
|
|
// QbzPlayer.bulkTracksAction (bulk_tracks_qt.rs). This is what the header
|
|
// note called out as the only missing piece.
|
|
property bool multiSelect: false
|
|
property var selected: ({})
|
|
readonly property int selectedCount: Object.keys(root.selected).length
|
|
SelectionModel { id: sel }
|
|
function setMultiSelect(on) {
|
|
root.multiSelect = on
|
|
if (!on) { root.selected = ({}); sel.anchorId = "" }
|
|
}
|
|
function toggleSelected(id, mods) {
|
|
root.selected = sel.next(root.selected, id, root.tracks,
|
|
mods === undefined ? Qt.NoModifier : mods)
|
|
}
|
|
function selectedIdsInOrder() {
|
|
var rows = root.tracks
|
|
var out = []
|
|
for (var i = 0; i < rows.length; i++)
|
|
if (root.selected[rows[i].id] === true) out.push(rows[i].id)
|
|
return out
|
|
}
|
|
function allTrackIds() {
|
|
var rows = root.tracks
|
|
var out = []
|
|
for (var i = 0; i < rows.length; i++) out.push(rows[i].id)
|
|
return out
|
|
}
|
|
function bulkAction(action) {
|
|
if (action === "select-all") {
|
|
var m = {}
|
|
var rows = root.tracks
|
|
for (var i = 0; i < rows.length; i++) m[rows[i].id] = true
|
|
root.selected = m
|
|
return
|
|
}
|
|
if (action === "clear") { root.selected = ({}); sel.anchorId = ""; return }
|
|
var ids = root.selectedIdsInOrder()
|
|
if (ids.length === 0) return
|
|
QbzPlayer.bulkTracksAction(JSON.stringify(ids), action, "mix", root.kind)
|
|
if (action !== "add-to-playlist" && action !== "add-to-mixtape")
|
|
root.selected = ({})
|
|
}
|
|
// Ctrl+A / Escape hotkey seam (AppShell duck-types these). Without
|
|
// `multiSelectOn` the Escape stack never saw this view in select mode, so
|
|
// Escape did not leave it here while it did on every other track list.
|
|
readonly property bool multiSelectOn: root.multiSelect
|
|
function selectAll() {
|
|
if (!root.multiSelect) root.setMultiSelect(true)
|
|
root.bulkAction("select-all")
|
|
}
|
|
function exitMultiSelectMode() {
|
|
if (root.multiSelect) root.setMultiSelect(false)
|
|
}
|
|
// Drop the selection when the mix identity changes (switching tiles).
|
|
onKindChanged: root.setMultiSelect(false)
|
|
|
|
// --- skeleton pulse (the HomeView gating rule: freeze on NOT VISIBLE,
|
|
// never on lost focus) --------------------------------------------------
|
|
property bool skelPhase: false
|
|
readonly property bool windowShowing: root.Window.window
|
|
? (root.Window.window.visibility !== Window.Minimized
|
|
&& root.Window.window.visibility !== Window.Hidden)
|
|
: true
|
|
Timer {
|
|
interval: 900
|
|
repeat: true
|
|
running: root.visible && root.windowShowing && root.loading
|
|
onTriggered: root.skelPhase = !root.skelPhase
|
|
}
|
|
|
|
// ============================ offline gate ============================
|
|
QbzOfflinePlaceholder {
|
|
visible: QbzSession.offline
|
|
anchors.centerIn: parent
|
|
showSettingsAction: true
|
|
onSettingsClicked: QbzShell.navigateTo("settings")
|
|
}
|
|
|
|
// Whole-mix queue menu (header "list-end" disc). Routes every track
|
|
// through the shared bulk seam, so Play next / Play later land in the same
|
|
// sections a per-row action would.
|
|
CardMenu {
|
|
id: mixQueueMenu
|
|
menuWidth: 190
|
|
entries: [
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "play-next" },
|
|
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "play-later" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" }
|
|
]
|
|
onPicked: function (a) {
|
|
if (root.tracks.length === 0) return
|
|
QbzPlayer.bulkTracksAction(
|
|
JSON.stringify(root.allTrackIds()), a, "mix", root.kind)
|
|
}
|
|
}
|
|
|
|
// ============================ the page ================================
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
visible: !QbzSession.offline
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: page
|
|
width: parent.width
|
|
leftPadding: 32
|
|
rightPadding: 32
|
|
topPadding: 11
|
|
bottomPadding: 100
|
|
spacing: 0
|
|
|
|
// The .slint's NavButtons row is not drawn (shell header owns
|
|
// back/forward) but its 22px spacer is kept.
|
|
Item { width: 1; height: 22 }
|
|
|
|
// --- Header -----------------------------------------------------
|
|
Row {
|
|
width: parent.width - 64
|
|
spacing: 32
|
|
|
|
// The mix gradient — the SAME square the For You tile drew,
|
|
// at 224px with no badge and a 34px name.
|
|
MixArtwork {
|
|
kind: root.kind
|
|
size: root.kioskHost ? 112 : 224
|
|
titleSize: 34
|
|
cornerRadius: theme.radiusMd
|
|
showBadge: false
|
|
interactive: false
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - (root.kioskHost ? 112 : 224) - 32
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 4
|
|
spacing: 0
|
|
|
|
Text {
|
|
text: QbzSession.tr("Qobuz Mixes", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 11
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 1.5
|
|
}
|
|
Item { width: 1; height: 6 }
|
|
Text {
|
|
width: parent.width
|
|
text: root.doc.title || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontSection
|
|
font.weight: theme.weightBold
|
|
elide: Text.ElideRight
|
|
}
|
|
Item { width: 1; height: 8 }
|
|
Text {
|
|
// .slint: `width: root.width - 320px`.
|
|
width: Math.max(0, root.width - 320)
|
|
text: root.doc.subtitle || ""
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Item { width: 1; height: 10 }
|
|
Text {
|
|
visible: (root.doc.trackCount || 0) > 0
|
|
text: (root.doc.trackCount || 0) + " "
|
|
+ QbzSession.tr("tracks", QbzSession.trRev)
|
|
+ " • " + (root.doc.totalDuration || "")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
}
|
|
Item { width: 1; height: 20 }
|
|
|
|
// Action discs (MixView.slint:161-210).
|
|
Row {
|
|
spacing: 12
|
|
QbzCircleAction {
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "play-fill"
|
|
primary: true
|
|
btnEnabled: root.tracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.mixPlayAll()
|
|
}
|
|
QbzCircleAction {
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "shuffle"
|
|
btnEnabled: root.tracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.mixShuffle()
|
|
}
|
|
// Queue options for the WHOLE mix — Play next / Play
|
|
// later / Add to queue (the header lacked these).
|
|
QbzCircleAction {
|
|
id: mixQueueDisc
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "list-end"
|
|
btnEnabled: root.tracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: mixQueueMenu.openBelowLeft(mixQueueDisc)
|
|
}
|
|
// Add-to-playlist — "create playlist from this mix":
|
|
// the picker (QbzPlaylistPicker) offers a New Playlist
|
|
// arm, so this seeds a fresh playlist from every track.
|
|
QbzCircleAction {
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "list-plus"
|
|
btnEnabled: root.tracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzPlaylistPicker.openForTracks(
|
|
JSON.stringify(root.allTrackIds()))
|
|
}
|
|
// Multi-select toggle — lights the bulk bar + the row
|
|
// checkboxes (parity with PlaylistView).
|
|
QbzCircleAction {
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "square-check-big"
|
|
active: root.multiSelect
|
|
btnEnabled: root.tracks.length > 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: root.setMultiSelect(!root.multiSelect)
|
|
}
|
|
QbzCircleAction {
|
|
diameterOverride: root.kioskHost ? 64 : 0
|
|
name: "home-gear"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onClicked: QbzHome.mixRefresh()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: 28 }
|
|
|
|
// --- Loading / empty --------------------------------------------
|
|
// The .slint mounts a bare centred spinner. This port shows the
|
|
// SHAPE of the list that is coming (the PlaylistView / LabelView
|
|
// convention): a column of track-row placeholders.
|
|
QbzSkeleton {
|
|
visible: root.loading && root.tracks.length === 0
|
|
width: parent.width - 64
|
|
height: visible ? 8 * 56 : 0
|
|
variant: "rowList"
|
|
rowH: 50
|
|
rowGap: 6
|
|
rowArt: true
|
|
rowArtSize: 36
|
|
phase: root.skelPhase
|
|
}
|
|
Text {
|
|
visible: !root.loading && root.tracks.length === 0
|
|
text: QbzSession.tr("No tracks for this mix.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontBody
|
|
}
|
|
|
|
// --- Multi-select bulk bar (parity with PlaylistView) -----------
|
|
// In flow above the column header; shown only while selecting.
|
|
QbzMultiSelectBar {
|
|
kioskHost: root.kioskHost
|
|
visible: root.multiSelect && root.tracks.length > 0
|
|
width: parent.width - 64
|
|
selectedCount: root.selectedCount
|
|
actions: [
|
|
{ "id": "select-all", "label": QbzSession.tr("Select all", QbzSession.trRev), "icon": "square-check-big", "danger": false, "needsSelection": false },
|
|
{ "id": "play-next", "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "danger": false, "needsSelection": true },
|
|
{ "id": "play-later", "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "danger": false, "needsSelection": true },
|
|
{ "id": "queue", "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "danger": false, "needsSelection": true },
|
|
{ "id": "add-to-playlist", "label": QbzSession.tr("Add to playlist", QbzSession.trRev), "icon": "list-music", "danger": false, "needsSelection": true },
|
|
{ "id": "add-to-mixtape", "label": QbzSession.tr("Add to Mixtape/Collection", QbzSession.trRev), "icon": "cassette-tape", "danger": false, "needsSelection": true },
|
|
{ "id": "add-to-favorites", "label": QbzSession.tr("Add to Library", QbzSession.trRev), "icon": "heart", "danger": false, "needsSelection": true },
|
|
{ "id": "make-offline", "label": QbzSession.tr("Make available offline", QbzSession.trRev), "icon": "cloud-download", "danger": false, "needsSelection": true },
|
|
{ "id": "clear", "label": QbzSession.tr("Clear", QbzSession.trRev), "icon": "x", "danger": false, "needsSelection": true }
|
|
]
|
|
onAction: function (id) { root.bulkAction(id) }
|
|
}
|
|
|
|
// --- Track list column header -----------------------------------
|
|
// `parent.width - 64` is the width the rows get (see the delegate
|
|
// below) — the header component's contract is that it is exactly
|
|
// as wide as the rows it labels.
|
|
TrackListHeader {
|
|
kioskHost: root.kioskHost
|
|
visible: root.tracks.length > 0
|
|
width: parent.width - 64
|
|
showArtwork: true
|
|
showAlbum: true
|
|
}
|
|
Item { visible: root.tracks.length > 0; width: 1; height: visible ? 3 : 0 }
|
|
Rectangle {
|
|
visible: root.tracks.length > 0
|
|
width: parent.width - 64
|
|
height: visible ? 1 : 0
|
|
color: theme.borderSubtle
|
|
}
|
|
Item { width: 1; height: 6 }
|
|
|
|
// --- Rows --------------------------------------------------------
|
|
// A Repeater, not a ListView: the mix is capped at 50 (dynamic
|
|
// suggest) / 100 (TopQ) / 200 (FavQ) rows and the page is ONE
|
|
// Flickable, exactly like LabelView's Popular Tracks. The 2000-row
|
|
// case that forces windowing does not arise here.
|
|
Repeater {
|
|
model: root.kioskHost ? [] : root.tracks
|
|
delegate: TrackRow {
|
|
required property var modelData
|
|
required property int index
|
|
// `parent` is the padded page Column (the LabelView
|
|
// guard: it is momentarily null while the delegate is
|
|
// being reparented).
|
|
width: parent ? parent.width - 64 : 0
|
|
item: modelData
|
|
number: index + 1
|
|
showArtwork: true
|
|
showAlbum: true
|
|
showFavorite: true
|
|
// Multi-select: the leading cell becomes a checkbox and
|
|
// the row toggles its membership (parity with PlaylistView).
|
|
selectMode: root.multiSelect
|
|
checked: root.selected[modelData.id] === true
|
|
onToggleSelect: function (mods) { root.toggleSelected(modelData.id, mods) }
|
|
// The mix list is not reorderable and is not a drag
|
|
// source for playlists (flat catalog rows, no container).
|
|
onPlayRequested: QbzHome.mixPlayTrack(modelData.id)
|
|
onEnqueueRequested: function (mode) {
|
|
QbzHome.mixEnqueueTrack(modelData.id, mode)
|
|
}
|
|
// MyQBZ "Add to mixtape" — the HOST builds the AddItem
|
|
// array (TrackRow does not know itemType/source).
|
|
//
|
|
// SOURCE: these rows carry no source field, and they do
|
|
// not need one — every one of the four mixes is built
|
|
// from `qbz_models::Track` values that came back from the
|
|
// Qobuz API (foryou_qt.rs:855-873: dynamic/suggest for
|
|
// daily+weekly, `get_favorites("tracks")` for fav,
|
|
// `get_playlist` for top), and the DailyQ/WeeklyQ seed
|
|
// explicitly drops local / Plex / ephemeral recents
|
|
// (foryou_qt.rs:774-776). `modelData.id` is therefore a
|
|
// Qobuz catalog id by construction of the document, not
|
|
// by assumption at this call site.
|
|
onMixtapeRequested: QbzMyQbzAdd.open(JSON.stringify([{
|
|
"itemType": "track", "source": "qobuz",
|
|
"sourceItemId": modelData.id,
|
|
"title": modelData.title || "",
|
|
"subtitle": modelData.artist || "",
|
|
// artworkUrl STAYS EMPTY here, deliberately: the mix row carries no remote art url.
|
|
// A file:// cache path must NOT be stored — the collection's
|
|
// artwork_url is a snapshot other machines read — so this needs a
|
|
// remote-url field on the document first. The five sister sites
|
|
// that HAD one were stamped 2026-08-22.
|
|
"artworkUrl": "", "year": null, "trackCount": null
|
|
}]))
|
|
}
|
|
}
|
|
Item {
|
|
id: mixWindow
|
|
visible: root.kioskHost
|
|
width: parent.width - 64
|
|
height: visible ? root.tracks.length * 64 : 0
|
|
readonly property real viewTop: { var h=flick.contentHeight; return flick.contentY-mapToItem(flick.contentItem,0,0).y }
|
|
readonly property int first: Math.max(0, Math.floor(viewTop/64)-1)
|
|
readonly property int last: Math.max(first,Math.min(root.tracks.length,Math.ceil((viewTop+flick.height)/64)+1))
|
|
Repeater {
|
|
model: root.kioskHost ? root.tracks.slice(mixWindow.first,mixWindow.last) : []
|
|
delegate: TrackRow {
|
|
kioskHost: true
|
|
y: (mixWindow.first + index) * 64
|
|
required property var modelData
|
|
required property int index
|
|
// `parent` is the padded page Column (the LabelView
|
|
// guard: it is momentarily null while the delegate is
|
|
// being reparented).
|
|
width: mixWindow.width
|
|
item: modelData
|
|
number: mixWindow.first + index + 1
|
|
showArtwork: true
|
|
showAlbum: true
|
|
showFavorite: true
|
|
// The mix list is not reorderable and is not a drag
|
|
// source for playlists (flat catalog rows, no container).
|
|
onPlayRequested: QbzHome.mixPlayTrack(modelData.id)
|
|
onEnqueueRequested: function (mode) {
|
|
QbzHome.mixEnqueueTrack(modelData.id, mode)
|
|
}
|
|
// MyQBZ "Add to mixtape" — the HOST builds the AddItem
|
|
// array (TrackRow does not know itemType/source).
|
|
//
|
|
// SOURCE: these rows carry no source field, and they do
|
|
// not need one — every one of the four mixes is built
|
|
// from `qbz_models::Track` values that came back from the
|
|
// Qobuz API (foryou_qt.rs:855-873: dynamic/suggest for
|
|
// daily+weekly, `get_favorites("tracks")` for fav,
|
|
// `get_playlist` for top), and the DailyQ/WeeklyQ seed
|
|
// explicitly drops local / Plex / ephemeral recents
|
|
// (foryou_qt.rs:774-776). `modelData.id` is therefore a
|
|
// Qobuz catalog id by construction of the document, not
|
|
// by assumption at this call site.
|
|
onMixtapeRequested: QbzMyQbzAdd.open(JSON.stringify([{
|
|
"itemType": "track", "source": "qobuz",
|
|
"sourceItemId": modelData.id,
|
|
"title": modelData.title || "",
|
|
"subtitle": modelData.artist || "",
|
|
// artworkUrl STAYS EMPTY here, deliberately: the mix row carries no remote art url.
|
|
// A file:// cache path must NOT be stored — the collection's
|
|
// artwork_url is a snapshot other machines read — so this needs a
|
|
// remote-url field on the document first. The five sister sites
|
|
// that HAD one were stamped 2026-08-22.
|
|
"artworkUrl": "", "year": null, "trackCount": null
|
|
}]))
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Thin auto-hiding scrollbar in the right gutter (ListScrollbar).
|
|
// Back/forward scroll memory (controls/ScrollMemory.qml): reports
|
|
// this container's offset while it is the live page, and restores it
|
|
// when a back/forward step arms this route.
|
|
ScrollMemory { target: flick; scope: "mix" }
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: flick
|
|
}
|
|
}
|