Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus 22 shared opt-in hosts, shared navigation history with Back/Forward, Albums as the default library tab, native paged Local Library (artists/tracks), lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and artwork sized to the physical frame, static skeletons, 64px transport bar, Settings touch reflow and a persistent Immersive exit. Desktop keeps its prior behavior: every shared component defaults to the desktop path and only opts into Kiosk behavior through explicit properties (origin-relative ScrollMemory, viewport-bounded artwork requests). Regression coverage: kiosk navigation and artwork Node tests added to the runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n strings for all eight locales. Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and 1280x800 and a Desktop A/B with seven pixel-identical captures. cc
367 lines
18 KiB
QML
367 lines
18 KiB
QML
// Settings > Integrations — the QML port of
|
|
// crates/qbz-ui/ui/settings/IntegrationsSettings.slint. Row order, group
|
|
// headers and auth-gating mirror the Slint 1:1; all state rides the single
|
|
// settingsJson document (integrations_qt.rs over the SAME scrobbler /
|
|
// discover / ui_prefs / offline-queue stores the Slint app uses).
|
|
//
|
|
// Every integration here is STRICTLY OPT-IN: nothing connects, fires or
|
|
// queues until the user signs the service in and its toggles are on.
|
|
//
|
|
// NOTE: the Last.fm connect flow opens the system browser (open::that) — the
|
|
// offscreen smoke never clicks it.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Column {
|
|
property bool kioskHost: false
|
|
|
|
id: root
|
|
|
|
property var doc: ({})
|
|
// Password drafts never enter settingsJson. Keep the live token inside
|
|
// this component until the user explicitly submits it.
|
|
property string listenbrainzTokenDraft: ""
|
|
/// The view-level SettingsConfirmHost (SettingsView.qml), for the
|
|
/// destructive "Clear listening history" row. Null in previews.
|
|
property var confirmHost: null
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
spacing: 4
|
|
|
|
function submitListenBrainzToken() {
|
|
if (root.doc.listenbrainzBusy === true)
|
|
return
|
|
QbzBridge.settingsString("listenbrainz-token", root.listenbrainzTokenDraft)
|
|
}
|
|
|
|
// ======================= RECOMMENDATIONS =============================
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("RECOMMENDATIONS", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Show Recommendations in Discover", QbzSession.trRev)
|
|
description: QbzSession.tr("A personalized Discover tab built from your listening. For the best results connect Last.fm and ListenBrainz below — MusicBrainz is used automatically to match releases.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.showRecommendations === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("show-recommendations", v) }
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// =========================== METADATA ================================
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("METADATA", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("MusicBrainz", QbzSession.trRev)
|
|
description: QbzSession.tr("Enable artist relationships and enhanced metadata from MusicBrainz. No telemetry — it only matches releases to enrich artist pages and playlist song suggestions. Turn off to disable those sections.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.musicbrainzEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("musicbrainz", v) }
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// ============================ PRIVACY ================================
|
|
// The local listen log (qbz_app::listen_log): one row per play, on this
|
|
// disk only, per account. Default ON. "Clear" is DELETE + VACUUM. The
|
|
// toggle's state comes from the per-user store (`listen_meta.paused`),
|
|
// not from a pref, so it follows the account.
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("PRIVACY", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Listening history", QbzSession.trRev)
|
|
description: QbzSession.tr("Keep a local log of what you play: track, source, how much of it you heard and when. It stays on this machine, per account, and never leaves it unless you enable a scrobbler.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.listenHistoryEnabled !== false
|
|
onToggled: function (v) { QbzBridge.settingsBool("listen-history", v) }
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Clear listening history", QbzSession.trRev)
|
|
description: QbzSession.tr("Deletes every play recorded on this machine for this account. Scrobbles already sent are not affected.", QbzSession.trRev)
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
danger: true
|
|
text: QbzSession.tr("Clear", QbzSession.trRev)
|
|
onClicked: {
|
|
if (!root.confirmHost) {
|
|
QbzBridge.integrationsAction("listen-history-clear")
|
|
return
|
|
}
|
|
root.confirmHost.ask(
|
|
QbzSession.tr("Clear listening history", QbzSession.trRev),
|
|
QbzSession.tr("Deletes every play recorded on this machine for this account. Scrobbles already sent are not affected.", QbzSession.trRev),
|
|
QbzSession.tr("Clear", QbzSession.trRev),
|
|
function () { QbzBridge.integrationsAction("listen-history-clear") })
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// ========================== SCROBBLERS ===============================
|
|
// Master header row (toggle + collapse chevron), then the body gated
|
|
// on enabled && !collapsed (IntegrationsSettings.slint:113-169).
|
|
Item {
|
|
width: parent.width
|
|
height: root.kioskHost ? 112 : 64
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: scrobbleControl.left
|
|
anchors.rightMargin: 24
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 3
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("SCROBBLERS", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (11) * 1.2 : (11)
|
|
font.letterSpacing: 1.5
|
|
font.weight: theme.weightSemibold
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("Send your plays to Last.fm and ListenBrainz. Works for Qobuz, local, and Plex tracks. This switch also decides whether Recommendations may read your listening history from those services.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (12) * 1.2 : (12)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
Row {
|
|
id: scrobbleControl
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 8
|
|
// Collapse chevron (only when the master toggle is on).
|
|
Rectangle {
|
|
id: collapseButton
|
|
visible: root.doc.scrobbleEnabled === true
|
|
width: root.kioskHost ? 44 : 28
|
|
height: root.kioskHost ? 44 : 28
|
|
radius: theme.radiusSm
|
|
color: colArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
activeFocusOnTab: visible && enabled
|
|
border.width: activeFocus ? 2 : 0
|
|
border.color: theme.accent
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: QbzSession.tr("Scrobbling", QbzSession.trRev)
|
|
Accessible.onPressAction: collapseButton.activate()
|
|
function activate() {
|
|
QbzBridge.settingsBool(
|
|
"scrobble-collapse", root.doc.scrobbleUiCollapsed !== true)
|
|
}
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
collapseButton.activate()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
// 28px hit-box / 18px glyph (IntegrationsSettings.slint:129).
|
|
name: root.doc.scrobbleUiCollapsed === true ? "chevron-right" : "chevron-down"
|
|
width: 18
|
|
height: 18
|
|
tintName: "secondary"
|
|
}
|
|
MouseArea {
|
|
id: colArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: collapseButton.forceActiveFocus()
|
|
onClicked: collapseButton.activate()
|
|
}
|
|
}
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: root.doc.scrobbleEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("scrobble-enable", v) }
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
visible: root.doc.scrobbleEnabled === true && root.doc.scrobbleUiCollapsed !== true
|
|
width: parent.width
|
|
spacing: 4
|
|
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Scrobble while logged out", QbzSession.trRev)
|
|
description: QbzSession.tr("Continue sending plays to connected scrobblers without a Qobuz session.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.allowLoggedOutScrobbling !== false
|
|
onToggled: function (v) {
|
|
QbzBridge.settingsBool("scrobble-logged-out", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// ------------------------- LAST.FM ------------------------------
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("LAST.FM", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Scrobble to Last.fm", QbzSession.trRev)
|
|
description: root.doc.lastfmAuthed === true
|
|
? QbzSession.tr("Signed in as {}.", QbzSession.trRev).replace("{}", root.doc.lastfmUsername || "")
|
|
: QbzSession.tr("Connect your Last.fm account to enable scrobbling.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.lastfmEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("lastfm-enable", v) }
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
visible: root.doc.lastfmAuthed !== true
|
|
label: QbzSession.tr("Connect", QbzSession.trRev)
|
|
description: QbzSession.tr("Authorize QBZ in your browser, then click Finish.", QbzSession.trRev)
|
|
Row {
|
|
spacing: 8
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
text: root.doc.lastfmBusy === true ? QbzSession.tr("Working...", QbzSession.trRev) : QbzSession.tr("Connect Last.fm", QbzSession.trRev)
|
|
enabled: root.doc.lastfmBusy !== true
|
|
onClicked: QbzBridge.integrationsAction("lastfm-connect")
|
|
}
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
visible: (root.doc.lastfmAuthUrl || "") !== ""
|
|
text: QbzSession.tr("Open authorize page", QbzSession.trRev)
|
|
onClicked: QbzBridge.integrationsAction("lastfm-open-auth-url")
|
|
}
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
visible: (root.doc.lastfmAuthUrl || "") !== ""
|
|
text: root.doc.lastfmBusy === true
|
|
? QbzSession.tr("Working...", QbzSession.trRev)
|
|
: QbzSession.tr("Finish", QbzSession.trRev)
|
|
enabled: root.doc.lastfmBusy !== true
|
|
onClicked: QbzBridge.integrationsAction("lastfm-finish")
|
|
}
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
visible: root.doc.lastfmAuthed === true
|
|
label: QbzSession.tr("Disconnect Last.fm", QbzSession.trRev)
|
|
description: QbzSession.tr("Sign out of Last.fm.", QbzSession.trRev)
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
danger: true
|
|
text: QbzSession.tr("Disconnect", QbzSession.trRev)
|
|
onClicked: QbzBridge.integrationsAction("lastfm-disconnect")
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// ----------------------- LISTENBRAINZ ----------------------------
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("LISTENBRAINZ", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Scrobble to ListenBrainz", QbzSession.trRev)
|
|
description: root.doc.listenbrainzBusy === true
|
|
? QbzSession.tr("Validating...", QbzSession.trRev)
|
|
: root.doc.listenbrainzAuthed === true
|
|
? QbzSession.tr("Signed in as {}.", QbzSession.trRev).replace("{}", root.doc.listenbrainzUsername || "")
|
|
: QbzSession.tr("Paste your ListenBrainz user token to enable scrobbling.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.listenbrainzEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("listenbrainz-enable", v) }
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
visible: root.doc.listenbrainzAuthed !== true
|
|
label: QbzSession.tr("User token", QbzSession.trRev)
|
|
description: QbzSession.tr("From listenbrainz.org/settings.", QbzSession.trRev)
|
|
QbzLineEdit { kioskHost: root.kioskHost;
|
|
id: lbTokenField
|
|
isPassword: true
|
|
enabled: root.doc.listenbrainzBusy !== true
|
|
placeholder: QbzSession.tr("ListenBrainz token", QbzSession.trRev)
|
|
// Typing only updates the local draft. Enter submits once;
|
|
// focus loss merely settles the draft, so a button click can
|
|
// never race an implicit blur validation.
|
|
onEdited: function (v) { root.listenbrainzTokenDraft = v }
|
|
onCommitted: function (v) { root.listenbrainzTokenDraft = v }
|
|
onAccepted: function (v) {
|
|
root.listenbrainzTokenDraft = v
|
|
root.submitListenBrainzToken()
|
|
}
|
|
// The row hides once the sign-in lands; drop the typed token
|
|
// with it (Slint resets listenbrainz-token-input on success).
|
|
onVisibleChanged: if (!visible) {
|
|
text = ""
|
|
root.listenbrainzTokenDraft = ""
|
|
}
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
visible: root.doc.listenbrainzAuthed !== true
|
|
label: QbzSession.tr("Save token", QbzSession.trRev)
|
|
description: QbzSession.tr("Validates the token against ListenBrainz.", QbzSession.trRev)
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
text: root.doc.listenbrainzBusy === true
|
|
? QbzSession.tr("Validating...", QbzSession.trRev)
|
|
: QbzSession.tr("Connect ListenBrainz", QbzSession.trRev)
|
|
enabled: root.doc.listenbrainzBusy !== true
|
|
&& root.listenbrainzTokenDraft.trim().length > 0
|
|
onClicked: root.submitListenBrainzToken()
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
visible: root.doc.listenbrainzAuthed === true
|
|
label: QbzSession.tr("Disconnect ListenBrainz", QbzSession.trRev)
|
|
description: QbzSession.tr("Sign out of ListenBrainz.", QbzSession.trRev)
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
danger: true
|
|
text: QbzSession.tr("Disconnect", QbzSession.trRev)
|
|
onClicked: QbzBridge.integrationsAction("listenbrainz-disconnect")
|
|
}
|
|
}
|
|
|
|
// Status line (scrobble.rs set_status; kind 1 info / 2 ok / 3 error).
|
|
Text {
|
|
visible: (root.doc.integrationsStatusText || "") !== ""
|
|
width: parent.width
|
|
text: root.doc.integrationsStatusText || ""
|
|
color: root.doc.integrationsStatusKind === 2 ? theme.success
|
|
: root.doc.integrationsStatusKind === 3 ? theme.danger
|
|
: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (12) * 1.2 : (12)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
SettingsSpacer { }
|
|
|
|
// =========================== DISCORD =================================
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("DISCORD", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Discord Rich Presence", QbzSession.trRev)
|
|
description: QbzSession.tr("Show what you're listening to as your Discord status. Opt-in — Discord must be running.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.discordEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("discord-rpc", v) }
|
|
}
|
|
}
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Flatpak socket access", QbzSession.trRev)
|
|
description: QbzSession.tr("Flatpak install and the presence isn't showing? Grant access to Discord's IPC socket, then restart QBZ:\nflatpak override --user --filesystem=xdg-run/discord-ipc-0 com.blitzfc.qbz", QbzSession.trRev)
|
|
}
|
|
|
|
// ========================= QOBUZ LINKS ===============================
|
|
// The OS-level claim on the official client's `qobuzapp://` scheme, as a
|
|
// choice (link_handler_qt.rs): on Windows the official client is where
|
|
// most users still are, so it is opt-IN there; elsewhere it defaults on.
|
|
// `qbz://` (our own scheme) is always registered and is not this switch.
|
|
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("QOBUZ LINKS", QbzSession.trRev) }
|
|
SettingRow { kioskHost: root.kioskHost;
|
|
label: QbzSession.tr("Open Qobuz links in QBZ", QbzSession.trRev)
|
|
description: QbzSession.tr("Make QBZ the handler for qobuzapp:// links from your browser. Off, the official Qobuz app keeps them; QBZ's own qbz:// links always open here.", QbzSession.trRev)
|
|
QbzToggle { kioskHost: root.kioskHost;
|
|
checked: root.doc.qobuzLinksEnabled === true
|
|
onToggled: function (v) { QbzBridge.settingsBool("qobuz-links", v) }
|
|
}
|
|
}
|
|
}
|