Files
qbz/crates/qbz-qt/qml/controls/LinkResolverModal.qml
T
vicrodh 0e76c1f486 fix(qt): preserve navigation shortcuts and release text input focus
Keep search, settings and link shortcuts usable while editing, dismiss focus on outside clicks and navigation, and guard deferred focus against hidden inputs. Add Qt Quick regression coverage.

Refs #753

cx
2026-09-08 21:40:28 -06:00

233 lines
8.2 KiB
QML

// App-wide music-link resolver. QbzLink owns state and async resolution;
// this file is only the modal surface and survives whichever menu/hotkey
// opened it.
import QtQuick
import QtQuick.Window
import com.blitzfc.qbz
import "../theme"
Item {
id: root
visible: QbzLink.modalOpen
QbzTheme { id: theme }
Keys.onEscapePressed: function(event) {
QbzLink.close()
event.accepted = true
}
Connections {
target: QbzLink
function onFocusRequested() { urlInput.focusField() }
}
onVisibleChanged: {
if (visible) {
urlInput.focusField()
} else {
// Return focus only if it still belongs to this modal. A search
// shortcut may already have focused the header before close arrives.
var window = root.Window.window
var item = window ? window.activeFocusItem : null
while (item && item !== root)
item = item.parent
if (item === root) {
var shell = root.parent
while (shell && shell.isQbzShellRoot !== true)
shell = shell.parent
if (shell)
shell.forceActiveFocus()
}
}
}
Rectangle {
anchors.fill: parent
color: "#bf000000"
MouseArea {
anchors.fill: parent
onClicked: QbzLink.close()
// Wheel-lock (the DiscoverConfigModal rule).
onWheel: function (wheel) { wheel.accepted = true }
}
}
Rectangle {
id: card
anchors.centerIn: parent
width: Math.min(parent.width - 80, 520)
height: panel.implicitHeight + 48
radius: theme.radiusMd
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
MouseArea {
anchors.fill: parent
// Wheel-lock (the DiscoverConfigModal rule).
onWheel: function (wheel) { wheel.accepted = true }
}
Column {
id: panel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 24
spacing: 16
Item {
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.right: closeButton.left
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Open Music Link", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: theme.fontHeading
font.weight: theme.weightSemibold
elide: Text.ElideRight
}
Item {
id: closeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 17
height: 17
tintName: closeArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzLink.close()
}
}
}
Row {
width: parent.width
height: 40
spacing: 8
Item {
width: 36
height: 40
Image {
anchors.centerIn: parent
width: 24
height: 24
fillMode: Image.PreserveAspectFit
visible: QbzLink.platform === "qobuz"
|| QbzLink.platform === "spotify"
|| QbzLink.platform === "apple"
|| QbzLink.platform === "deezer"
source: QbzLink.platform === "qobuz"
? "../assets/brand/qobuz-logo-filled.svg"
: QbzLink.platform === "spotify"
? "../assets/brand/spotify-logo.svg"
: QbzLink.platform === "apple"
? "../assets/brand/apple-music-logo.svg"
: "../assets/brand/deezer-logo.svg"
}
QbzIcon {
anchors.centerIn: parent
width: 24
height: 24
visible: QbzLink.platform === "tidal"
name: "tidal-tidal"
tintName: "accent"
}
QbzIcon {
anchors.centerIn: parent
width: 22
height: 22
visible: QbzLink.platform !== "qobuz"
&& QbzLink.platform !== "spotify"
&& QbzLink.platform !== "apple"
&& QbzLink.platform !== "deezer"
&& QbzLink.platform !== "tidal"
name: "link"
tintName: "muted"
}
}
QbzLineEdit {
id: urlInput
width: parent.width - 36 - goButton.width - 2 * parent.spacing
height: 40
text: QbzLink.url
placeholder: QbzSession.tr("Paste a Qobuz, Spotify, Apple Music, Tidal, Deezer or song.link URL", QbzSession.trRev)
enabled: !QbzLink.resolving
opacity: enabled ? 1.0 : 0.6
onEdited: function(value) { QbzLink.urlEdited(value) }
onAccepted: function(value) { QbzLink.submit(value) }
}
QbzPrimaryButton {
id: goButton
anchors.verticalCenter: parent.verticalCenter
label: QbzLink.resolving
? QbzSession.tr("Resolving…", QbzSession.trRev)
: QbzSession.tr("Go", QbzSession.trRev)
btnHeight: 40
labelSize: theme.fontBody
btnEnabled: QbzLink.url.trim() !== "" && !QbzLink.resolving
onClicked: QbzLink.submit(QbzLink.url)
}
}
Text {
width: parent.width
visible: QbzLink.error !== ""
text: QbzLink.error
color: theme.danger
font.pixelSize: theme.fontLegal
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
height: playlistColumn.implicitHeight + 24
visible: QbzLink.playlistDetected
radius: theme.radiusSm
color: theme.surfaceElevated
border.width: 1
border.color: theme.borderSubtle
Column {
id: playlistColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 12
spacing: 10
Text {
width: parent.width
text: QbzSession.tr("This looks like a playlist.", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: theme.fontLegal
wrapMode: Text.WordWrap
}
QbzPrimaryButton {
label: QbzSession.tr("Open Playlist Importer", QbzSession.trRev)
btnHeight: 34
labelSize: theme.fontLegal
onClicked: QbzLink.openImporter()
}
}
}
}
}
}