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
398 lines
17 KiB
QML
398 lines
17 KiB
QML
// QBZ login screen — QML port of crates/qbz-ui/ui/login/LoginScreen.slint.
|
|
//
|
|
// Centered 720px dark card: logo, wordmark, ToS checkbox gating the
|
|
// primary button, system-browser sign-in (NO webview, NO email/password),
|
|
// phase narration, sign-in error box, offline-connectivity callout (with
|
|
// captive-portal line), session-restore error box, "Start offline" link,
|
|
// legal disclaimer.
|
|
//
|
|
// All user-visible strings go through QbzBridge.tr() with the EXACT msgids
|
|
// of the Slint @tr() calls so the existing .po translations apply.
|
|
// State comes from the QbzBridge singleton (Slint's LoginState +
|
|
// OfflineState globals); actions call the bridge invokables.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
|
|
Rectangle {
|
|
id: root
|
|
color: theme.surfaceMain
|
|
|
|
// Slint: in-out property tos-accepted: true.
|
|
property bool tosAccepted: true
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// Faked drop shadow (blur 32, offset-y 8, #00000066): a translucent
|
|
// black rounded rect behind the card. Qt5Compat DropShadow is not
|
|
// assumed to be installed on the target.
|
|
Rectangle {
|
|
anchors.horizontalCenter: card.horizontalCenter
|
|
y: card.y + 8
|
|
width: card.width
|
|
height: card.height
|
|
radius: theme.radiusLg
|
|
color: theme.cardShadow
|
|
opacity: 0.5
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
anchors.centerIn: parent
|
|
width: 720
|
|
height: cardColumn.implicitHeight + 2 * theme.cardPadding
|
|
color: theme.surfaceCard
|
|
radius: theme.radiusLg
|
|
|
|
Column {
|
|
id: cardColumn
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: theme.cardPadding
|
|
spacing: 0
|
|
|
|
// --- Brand ------------------------------------------------
|
|
Image {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
source: "assets/qbz-logo.png"
|
|
width: 140
|
|
height: 140
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
Item { width: 1; height: theme.spacingSm }
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzBridge.tr("QBZ")
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontWordmark
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 8
|
|
}
|
|
Item { width: 1; height: 2 }
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzBridge.tr("QOBUZ™ PLAYER")
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontSubtitle
|
|
font.letterSpacing: 4
|
|
}
|
|
|
|
Item { width: 1; height: theme.spacingXl }
|
|
|
|
// --- Terms of Service row --------------------------------
|
|
Row {
|
|
spacing: theme.spacingSm
|
|
|
|
// Minimal dark checkbox (Slint QbzCheckbox equivalent).
|
|
Rectangle {
|
|
id: tosCheckbox
|
|
width: 18
|
|
height: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: 4
|
|
color: root.tosAccepted ? theme.accent : "transparent"
|
|
border.color: theme.textMuted
|
|
border.width: root.tosAccepted ? 0 : 1.5
|
|
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
visible: root.tosAccepted
|
|
name: "check"
|
|
width: 12
|
|
height: 12
|
|
tintName: "primary"
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.tosAccepted = !root.tosAccepted
|
|
}
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzBridge.tr("I have read the")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
}
|
|
Text {
|
|
id: tosLink
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzBridge.tr("Qobuz™ Terms of Service")
|
|
color: theme.accent
|
|
font.pixelSize: theme.fontBody
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzBridge.openTos()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: theme.spacingLg }
|
|
|
|
// --- Sign in ---------------------------------------------
|
|
// Opens the user's default web browser (no embedded webview).
|
|
Rectangle {
|
|
id: signInButton
|
|
width: parent.width
|
|
height: 48
|
|
radius: theme.radiusSm
|
|
property bool canSignIn: root.tosAccepted && QbzBridge.loginPhase === 0
|
|
opacity: canSignIn ? 1.0 : 0.5
|
|
color: signInArea.containsMouse && canSignIn ? theme.accentHover : theme.accent
|
|
Behavior on color { ColorAnimation { duration: 150 } }
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: QbzBridge.tr("Sign in with your browser")
|
|
color: theme.accentText
|
|
font.pixelSize: theme.fontButton
|
|
font.weight: theme.weightSemibold
|
|
}
|
|
MouseArea {
|
|
id: signInArea
|
|
anchors.fill: parent
|
|
enabled: signInButton.canSignIn
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzBridge.signInViaBrowser()
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: theme.spacingMd }
|
|
|
|
// --- Browser-flow status ---------------------------------
|
|
// The browser may open in the background without stealing
|
|
// focus, and the code exchange takes a few seconds — the
|
|
// screen must narrate instead of sitting inert.
|
|
Column {
|
|
visible: QbzBridge.loginPhase === 1
|
|
width: parent.width
|
|
spacing: 4
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("Continue in your web browser — QBZ is waiting for you to finish signing in.")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("The browser may open in the background without taking focus — check your other windows.")
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Item { width: 1; height: 4 }
|
|
Item {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: cancelText.implicitWidth
|
|
height: cancelText.implicitHeight + 3
|
|
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 {
|
|
id: cancelArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzBridge.cancelLogin()
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
visible: QbzBridge.loginPhase === 2
|
|
width: parent.width
|
|
text: QbzBridge.tr("Signing you in…")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
// Sign-in failure box (phase 0 only).
|
|
Rectangle {
|
|
visible: QbzBridge.loginPhase === 0 && QbzBridge.loginError !== ""
|
|
width: parent.width
|
|
height: signInErrorColumn.implicitHeight + 24
|
|
color: theme.surfaceElevated
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
Column {
|
|
id: signInErrorColumn
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 12
|
|
spacing: 4
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("Sign-in failed")
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightMedium
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.loginError
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
Item { width: 1; height: 10 }
|
|
|
|
// --- Offline-boot callout (spec §4.1) --------------------
|
|
// No connectivity: point the user at the Start-offline link
|
|
// right below. Gated on has-previous-session too.
|
|
Column {
|
|
visible: QbzBridge.hasPreviousSession && QbzBridge.connectivity === 2
|
|
width: parent.width
|
|
spacing: 2
|
|
Rectangle {
|
|
width: parent.width
|
|
height: offlineColumn.implicitHeight + 24
|
|
color: theme.surfaceElevated
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
Column {
|
|
id: offlineColumn
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 12
|
|
spacing: 4
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("No internet connection — you can start in offline mode with your local library and downloads")
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Text {
|
|
visible: QbzBridge.captivePortal
|
|
width: parent.width
|
|
text: QbzBridge.tr("A network sign-in page may be blocking the connection")
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
// Caret pointing at the Start-offline link below.
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: "▼"
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
}
|
|
}
|
|
|
|
// Init-error variant of the same box (session restore failed
|
|
// for a non-connectivity reason). Hidden while the
|
|
// connectivity box shows — never two boxes at once.
|
|
Column {
|
|
visible: QbzBridge.restoreError !== "" && QbzBridge.connectivity !== 2
|
|
width: parent.width
|
|
spacing: 0
|
|
Rectangle {
|
|
width: parent.width
|
|
height: restoreColumn.implicitHeight + 24
|
|
color: theme.surfaceElevated
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
Column {
|
|
id: restoreColumn
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 12
|
|
spacing: 4
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("Could not restore your session")
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightMedium
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.restoreError
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
Item { width: 1; height: theme.spacingSm }
|
|
}
|
|
|
|
// Always visible (#553): without a previous session this
|
|
// opens the GUEST profile (user 0) — Local Library only,
|
|
// adopted by the account on the first real login.
|
|
Item {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: offlineText.implicitWidth
|
|
height: offlineText.implicitHeight + 3
|
|
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 {
|
|
id: offlineArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzBridge.startOffline()
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: theme.spacingXl }
|
|
|
|
// --- Legal disclaimer ------------------------------------
|
|
Text {
|
|
width: parent.width
|
|
text: QbzBridge.tr("QBZ requires an active Qobuz™ subscription. Your credentials are sent directly to Qobuz™.")
|
|
+ "\n"
|
|
+ QbzBridge.tr("QBZ can be used as an offline player without a Qobuz™ account (no access to the Qobuz™ library).")
|
|
+ "\n"
|
|
+ QbzBridge.tr("This application uses the Qobuz API but is not certified by Qobuz.")
|
|
+ " "
|
|
+ QbzBridge.tr("Qobuz™ is a trademark of Qobuz. QBZ is an open-source application licensed under the MIT License and is not affiliated with, endorsed by, or certified by Qobuz.")
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|