fix(settings): harden migration and accessibility
Apply interface scaling through Qt at startup, complete keyboard navigation, move account migration into setup and progress modals, and defer Subsonic sync while the server scans. cx
This commit is contained in:
38 files changed
+2962
-275
No files matched your search
@@ -92,16 +92,35 @@ Column {
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
id: swatch
|
||||
width: 56
|
||||
height: 32
|
||||
radius: theme.radiusSm
|
||||
border.width: cell.open ? 2 : 1
|
||||
border.color: cell.open ? theme.accent : theme.borderSubtle
|
||||
border.width: cell.open || swatch.activeFocus ? 2 : 1
|
||||
border.color: cell.open || swatch.activeFocus
|
||||
? theme.accent : theme.borderSubtle
|
||||
color: root.tokenColor(cell.tokenKey)
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: cell.label
|
||||
Accessible.onPressAction: swatch.activate()
|
||||
function activate() {
|
||||
root.openToken = cell.open ? "" : cell.tokenKey
|
||||
}
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
swatch.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.openToken = cell.open ? "" : cell.tokenKey
|
||||
onPressed: swatch.forceActiveFocus()
|
||||
onClicked: swatch.activate()
|
||||
}
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -69,6 +69,9 @@ Item {
|
||||
property bool showManual: false
|
||||
|
||||
readonly property int openSeq: doc.openSeq || 0
|
||||
onVisibleChanged: if (visible) {
|
||||
Qt.callLater(function () { closeX.forceActiveFocus() })
|
||||
}
|
||||
onOpenSeqChanged: {
|
||||
root.step = 0
|
||||
root.welcomeConfirmed = false
|
||||
@@ -77,6 +80,13 @@ Item {
|
||||
root.reviewRestartDone = false
|
||||
root.showManual = false
|
||||
manualInput.text = ""
|
||||
if (root.visible)
|
||||
Qt.callLater(function () { closeX.forceActiveFocus() })
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: function (event) {
|
||||
QbzDacWizard.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
// Step labels for the header rail (index = step).
|
||||
@@ -116,12 +126,34 @@ Item {
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
height: 28
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.CheckBox
|
||||
Accessible.name: label
|
||||
Accessible.checked: checked
|
||||
Accessible.onToggleAction: toggled()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
checkRow.toggled()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusSm
|
||||
color: "transparent"
|
||||
border.width: checkRow.activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
}
|
||||
|
||||
QbzCheckbox {
|
||||
id: box
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: checkRow.checked
|
||||
enabled: false
|
||||
// DISPLAY-ONLY. The row MouseArea below is declared last, so it
|
||||
// sits on top and owns every click — exactly as the reference
|
||||
// shadows QbzCheckbox's internal toggle. No `onToggled` here on
|
||||
@@ -142,6 +174,7 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: checkRow.forceActiveFocus()
|
||||
onClicked: checkRow.toggled()
|
||||
}
|
||||
}
|
||||
@@ -231,6 +264,26 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 28
|
||||
height: 28
|
||||
activeFocusOnTab: root.visible
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Close", QbzSession.trRev)
|
||||
Accessible.onPressAction: QbzDacWizard.close()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
QbzDacWizard.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusSm
|
||||
color: "transparent"
|
||||
border.width: closeX.activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
}
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
width: 17
|
||||
@@ -243,6 +296,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: closeX.forceActiveFocus()
|
||||
onClicked: QbzDacWizard.close()
|
||||
}
|
||||
}
|
||||
@@ -535,9 +589,33 @@ Item {
|
||||
// Escape-hatch toggle (only when enumeration found devices;
|
||||
// otherwise the manual block shows unconditionally).
|
||||
Item {
|
||||
id: manualEntryToggle
|
||||
visible: root.doc.hasEnumeration === true
|
||||
width: parent.width
|
||||
height: visible ? 22 : 0
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: manualToggle.text
|
||||
Accessible.onPressAction: root.showManual = !root.showManual
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
root.showManual = !root.showManual
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: manualToggle.implicitWidth
|
||||
height: parent.height
|
||||
radius: theme.radiusSm
|
||||
color: "transparent"
|
||||
border.width: manualEntryToggle.activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
}
|
||||
Text {
|
||||
id: manualToggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -553,6 +631,7 @@ Item {
|
||||
anchors.bottom: parent.bottom
|
||||
width: manualToggle.implicitWidth
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: manualEntryToggle.forceActiveFocus()
|
||||
onClicked: root.showManual = !root.showManual
|
||||
}
|
||||
}
|
||||
@@ -667,12 +746,26 @@ Item {
|
||||
spacing: 8
|
||||
|
||||
Rectangle {
|
||||
id: configHeader
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: theme.radiusSm
|
||||
color: cfgHead.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: cfgCol.modelData.name || ""
|
||||
Accessible.onPressAction: QbzDacWizard.toggleConfig(cfgCol.index)
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
QbzDacWizard.toggleConfig(cfgCol.index)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
QbzIcon {
|
||||
id: cfgChevron
|
||||
@@ -701,6 +794,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: configHeader.forceActiveFocus()
|
||||
onClicked: QbzDacWizard.toggleConfig(cfgCol.index)
|
||||
}
|
||||
}
|
||||
@@ -776,6 +870,24 @@ Item {
|
||||
radius: theme.radiusSm
|
||||
color: (trackTa.containsMouse && testStep.playing)
|
||||
? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: testStep.playing
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: trackRow.modelData
|
||||
Accessible.onPressAction: {
|
||||
if (testStep.playing)
|
||||
QbzDacWizard.testPlayIndex(trackRow.index)
|
||||
}
|
||||
Keys.onPressed: function (event) {
|
||||
if (testStep.playing && !event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
QbzDacWizard.testPlayIndex(trackRow.index)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed glyph slot (width AND height) so the
|
||||
// ♪→▶ hover swap can never reflow the row.
|
||||
@@ -809,6 +921,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: testStep.playing ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onPressed: if (testStep.playing) trackRow.forceActiveFocus()
|
||||
onClicked: {
|
||||
if (testStep.playing)
|
||||
QbzDacWizard.testPlayIndex(trackRow.index)
|
||||
@@ -841,12 +954,26 @@ Item {
|
||||
// playing (the 4 test tracks or the user's own), which
|
||||
// is why they call the PLAYER, not the wizard.
|
||||
Rectangle {
|
||||
id: prevButton
|
||||
visible: testStep.playing
|
||||
width: visible ? 34 : 0
|
||||
height: 34
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Previous", QbzSession.trRev)
|
||||
Accessible.onPressAction: QbzPlayer.previous()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
QbzPlayer.previous()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
color: prevTa.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -860,16 +987,31 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: prevButton.forceActiveFocus()
|
||||
onClicked: QbzPlayer.previous()
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: nextButton
|
||||
visible: testStep.playing
|
||||
width: visible ? 34 : 0
|
||||
height: 34
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Next", QbzSession.trRev)
|
||||
Accessible.onPressAction: QbzPlayer.next()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
QbzPlayer.next()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
color: nextTa.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -883,6 +1025,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: nextButton.forceActiveFocus()
|
||||
onClicked: QbzPlayer.next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +120,29 @@ Column {
|
||||
visible: (section.rows || []).length > 0
|
||||
|
||||
Item {
|
||||
id: sectionHeader
|
||||
width: parent.width
|
||||
height: 28
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: section.title
|
||||
Accessible.onPressAction: section.open = !section.open
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
section.open = !section.open
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusSm
|
||||
color: "transparent"
|
||||
border.width: sectionHeader.activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: section.title
|
||||
@@ -140,6 +161,7 @@ Column {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: sectionHeader.forceActiveFocus()
|
||||
onClicked: section.open = !section.open
|
||||
}
|
||||
}
|
||||
@@ -180,8 +202,35 @@ Column {
|
||||
|
||||
// ------------------------------------------------------------- header
|
||||
Item {
|
||||
id: panelHeader
|
||||
width: parent.width
|
||||
height: 44
|
||||
activeFocusOnTab: visible && enabled
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: root.t("Diagnostics")
|
||||
Accessible.onPressAction: panelHeader.activate()
|
||||
function activate() {
|
||||
root.expanded = !root.expanded
|
||||
QbzShell.diagSetOpen(root.expanded)
|
||||
if (root.expanded && root.doc.loaded !== true && root.doc.loading !== true)
|
||||
QbzShell.diagRefresh()
|
||||
}
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
panelHeader.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusSm
|
||||
color: "transparent"
|
||||
border.width: panelHeader.activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
}
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
@@ -208,16 +257,8 @@ Column {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.expanded = !root.expanded
|
||||
// Gates the publisher: a collapsed panel is not reading, and a
|
||||
// publish can carry a hundred rows.
|
||||
QbzShell.diagSetOpen(root.expanded)
|
||||
// First expand collects; afterwards Refresh is explicit, 1:1
|
||||
// with DiagnosticsPanel.slint:201-203.
|
||||
if (root.expanded && root.doc.loaded !== true && root.doc.loading !== true)
|
||||
QbzShell.diagRefresh()
|
||||
}
|
||||
onPressed: panelHeader.forceActiveFocus()
|
||||
onClicked: panelHeader.activate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ Column {
|
||||
id: root
|
||||
|
||||
property var doc: ({})
|
||||
/// View-level migration setup surface. Kept nullable for isolated previews.
|
||||
property var migrationSetupModal: null
|
||||
readonly property var ie: doc.importExport || ({})
|
||||
|
||||
QbzTheme { id: theme }
|
||||
@@ -129,74 +131,13 @@ Column {
|
||||
SettingRow {
|
||||
label: QbzSession.tr("Migrate into this account", QbzSession.trRev)
|
||||
description: (root.ie.snapshots || []).length > 0
|
||||
? QbzSession.tr("Sign in with the account you are moving TO, then pick a snapshot below. Safe to repeat: a second run adds nothing.", QbzSession.trRev)
|
||||
? QbzSession.tr("Review the saved source profiles, choose what to copy, and confirm the signed-in destination account.", QbzSession.trRev)
|
||||
: QbzSession.tr("No snapshots yet. Sign in with the old account and create one first.", QbzSession.trRev)
|
||||
}
|
||||
// What the local half carries besides library, playlist folders, pinned
|
||||
// items, blacklist and preferences. Asked every time, all on by
|
||||
// default (owner decision 2026-09-02); session state, not persisted.
|
||||
property bool copyLocalProfile: true
|
||||
property bool copyMediaServers: true
|
||||
property bool copyScrobblers: true
|
||||
property bool copyListeningHistory: true
|
||||
SettingRow {
|
||||
label: QbzSession.tr("Also copy the local profile", QbzSession.trRev)
|
||||
description: QbzSession.tr("Library folders, playlist folders and order, pinned items, blacklist and preferences of the old account's QBZ profile on this computer.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyLocalProfile
|
||||
onToggled: function (v) { root.copyLocalProfile = v }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Media server connections", QbzSession.trRev)
|
||||
description: QbzSession.tr("Plex, Jellyfin and Subsonic settings, including their credentials.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyMediaServers
|
||||
onToggled: function (v) { root.copyMediaServers = v }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Scrobbler accounts", QbzSession.trRev)
|
||||
description: QbzSession.tr("Last.fm and ListenBrainz settings, including their credentials.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyScrobblers
|
||||
onToggled: function (v) { root.copyScrobblers = v }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Listening history", QbzSession.trRev)
|
||||
description: QbzSession.tr("The listen log and the events behind offline recommendations.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyListeningHistory
|
||||
onToggled: function (v) { root.copyListeningHistory = v }
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: root.ie.snapshots || []
|
||||
delegate: SettingRow {
|
||||
required property var modelData
|
||||
label: modelData.label
|
||||
description: QbzSession.tr("{} favorites · {} playlists · {} followed", QbzSession.trRev)
|
||||
.replace("{}", modelData.favorites)
|
||||
.replace("{}", modelData.playlists)
|
||||
.replace("{}", modelData.subscriptions)
|
||||
+ (modelData.isCurrentAccount
|
||||
? " · " + QbzSession.tr("this account", QbzSession.trRev)
|
||||
: "")
|
||||
SettingsButton {
|
||||
text: QbzSession.tr("Migrate…", QbzSession.trRev)
|
||||
enabled: !root.ie.migrationBusy && !modelData.isCurrentAccount
|
||||
onClicked: QbzBridge.settingsString("account-migrate", JSON.stringify({
|
||||
path: modelData.path,
|
||||
local_profile: root.copyLocalProfile,
|
||||
media_servers: root.copyMediaServers,
|
||||
scrobblers: root.copyScrobblers,
|
||||
listening_history: root.copyListeningHistory
|
||||
}))
|
||||
}
|
||||
SettingsButton {
|
||||
text: QbzSession.tr("Migrate…", QbzSession.trRev)
|
||||
enabled: !root.ie.migrationBusy
|
||||
&& (root.ie.snapshots || []).length > 0
|
||||
onClicked: if (root.migrationSetupModal) root.migrationSetupModal.open()
|
||||
}
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -134,11 +134,31 @@ Column {
|
||||
spacing: 8
|
||||
// Collapse chevron (only when the master toggle is on).
|
||||
Rectangle {
|
||||
id: collapseButton
|
||||
visible: root.doc.scrobbleEnabled === true
|
||||
width: 28
|
||||
height: 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).
|
||||
@@ -152,7 +172,8 @@ Column {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.settingsBool("scrobble-collapse", root.doc.scrobbleUiCollapsed !== true)
|
||||
onPressed: collapseButton.forceActiveFocus()
|
||||
onClicked: collapseButton.activate()
|
||||
}
|
||||
}
|
||||
QbzToggle {
|
||||
|
||||
@@ -63,7 +63,7 @@ Item {
|
||||
root.draftOverride = root.st.userOverrideNetwork === true
|
||||
root.draftFsIndex = root.st.fsTypeIndex || 0
|
||||
aliasField.text = root.draftAlias
|
||||
keyScope.forceActiveFocus()
|
||||
closeButton.forceActiveFocus()
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -81,12 +81,9 @@ Item {
|
||||
}))
|
||||
}
|
||||
|
||||
// Esc dismiss. No visual and no MouseArea, so it never intercepts a press
|
||||
// meant for the scrim (the QbzConfirmModal idiom).
|
||||
FocusScope {
|
||||
id: keyScope
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.close()
|
||||
Keys.onEscapePressed: function (event) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
// Scrim — click dismisses.
|
||||
@@ -149,12 +146,28 @@ Item {
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 28
|
||||
height: 28
|
||||
radius: theme.radiusSm
|
||||
color: closeArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: root.visible
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Close", QbzSession.trRev)
|
||||
Accessible.onPressAction: root.close()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "x"
|
||||
@@ -167,6 +180,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: closeButton.forceActiveFocus()
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
@@ -422,11 +436,27 @@ Item {
|
||||
// this same way (:218-232), including the
|
||||
// `accentGlyphColor` selector for the label.
|
||||
Rectangle {
|
||||
id: saveButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: saveLabel.implicitWidth + 36
|
||||
height: 36
|
||||
radius: theme.radiusSm
|
||||
color: saveArea.containsMouse ? theme.accentHover : theme.accent
|
||||
activeFocusOnTab: root.visible
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accentGlyphColor
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Save", QbzSession.trRev)
|
||||
Accessible.onPressAction: root.save()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
root.save()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: saveLabel
|
||||
anchors.centerIn: parent
|
||||
@@ -440,6 +470,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: saveButton.forceActiveFocus()
|
||||
onClicked: root.save()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ Item {
|
||||
function open() {
|
||||
root.draftOrder = root.storedOrder()
|
||||
root._open = true
|
||||
Qt.callLater(function () { closeButton.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function close() { root._open = false }
|
||||
@@ -74,6 +75,10 @@ Item {
|
||||
visible: root._open
|
||||
enabled: root._open
|
||||
z: 3100
|
||||
Keys.onEscapePressed: function (event) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@@ -122,12 +127,28 @@ Item {
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 6
|
||||
color: closeArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: root.opened
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Close", QbzSession.trRev)
|
||||
Accessible.onPressAction: root.close()
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "x"
|
||||
@@ -140,6 +161,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: closeButton.forceActiveFocus()
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
@@ -234,12 +256,30 @@ Item {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: resetRow.width + 24
|
||||
height: 34
|
||||
radius: theme.radiusSm
|
||||
color: resetArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
||||
activeFocusOnTab: root.opened
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: QbzSession.tr("Reset to defaults", QbzSession.trRev)
|
||||
Accessible.onPressAction: resetButton.activate()
|
||||
function activate() {
|
||||
root.draftOrder = root.defaultOrder.slice()
|
||||
QbzBridge.settingsString("local-tab-order-reset", "")
|
||||
}
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
resetButton.activate()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: resetRow
|
||||
anchors.centerIn: parent
|
||||
@@ -263,10 +303,8 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.draftOrder = root.defaultOrder.slice()
|
||||
QbzBridge.settingsString("local-tab-order-reset", "")
|
||||
}
|
||||
onPressed: resetButton.forceActiveFocus()
|
||||
onClicked: resetButton.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +322,20 @@ Item {
|
||||
opacity: reorderRoot.buttonEnabled ? 1.0 : 0.3
|
||||
color: reorderRoot.buttonEnabled && reorderArea.containsMouse
|
||||
? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: reorderRoot.buttonEnabled
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.onPressAction: if (reorderRoot.buttonEnabled) reorderRoot.clicked()
|
||||
Keys.onPressed: function (event) {
|
||||
if (reorderRoot.buttonEnabled && !event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
reorderRoot.clicked()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -298,6 +350,7 @@ Item {
|
||||
enabled: reorderRoot.buttonEnabled
|
||||
hoverEnabled: reorderRoot.buttonEnabled
|
||||
cursorShape: reorderRoot.buttonEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onPressed: reorderRoot.forceActiveFocus()
|
||||
onClicked: reorderRoot.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,12 +131,32 @@ Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
id: collapseButton
|
||||
visible: root.state.enabled === true
|
||||
width: 28
|
||||
height: 28
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: chevArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: visible && enabled
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: root.title
|
||||
Accessible.onPressAction: collapseButton.activate()
|
||||
function activate() {
|
||||
QbzBridge.settingsBool(
|
||||
root.server + "-collapse", root.state.collapsed !== 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
|
||||
name: root.state.collapsed === true ? "chevron-right" : "chevron-down"
|
||||
@@ -149,8 +169,8 @@ Column {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.settingsBool(
|
||||
root.server + "-collapse", root.state.collapsed !== true)
|
||||
onPressed: collapseButton.forceActiveFocus()
|
||||
onClicked: collapseButton.activate()
|
||||
}
|
||||
}
|
||||
QbzToggle {
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
// Full-view account-migration progress. The operation belongs to Rust and
|
||||
// keeps running if this panel is dismissed; this overlay only makes the
|
||||
// long-lived phases, account direction and elapsed time explicit.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var doc: ({})
|
||||
readonly property var ie: doc.importExport || ({})
|
||||
readonly property bool running: ie.migrationRunning === true
|
||||
readonly property bool succeeded: ie.migrationSucceeded === true
|
||||
readonly property int currentStep: Math.max(1, ie.migrationStep || 1)
|
||||
readonly property int stepTotal: Math.max(1, ie.migrationStepTotal || 6)
|
||||
readonly property int itemDone: Math.max(0, ie.migrationProgressDone || 0)
|
||||
readonly property int itemTotal: Math.max(0, ie.migrationProgressTotal || 0)
|
||||
readonly property real itemProgress: itemTotal > 0
|
||||
? Math.min(1, itemDone / itemTotal) : (running ? 0.08 : 0)
|
||||
readonly property real overallProgress: (!running && succeeded) ? 1
|
||||
: Math.max(0, Math.min(1,
|
||||
((currentStep - 1) + itemProgress) / stepTotal))
|
||||
|
||||
property bool openedForRun: false
|
||||
property bool dismissed: true
|
||||
property int elapsedSeconds: 0
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
function beginRun() {
|
||||
root.openedForRun = true
|
||||
root.dismissed = false
|
||||
root.elapsedSeconds = 0
|
||||
Qt.callLater(function () { closeButton.forceActiveFocus() })
|
||||
}
|
||||
function close() {
|
||||
root.dismissed = true
|
||||
root.restoreShellFocus()
|
||||
}
|
||||
function restoreShellFocus() {
|
||||
var item = root
|
||||
while (item.parent) {
|
||||
if (item.parent.isQbzShellRoot === true) {
|
||||
item.parent.forceActiveFocus()
|
||||
return
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
}
|
||||
function elapsedLabel() {
|
||||
const minutes = Math.floor(root.elapsedSeconds / 60)
|
||||
const seconds = root.elapsedSeconds % 60
|
||||
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds
|
||||
}
|
||||
function stepLabel(step) {
|
||||
switch (step) {
|
||||
case 1: return QbzSession.tr("Reading this account…", QbzSession.trRev)
|
||||
case 2: return QbzSession.tr("Adding favorites…", QbzSession.trRev)
|
||||
case 3: return QbzSession.tr("Adding playlists…", QbzSession.trRev)
|
||||
case 4: return QbzSession.tr("Following playlists…", QbzSession.trRev)
|
||||
case 5: return QbzSession.tr("Copying local profile…", QbzSession.trRev)
|
||||
default: return QbzSession.tr("Refreshing migrated data…", QbzSession.trRev)
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: if (running) beginRun()
|
||||
Component.onCompleted: if (running) beginRun()
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: root.running
|
||||
onTriggered: root.elapsedSeconds += 1
|
||||
}
|
||||
|
||||
visible: openedForRun && !dismissed
|
||||
enabled: visible
|
||||
z: 3200
|
||||
|
||||
Keys.onEscapePressed: function (event) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusMd
|
||||
color: "#bf000000"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.close()
|
||||
onWheel: function (wheel) { wheel.accepted = true }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width - 80, 620)
|
||||
height: Math.min(parent.height * 0.9, 680)
|
||||
radius: theme.radiusLg
|
||||
color: theme.surfaceCard
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onWheel: function (wheel) { wheel.accepted = true }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 68
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24
|
||||
anchors.right: headerSpinner.left
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("ACCOUNT MIGRATION", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontSection
|
||||
font.weight: theme.weightSemibold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QbzSpinner {
|
||||
id: headerSpinner
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 20
|
||||
visible: root.running
|
||||
spinning: root.running
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: headerDivider
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
Item {
|
||||
id: footer
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 70
|
||||
|
||||
SettingsButton {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("Close", QbzSession.trRev)
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: footerDivider
|
||||
anchors.bottom: footer.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
Flickable {
|
||||
anchors.top: headerDivider.bottom
|
||||
anchors.bottom: footerDivider.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: body.implicitHeight + 48
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: body
|
||||
x: 24
|
||||
y: 24
|
||||
width: parent.width - 48
|
||||
spacing: 16
|
||||
|
||||
WarningBanner {
|
||||
width: parent.width
|
||||
variant: "warning"
|
||||
title: QbzSession.tr("This only happens once. Please wait for it to finish and do not close the window.", QbzSession.trRev)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("You can close this panel; the migration will continue in the background.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: identityCol.implicitHeight + 24
|
||||
radius: theme.radiusMd
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
|
||||
Column {
|
||||
id: identityCol
|
||||
x: 12
|
||||
y: 12
|
||||
width: parent.width - 24
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("From", QbzSession.trRev)
|
||||
+ ": " + (root.ie.migrationSource || "—")
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("To", QbzSession.trRev)
|
||||
+ ": " + (root.ie.migrationTarget || "—")
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: theme.weightSemibold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 20
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: elapsed.right
|
||||
anchors.rightMargin: 12
|
||||
text: QbzSession.tr("Step {} of {}", QbzSession.trRev)
|
||||
.replace("{}", root.currentStep)
|
||||
.replace("{}", root.stepTotal)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Text {
|
||||
id: elapsed
|
||||
anchors.right: parent.right
|
||||
text: QbzSession.tr("Elapsed: {}", QbzSession.trRev)
|
||||
.replace("{}", root.elapsedLabel())
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 10
|
||||
radius: 5
|
||||
color: theme.borderSubtle
|
||||
clip: true
|
||||
Rectangle {
|
||||
width: parent.width * root.overallProgress
|
||||
height: parent.height
|
||||
radius: 5
|
||||
color: theme.accent
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.ie.migrationStatus || root.stepLabel(root.currentStep)
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.stepTotal
|
||||
delegate: Item {
|
||||
required property int index
|
||||
readonly property int number: index + 1
|
||||
readonly property bool complete: number < root.currentStep
|
||||
|| (!root.running && root.succeeded)
|
||||
readonly property bool active: number === root.currentStep
|
||||
&& root.running
|
||||
width: parent ? parent.width : 0
|
||||
height: 30
|
||||
|
||||
Rectangle {
|
||||
id: stepDot
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: parent.complete || parent.active
|
||||
? theme.accent : "transparent"
|
||||
border.width: parent.complete || parent.active ? 0 : 1
|
||||
border.color: theme.borderSubtle
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: stepDot.parent.complete ? "✓" : stepDot.parent.number
|
||||
color: stepDot.parent.complete || stepDot.parent.active
|
||||
? theme.onAccent : theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.left: stepDot.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.stepLabel(parent.number)
|
||||
color: parent.active ? theme.textPrimary : theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: parent.active
|
||||
? theme.weightSemibold : theme.weightRegular
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// Account-migration preparation surface. Selection and copy options live here
|
||||
// rather than in the Settings page so the user reviews one explicit
|
||||
// source -> authenticated destination before the long-running operation.
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var doc: ({})
|
||||
property var confirmHost: null
|
||||
readonly property var ie: doc.importExport || ({})
|
||||
readonly property var profiles: ie.snapshots || []
|
||||
readonly property var selectedProfile: {
|
||||
for (let i = 0; i < profiles.length; i++) {
|
||||
if (profiles[i].path === selectedPath)
|
||||
return profiles[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
property bool opened: false
|
||||
property string selectedPath: ""
|
||||
property bool copyLocalProfile: true
|
||||
property bool copyMediaServers: true
|
||||
property bool copyScrobblers: true
|
||||
property bool copyListeningHistory: true
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
function open() {
|
||||
root.selectedPath = ""
|
||||
root.copyLocalProfile = true
|
||||
root.copyMediaServers = true
|
||||
root.copyScrobblers = true
|
||||
root.copyListeningHistory = true
|
||||
root.opened = true
|
||||
Qt.callLater(function () { closeButton.forceActiveFocus() })
|
||||
}
|
||||
function close() {
|
||||
root.opened = false
|
||||
root.restoreShellFocus()
|
||||
}
|
||||
function restoreShellFocus() {
|
||||
var item = root
|
||||
while (item.parent) {
|
||||
if (item.parent.isQbzShellRoot === true) {
|
||||
item.parent.forceActiveFocus()
|
||||
return
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
}
|
||||
function askDelete(path) {
|
||||
if (!root.confirmHost)
|
||||
return
|
||||
root.confirmHost.ask(
|
||||
QbzSession.tr("Delete migration snapshot?", QbzSession.trRev),
|
||||
QbzSession.tr("This removes only the migration bundle. The old account's QBZ profile and user data are kept.", QbzSession.trRev),
|
||||
QbzSession.tr("Delete snapshot", QbzSession.trRev),
|
||||
function () {
|
||||
if (root.selectedPath === path)
|
||||
root.selectedPath = ""
|
||||
QbzBridge.settingsString("account-delete-snapshot", path)
|
||||
})
|
||||
}
|
||||
function startMigration() {
|
||||
if (!root.selectedProfile || root.ie.migrationBusy)
|
||||
return
|
||||
QbzBridge.settingsString("account-migrate", JSON.stringify({
|
||||
path: root.selectedProfile.path,
|
||||
local_profile: root.copyLocalProfile,
|
||||
media_servers: root.copyMediaServers,
|
||||
scrobblers: root.copyScrobblers,
|
||||
listening_history: root.copyListeningHistory
|
||||
}))
|
||||
root.close()
|
||||
}
|
||||
|
||||
visible: root.opened
|
||||
enabled: root.opened
|
||||
z: 3050
|
||||
|
||||
Keys.onEscapePressed: function (event) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusMd
|
||||
color: "#bf000000"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.close()
|
||||
onWheel: function (wheel) { wheel.accepted = true }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width - 80, 760)
|
||||
height: Math.min(parent.height - 48, 780)
|
||||
radius: theme.radiusLg
|
||||
color: theme.surfaceCard
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onWheel: function (wheel) { wheel.accepted = true }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 68
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("Migrate account data", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: headerDivider
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
Item {
|
||||
id: footer
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 70
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 10
|
||||
SettingsButton {
|
||||
id: closeButton
|
||||
minWidth: 100
|
||||
text: QbzSession.tr("Cancel", QbzSession.trRev)
|
||||
onClicked: root.close()
|
||||
}
|
||||
SettingsButton {
|
||||
minWidth: 160
|
||||
text: QbzSession.tr("Start migration", QbzSession.trRev)
|
||||
enabled: root.selectedProfile !== null && !root.ie.migrationBusy
|
||||
onClicked: root.startMigration()
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: footerDivider
|
||||
anchors.bottom: footer.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
anchors.top: headerDivider.bottom
|
||||
anchors.bottom: footerDivider.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: body.implicitHeight + 48
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Column {
|
||||
id: body
|
||||
x: 24
|
||||
y: 24
|
||||
width: parent.width - 48
|
||||
spacing: 12
|
||||
|
||||
WarningBanner {
|
||||
width: parent.width
|
||||
variant: "info"
|
||||
title: QbzSession.tr("What is a migration profile?", QbzSession.trRev)
|
||||
body: QbzSession.tr("It is a one-time bundle created from the source account. It preserves that account's Qobuz favorites and playlists so they can be added to the account signed in now. Optional local data is copied directly from the old QBZ profile on this computer. Deleting the bundle does not delete the old profile or either account's data.", QbzSession.trRev)
|
||||
}
|
||||
|
||||
GroupHeader { text: QbzSession.tr("SAVED SOURCE PROFILES", QbzSession.trRev) }
|
||||
|
||||
Text {
|
||||
visible: root.profiles.length === 0
|
||||
width: parent.width
|
||||
text: QbzSession.tr("No saved migration profiles are available.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontBody
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.profiles
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
readonly property bool selected:
|
||||
root.selectedPath === modelData.path
|
||||
width: body.width
|
||||
height: 92
|
||||
radius: theme.radiusMd
|
||||
color: selected ? theme.surfaceHover : theme.surfaceElevated
|
||||
border.width: selected ? 2 : 1
|
||||
border.color: selected ? theme.accent : theme.borderSubtle
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: actions.left
|
||||
anchors.rightMargin: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
Text {
|
||||
width: parent.width
|
||||
text: modelData.label
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: theme.weightMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("{} favorites · {} playlists · {} followed", QbzSession.trRev)
|
||||
.replace("{}", modelData.favorites)
|
||||
.replace("{}", modelData.playlists)
|
||||
.replace("{}", modelData.subscriptions)
|
||||
+ (modelData.isCurrentAccount
|
||||
? " · " + QbzSession.tr("this account", QbzSession.trRev)
|
||||
: "")
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: actions
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
SettingsButton {
|
||||
minWidth: 92
|
||||
text: parent.parent.selected
|
||||
? QbzSession.tr("Selected", QbzSession.trRev)
|
||||
: QbzSession.tr("Select", QbzSession.trRev)
|
||||
enabled: !modelData.isCurrentAccount
|
||||
&& !root.ie.migrationBusy
|
||||
onClicked: root.selectedPath = modelData.path
|
||||
}
|
||||
SettingsButton {
|
||||
minWidth: 92
|
||||
text: QbzSession.tr("Delete…", QbzSession.trRev)
|
||||
danger: true
|
||||
enabled: !root.ie.migrationBusy
|
||||
onClicked: root.askDelete(modelData.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader { text: QbzSession.tr("MIGRATION DIRECTION", QbzSession.trRev) }
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: direction.implicitHeight + 24
|
||||
radius: theme.radiusMd
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
Column {
|
||||
id: direction
|
||||
x: 12
|
||||
y: 12
|
||||
width: parent.width - 24
|
||||
spacing: 8
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("From", QbzSession.trRev) + ": "
|
||||
+ (root.selectedProfile
|
||||
? root.selectedProfile.sourceIdentity
|
||||
: QbzSession.tr("Select a source profile above", QbzSession.trRev))
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("To", QbzSession.trRev) + ": "
|
||||
+ QbzSession.tr("Signed-in Qobuz account · ID {}", QbzSession.trRev)
|
||||
.replace("{}", root.ie.currentUserId || "—")
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: theme.weightSemibold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader { text: QbzSession.tr("WHAT TO COPY", QbzSession.trRev) }
|
||||
SettingRow {
|
||||
label: QbzSession.tr("Also copy the local profile", QbzSession.trRev)
|
||||
description: QbzSession.tr("Library folders, playlist folders and order, pinned items, blacklist and preferences of the old account's QBZ profile on this computer.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyLocalProfile
|
||||
onToggled: function (value) { root.copyLocalProfile = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Media server connections", QbzSession.trRev)
|
||||
description: QbzSession.tr("Plex, Jellyfin and Subsonic settings, including their credentials.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyMediaServers
|
||||
enabled: root.copyLocalProfile
|
||||
onToggled: function (value) { root.copyMediaServers = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Scrobbler accounts", QbzSession.trRev)
|
||||
description: QbzSession.tr("Last.fm and ListenBrainz settings, including their credentials.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyScrobblers
|
||||
enabled: root.copyLocalProfile
|
||||
onToggled: function (value) { root.copyScrobblers = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
rowEnabled: root.copyLocalProfile
|
||||
label: QbzSession.tr("Listening history", QbzSession.trRev)
|
||||
description: QbzSession.tr("The listen log and the events behind offline recommendations.", QbzSession.trRev)
|
||||
QbzToggle {
|
||||
checked: root.copyListeningHistory
|
||||
enabled: root.copyLocalProfile
|
||||
onToggled: function (value) { root.copyListeningHistory = value }
|
||||
}
|
||||
}
|
||||
|
||||
WarningBanner {
|
||||
width: parent.width
|
||||
variant: "warning"
|
||||
title: QbzSession.tr("This only happens once. Please wait for it to finish and do not close the window.", QbzSession.trRev)
|
||||
body: QbzSession.tr("Migration is additive: it does not remove favorites or playlists from either account.", QbzSession.trRev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,12 +133,32 @@ Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
id: collapseButton
|
||||
visible: QbzLocal.plexEnabled
|
||||
width: 28
|
||||
height: 28
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: chevArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: visible && enabled
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: "Plex"
|
||||
Accessible.onPressAction: collapseButton.activate()
|
||||
function activate() {
|
||||
QbzBridge.settingsBool(
|
||||
"plex-collapse", root.plex.collapsed !== 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
|
||||
name: root.plex.collapsed === true ? "chevron-right" : "chevron-down"
|
||||
@@ -151,7 +171,8 @@ Column {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.settingsBool("plex-collapse", root.plex.collapsed !== true)
|
||||
onPressed: collapseButton.forceActiveFocus()
|
||||
onClicked: collapseButton.activate()
|
||||
}
|
||||
}
|
||||
QbzToggle {
|
||||
|
||||
@@ -24,6 +24,18 @@
|
||||
// republishes, and the rows re-render from the new document (the Slint
|
||||
// "single source of truth in SettingsState" pattern).
|
||||
//
|
||||
// KEYBOARD FOCUS CONTRACT (for every new setting): Qt Quick has no HTML-like
|
||||
// numeric tabindex; its tab index is the declaration order of enabled,
|
||||
// visible items with `activeFocusOnTab`. Therefore settings and their controls
|
||||
// MUST be declared in the same top-to-bottom order in which they are drawn and
|
||||
// MUST use the shared QbzToggle/QbzSelect/QbzSlider/QbzLineEdit/SettingsButton/
|
||||
// QbzCheckbox controls. Do not put a bare MouseArea on a new setting action:
|
||||
// the shared controls provide the focus ring and Enter/Space activation, and
|
||||
// accept Space so it cannot bubble into AppShell's global play/pause hotkey.
|
||||
// If a custom interactive item is genuinely necessary, it inherits this same
|
||||
// contract. This comment is intentionally at the composition root so both
|
||||
// contributors and coding agents encounter it when adding a settings row.
|
||||
//
|
||||
// The NavButtons row inside the 92px header is a 0px placeholder — nav
|
||||
// history lives in the global HeaderBar in this port, like every other view.
|
||||
|
||||
@@ -53,6 +65,8 @@ Item {
|
||||
// `root.section = n` would work once and then re-introduce the bug one
|
||||
// navigation later, in a way that looks fixed under casual testing.
|
||||
readonly property int section: QbzBridge.settingsSection
|
||||
readonly property bool migrationRunning:
|
||||
(doc.importExport || ({})).migrationRunning === true
|
||||
|
||||
readonly property bool sandboxed: {
|
||||
const m = (doc.dev || ({})).installMethod || ""
|
||||
@@ -74,8 +88,12 @@ Item {
|
||||
|
||||
// ============================ the view ================================
|
||||
Column {
|
||||
id: settingsContent
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
// A migration may be hidden, but its settings tree remains inert until
|
||||
// Rust finishes. Global navigation is outside this view and stays live.
|
||||
enabled: !root.migrationRunning
|
||||
|
||||
// --- Header (92px; NavButtons is a 0px placeholder in this port) --
|
||||
Item {
|
||||
@@ -114,6 +132,22 @@ Item {
|
||||
radius: theme.radiusSm
|
||||
color: active ? theme.surfaceElevated
|
||||
: snArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
activeFocusOnTab: visible && enabled
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: label
|
||||
Accessible.onPressAction: clicked()
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (!event.isAutoRepeat
|
||||
&& (event.key === Qt.Key_Space
|
||||
|| event.key === Qt.Key_Return
|
||||
|| event.key === Qt.Key_Enter)) {
|
||||
clicked()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
@@ -144,6 +178,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: parent.forceActiveFocus()
|
||||
onClicked: parent.clicked()
|
||||
}
|
||||
}
|
||||
@@ -260,6 +295,37 @@ Item {
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
// Tab can move into a control below the viewport. Reveal
|
||||
// that control as focus changes so keyboard navigation is
|
||||
// never technically correct but visually lost offscreen.
|
||||
function revealFocusItem(item) {
|
||||
if (!item)
|
||||
return
|
||||
var ancestor = item
|
||||
while (ancestor && ancestor !== panelCol)
|
||||
ancestor = ancestor.parent
|
||||
if (ancestor !== panelCol)
|
||||
return
|
||||
const point = item.mapToItem(flick.contentItem, 0, 0)
|
||||
const margin = 12
|
||||
if (point.y < flick.contentY + margin)
|
||||
flick.contentY = Math.max(0, point.y - margin)
|
||||
else if (point.y + item.height > flick.contentY + flick.height - margin)
|
||||
flick.contentY = Math.min(
|
||||
Math.max(0, flick.contentHeight - flick.height),
|
||||
point.y + item.height - flick.height + margin)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.Window.window
|
||||
function onActiveFocusItemChanged() {
|
||||
const win = root.Window.window
|
||||
Qt.callLater(function () {
|
||||
flick.revealFocusItem(win ? win.activeFocusItem : null)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: panelCol
|
||||
x: 20
|
||||
@@ -332,7 +398,7 @@ Item {
|
||||
OfflineSettings {
|
||||
width: parent.width
|
||||
doc: root.doc
|
||||
confirmHost: confirmHost
|
||||
confirmHost: settingsConfirmHost
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,7 +408,7 @@ Item {
|
||||
LocalLibrarySettings {
|
||||
width: parent.width
|
||||
doc: root.doc
|
||||
confirmHost: confirmHost
|
||||
confirmHost: settingsConfirmHost
|
||||
tabOrderModal: localTabsConfigModal
|
||||
}
|
||||
}
|
||||
@@ -362,7 +428,7 @@ Item {
|
||||
IntegrationsSettings {
|
||||
width: parent.width
|
||||
doc: root.doc
|
||||
confirmHost: confirmHost
|
||||
confirmHost: settingsConfirmHost
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,6 +438,7 @@ Item {
|
||||
ImportExportSettings {
|
||||
width: parent.width
|
||||
doc: root.doc
|
||||
migrationSetupModal: migrationSetupOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,7 +483,7 @@ Item {
|
||||
// declaration order alone puts it over every panel and the sub-nav.
|
||||
// Panels that need it are handed the reference (see LocalLibrarySettings
|
||||
// / PlexSettings `confirmHost`).
|
||||
SettingsConfirmHost { id: confirmHost }
|
||||
SettingsConfirmHost { id: settingsConfirmHost }
|
||||
|
||||
// App-wide Local Library order. Mounted at the view root so its scrim
|
||||
// covers the sub-navigation and the scrolled panel alike.
|
||||
@@ -440,4 +507,18 @@ Item {
|
||||
// area, and the reference is an overlay inside the app shell too, not a
|
||||
// separate window (DacWizardModal.slint:8-9).
|
||||
DacWizardModal { }
|
||||
|
||||
MigrationSetupModal {
|
||||
id: migrationSetupOverlay
|
||||
anchors.fill: parent
|
||||
doc: root.doc
|
||||
confirmHost: settingsConfirmHost
|
||||
}
|
||||
|
||||
// Mounted last so a long account migration has one unmistakable progress
|
||||
// surface over every Settings section. Closing it never cancels the task.
|
||||
MigrationProgressModal {
|
||||
anchors.fill: parent
|
||||
doc: root.doc
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user