feat(qbz-qt): phase 5.5 — visual polish pass audited against the Slint sources
Screen-by-screen audit with before/after screenshots: rounded surface-main content panel with 8px bezel (the Slack-style frame), real splash card, QbzPrimaryButton/QbzCheckbox/QbzTextLink replicas, accent arc spinner replacing BusyIndicator, custom thin auto-hide scrollbars (Home + Library grid/list), carousel edge fades, toolbar pill offset, QbzSlider/SeekBar metrics, alpha-format re-audit of all 8-digit hex literals. Pure look — zero functionality changes. km
No files matched your search
@@ -48,6 +48,8 @@ fn main() {
|
|||||||
"qml/LibraryView.qml",
|
"qml/LibraryView.qml",
|
||||||
"qml/QbzTheme.qml",
|
"qml/QbzTheme.qml",
|
||||||
"qml/QbzIcon.qml",
|
"qml/QbzIcon.qml",
|
||||||
|
"qml/QbzScrollBar.qml",
|
||||||
|
"qml/QbzSpinner.qml",
|
||||||
],
|
],
|
||||||
qrc_files: &qrc_refs,
|
qrc_files: &qrc_refs,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
// App shell — the QML port of crates/qbz-ui/ui/shell/AppShell.slint's
|
// App shell — the QML port of crates/qbz-ui/ui/shell/AppShell.slint's
|
||||||
// chrome: HeaderBar (top, 42px) / { Sidebar | content | queue column } /
|
// chrome: HeaderBar (top, 42px) / { Sidebar | content frame | queue
|
||||||
// NowPlayingBarSmall (bottom). The recovery affordance and logout live in
|
// column } / NowPlayingBarSmall (bottom).
|
||||||
// the header (offline badge flyout + app menu), like the Slint shell.
|
//
|
||||||
|
// The window chrome is surface-card throughout; the content area is a
|
||||||
|
// ROUNDED surface-main panel inset 8px left/right/bottom (0 top — it
|
||||||
|
// butts the header), the "Slack-style bezel on all four corners" of
|
||||||
|
// AppShell.slint:358-390. The recovery affordance and logout live in the
|
||||||
|
// header (offline badge flyout + app menu), like the Slint shell.
|
||||||
|
//
|
||||||
|
// POC-NOTE: the artwork-derived ambient background (AppearanceState
|
||||||
|
// "Ambient"/"Blurred art" modes) is not implemented — the Slint default
|
||||||
|
// is Off, which is what this static dark treatment matches.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import com.blitzfc.qbz
|
import com.blitzfc.qbz
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
color: theme.surfaceMain
|
// The window chrome is surface-card (header / sidebar / queue column /
|
||||||
|
// NPB all paint it too); the content panel contrasts in surface-main.
|
||||||
|
color: theme.surfaceCard
|
||||||
|
|
||||||
QbzTheme { id: theme }
|
QbzTheme { id: theme }
|
||||||
|
|
||||||
@@ -58,14 +69,20 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content frame — the view area between sidebar and queue column.
|
// Content frame — the rounded, inset surface-main panel (Radius.md,
|
||||||
|
// 8px gaps left/right/bottom, flush to the header).
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: contentFrame
|
id: contentFrame
|
||||||
anchors.left: sidebar.right
|
anchors.left: sidebar.right
|
||||||
anchors.right: queueColumn.left
|
anchors.right: queueColumn.left
|
||||||
anchors.top: header.bottom
|
anchors.top: header.bottom
|
||||||
anchors.bottom: npb.top
|
anchors.bottom: npb.top
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
radius: theme.radiusMd
|
||||||
color: theme.surfaceMain
|
color: theme.surfaceMain
|
||||||
|
border.width: 0
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -370,16 +370,47 @@ Rectangle {
|
|||||||
onPageLeft: rail.contentX = Math.min(0, rail.contentX - step)
|
onPageLeft: rail.contentX = Math.min(0, rail.contentX - step)
|
||||||
onPageRight: rail.contentX = Math.min(maxScroll, rail.contentX + step)
|
onPageRight: rail.contentX = Math.min(maxScroll, rail.contentX + step)
|
||||||
}
|
}
|
||||||
ListView {
|
Item {
|
||||||
id: rail
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 246
|
height: 246
|
||||||
orientation: ListView.Horizontal
|
ListView {
|
||||||
spacing: 32
|
id: rail
|
||||||
clip: true
|
anchors.fill: parent
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
orientation: ListView.Horizontal
|
||||||
model: sectionData.items
|
spacing: 32
|
||||||
delegate: AlbumCard { card: modelData }
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
model: sectionData.items
|
||||||
|
delegate: AlbumCard { card: modelData }
|
||||||
|
}
|
||||||
|
// Cider-style edge fades (Carousel.slint): content dissolves
|
||||||
|
// into the page background at the scrolled edges.
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: 56
|
||||||
|
opacity: rail.contentX > 1 ? 1.0 : 0.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
GradientStop { position: 0.0; color: theme.surfaceMain }
|
||||||
|
GradientStop { position: 1.0; color: "transparent" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: 56
|
||||||
|
opacity: rail.contentX < maxScroll - 1 ? 1.0 : 0.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
GradientStop { position: 0.0; color: "transparent" }
|
||||||
|
GradientStop { position: 1.0; color: theme.surfaceMain }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +719,9 @@ Rectangle {
|
|||||||
height: 56
|
height: 56
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
x: 32
|
// Slint left-controls: x 32 + NavButtons (now a 0px
|
||||||
|
// placeholder) + 16px spacing -> the pill starts at 48.
|
||||||
|
x: 48
|
||||||
y: 25 - height / 2
|
y: 25 - height / 2
|
||||||
height: tabPill.height
|
height: tabPill.height
|
||||||
spacing: 16
|
spacing: 16
|
||||||
@@ -805,10 +838,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Scrollable sections -----------------------------------------
|
// --- Scrollable sections -----------------------------------------
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 57
|
||||||
Flickable {
|
Flickable {
|
||||||
id: homeFlick
|
id: homeFlick
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 57
|
height: parent.height
|
||||||
clip: true
|
clip: true
|
||||||
contentWidth: width
|
contentWidth: width
|
||||||
contentHeight: homeContent.implicitHeight
|
contentHeight: homeContent.implicitHeight
|
||||||
@@ -988,5 +1024,14 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Thin auto-hiding scrollbar in the right gutter (ListScrollbar).
|
||||||
|
QbzScrollBar {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
target: homeFlick
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,10 +949,10 @@ Rectangle {
|
|||||||
height: parent.height - 57
|
height: parent.height - 57
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
// Loading.
|
// Loading (LoadingSpinner.slint: accent arc, 1s spin).
|
||||||
BusyIndicator {
|
QbzSpinner {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
running: QbzBridge.libraryLoading
|
size: 36
|
||||||
visible: QbzBridge.libraryLoading
|
visible: QbzBridge.libraryLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,6 +1049,24 @@ Rectangle {
|
|||||||
item: modelData
|
item: modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thin auto-hiding scrollbars (ListScrollbar replica).
|
||||||
|
QbzScrollBar {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
target: grid
|
||||||
|
visible: grid.visible && grid.contentHeight > grid.height
|
||||||
|
}
|
||||||
|
QbzScrollBar {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
target: list
|
||||||
|
visible: list.visible && list.contentHeight > list.height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,15 +92,16 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
radius: 4
|
radius: 4
|
||||||
color: root.tosAccepted ? theme.accent : "transparent"
|
color: root.tosAccepted ? theme.accent : "transparent"
|
||||||
border.color: root.tosAccepted ? theme.accent : theme.textMuted
|
border.color: theme.textMuted
|
||||||
border.width: 1
|
border.width: root.tosAccepted ? 0 : 1.5
|
||||||
|
|
||||||
Text {
|
QbzIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: root.tosAccepted
|
visible: root.tosAccepted
|
||||||
text: "✓"
|
name: "check"
|
||||||
color: theme.textPrimary
|
width: 12
|
||||||
font.pixelSize: 13
|
height: 12
|
||||||
|
tintName: "primary"
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -135,18 +136,19 @@ Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: signInButton
|
id: signInButton
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 44
|
height: 48
|
||||||
radius: theme.radiusSm
|
radius: theme.radiusSm
|
||||||
property bool canSignIn: root.tosAccepted && QbzBridge.loginPhase === 0
|
property bool canSignIn: root.tosAccepted && QbzBridge.loginPhase === 0
|
||||||
color: canSignIn ? (signInArea.containsMouse ? "#5a95f5" : theme.accent)
|
opacity: canSignIn ? 1.0 : 0.5
|
||||||
: theme.surfaceElevated
|
color: signInArea.containsMouse && canSignIn ? theme.accentHover : theme.accent
|
||||||
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: QbzBridge.tr("Sign in with your browser")
|
text: QbzBridge.tr("Sign in with your browser")
|
||||||
color: signInButton.canSignIn ? theme.textPrimary : theme.textMuted
|
color: theme.accentText
|
||||||
font.pixelSize: theme.fontBody
|
font.pixelSize: theme.fontButton
|
||||||
font.weight: theme.weightMedium
|
font.weight: theme.weightSemibold
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: signInArea
|
id: signInArea
|
||||||
@@ -185,13 +187,26 @@ Rectangle {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
Item { width: 1; height: 4 }
|
Item { width: 1; height: 4 }
|
||||||
Text {
|
Item {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: QbzBridge.tr("Cancel")
|
width: cancelText.implicitWidth
|
||||||
color: theme.accent
|
height: cancelText.implicitHeight + 3
|
||||||
font.pixelSize: theme.fontBody
|
Text {
|
||||||
|
id: cancelText
|
||||||
|
text: QbzBridge.tr("Cancel")
|
||||||
|
color: cancelArea.containsMouse ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: theme.fontLink
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
y: cancelText.implicitHeight + 1
|
||||||
|
width: cancelText.implicitWidth
|
||||||
|
height: 1
|
||||||
|
color: cancelArea.containsMouse ? theme.accent : theme.textMuted
|
||||||
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: QbzBridge.cancelLogin()
|
onClicked: QbzBridge.cancelLogin()
|
||||||
}
|
}
|
||||||
@@ -335,13 +350,26 @@ Rectangle {
|
|||||||
// Always visible (#553): without a previous session this
|
// Always visible (#553): without a previous session this
|
||||||
// opens the GUEST profile (user 0) — Local Library only,
|
// opens the GUEST profile (user 0) — Local Library only,
|
||||||
// adopted by the account on the first real login.
|
// adopted by the account on the first real login.
|
||||||
Text {
|
Item {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: QbzBridge.tr("Start offline (no access to Qobuz™ services)")
|
width: offlineText.implicitWidth
|
||||||
color: theme.accent
|
height: offlineText.implicitHeight + 3
|
||||||
font.pixelSize: theme.fontBody
|
Text {
|
||||||
|
id: offlineText
|
||||||
|
text: QbzBridge.tr("Start offline (no access to Qobuz™ services)")
|
||||||
|
color: offlineArea.containsMouse ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: theme.fontLink
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
y: offlineText.implicitHeight + 1
|
||||||
|
width: offlineText.implicitWidth
|
||||||
|
height: 1
|
||||||
|
color: offlineArea.containsMouse ? theme.accent : theme.textMuted
|
||||||
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: offlineArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: QbzBridge.startOffline()
|
onClicked: QbzBridge.startOffline()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,25 +38,56 @@ ApplicationWindow {
|
|||||||
: (QbzBridge.screen === "shell" ? "AppShell.qml" : "")
|
: (QbzBridge.screen === "shell" ? "AppShell.qml" : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splash: shown until the silent session restore resolves.
|
// Splash (SplashScreen.slint): the same 720px dark card as the login
|
||||||
|
// screen while the silent session restore resolves.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "#0f0f0f"
|
color: "#0f0f0f"
|
||||||
visible: QbzBridge.screen === "splash"
|
visible: QbzBridge.screen === "splash"
|
||||||
|
|
||||||
Column {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 16
|
width: 720
|
||||||
Image {
|
height: splashColumn.height + 104
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
radius: 16
|
||||||
source: "assets/qbz-logo.png"
|
color: "#1a1a1a"
|
||||||
width: 120
|
|
||||||
height: 120
|
Column {
|
||||||
fillMode: Image.PreserveAspectFit
|
id: splashColumn
|
||||||
}
|
anchors.left: parent.left
|
||||||
BusyIndicator {
|
anchors.right: parent.right
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.top: parent.top
|
||||||
running: true
|
anchors.margins: 52
|
||||||
|
spacing: 0
|
||||||
|
Image {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
source: "assets/qbz-logo.png"
|
||||||
|
width: 140
|
||||||
|
height: 140
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
Item { width: 1; height: 8 }
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "QBZ"
|
||||||
|
color: "#ffffff"
|
||||||
|
font.pixelSize: 29
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 8
|
||||||
|
}
|
||||||
|
Item { width: 1; height: 2 }
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "QOBUZ™ PLAYER"
|
||||||
|
color: "#888888"
|
||||||
|
font.pixelSize: 15
|
||||||
|
font.letterSpacing: 4
|
||||||
|
}
|
||||||
|
Item { width: 1; height: 32 }
|
||||||
|
QbzSpinner {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
size: 32
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,14 +108,14 @@ Rectangle {
|
|||||||
id: seekTrack
|
id: seekTrack
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 3
|
height: 3
|
||||||
radius: 1.5
|
radius: 2
|
||||||
color: theme.surfaceElevated
|
color: theme.surfaceElevated
|
||||||
|
|
||||||
// Buffered / cache line.
|
// Buffered / cache line.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width * Math.min(Math.max(QbzBridge.npCacheProgress, 0), 1)
|
width: parent.width * Math.min(Math.max(QbzBridge.npCacheProgress, 0), 1)
|
||||||
height: parent.height
|
height: parent.height
|
||||||
radius: 1.5
|
radius: 2
|
||||||
color: theme.textMuted
|
color: theme.textMuted
|
||||||
opacity: 0.35
|
opacity: 0.35
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width * Math.min(Math.max(QbzBridge.npProgress, 0), 1)
|
width: parent.width * Math.min(Math.max(QbzBridge.npProgress, 0), 1)
|
||||||
height: parent.height
|
height: parent.height
|
||||||
radius: 1.5
|
radius: 2
|
||||||
color: theme.accent
|
color: theme.accent
|
||||||
}
|
}
|
||||||
// Hover thumb.
|
// Hover thumb.
|
||||||
@@ -421,20 +421,20 @@ Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: volTrack
|
id: volTrack
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 3
|
height: 4
|
||||||
radius: 1.5
|
radius: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: theme.surfaceElevated
|
color: theme.surfaceElevated
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width * QbzBridge.npVolume
|
width: parent.width * QbzBridge.npVolume
|
||||||
height: parent.height
|
height: parent.height
|
||||||
radius: 1.5
|
radius: 2
|
||||||
color: theme.accent
|
color: theme.accent
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12
|
width: 16
|
||||||
height: 12
|
height: 16
|
||||||
radius: 6
|
radius: 8
|
||||||
color: theme.textPrimary
|
color: theme.textPrimary
|
||||||
x: parent.width * QbzBridge.npVolume - width / 2
|
x: parent.width * QbzBridge.npVolume - width / 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Custom vertical scrollbar — replica of primitives/ListScrollbar.slint.
|
||||||
|
// 14px gutter (not over the rows), 6px #ffffff12 track, 8px thumb
|
||||||
|
// (#ffffff44, widens to 10px / #ffffff77 on hover|drag), 48px min thumb,
|
||||||
|
// auto-hide 900ms after the last scroll. Attach as a sibling anchored to
|
||||||
|
// the right of a Flickable/GridView/ListView and set `target`.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
required property Flickable target
|
||||||
|
|
||||||
|
width: 14
|
||||||
|
visible: maxScroll > 0
|
||||||
|
|
||||||
|
readonly property real maxScroll: Math.max(0, target.contentHeight - target.height)
|
||||||
|
readonly property real thumbH: target.contentHeight > 0
|
||||||
|
? Math.max(48, target.height / target.contentHeight * height)
|
||||||
|
: height
|
||||||
|
readonly property real travel: Math.max(0, height - thumbH)
|
||||||
|
// Shown while scrolling, hovering, or dragging (auto-hide).
|
||||||
|
property bool scrollActive: false
|
||||||
|
readonly property bool shown: scrollActive || barArea.containsMouse || barArea.pressed
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.target
|
||||||
|
function onContentYChanged() {
|
||||||
|
root.scrollActive = true
|
||||||
|
hideTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: hideTimer
|
||||||
|
interval: 900
|
||||||
|
onTriggered: root.scrollActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track.
|
||||||
|
Rectangle {
|
||||||
|
width: 6
|
||||||
|
x: Math.round((parent.width - width) / 2)
|
||||||
|
height: parent.height
|
||||||
|
radius: 3
|
||||||
|
color: "#12ffffff"
|
||||||
|
opacity: root.shown ? 1.0 : 0.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||||
|
}
|
||||||
|
// Thumb.
|
||||||
|
Rectangle {
|
||||||
|
width: barArea.containsMouse || barArea.pressed ? 10 : 8
|
||||||
|
x: Math.round((parent.width - width) / 2)
|
||||||
|
height: root.thumbH
|
||||||
|
y: root.maxScroll > 0 ? (root.target.contentY / root.maxScroll) * root.travel : 0
|
||||||
|
radius: 5
|
||||||
|
color: barArea.containsMouse || barArea.pressed ? "#77ffffff" : "#44ffffff"
|
||||||
|
opacity: root.shown ? 1.0 : 0.0
|
||||||
|
Behavior on width { NumberAnimation { duration: 100 } }
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||||
|
}
|
||||||
|
// Drag / click-to-position over the whole gutter.
|
||||||
|
MouseArea {
|
||||||
|
id: barArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onPressed: position(mouseY)
|
||||||
|
onPositionChanged: if (pressed) position(mouseY)
|
||||||
|
function position(my) {
|
||||||
|
if (root.travel <= 0) return
|
||||||
|
var frac = Math.min(1, Math.max(0, (my - root.thumbH / 2) / root.travel))
|
||||||
|
root.target.contentY = frac * root.maxScroll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Loading spinner — replica of primitives/LoadingSpinner.slint: the
|
||||||
|
// Lucide loader-circle arc in accent, rotating 360deg/1000ms linear.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
property int size: 32
|
||||||
|
property bool spinning: true
|
||||||
|
|
||||||
|
width: size
|
||||||
|
height: size
|
||||||
|
|
||||||
|
QbzIcon {
|
||||||
|
name: "loader-circle"
|
||||||
|
width: root.size
|
||||||
|
height: root.size
|
||||||
|
tintName: "accent"
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: root.spinning && root.visible
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
loops: Animation.Infinite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 222 B |