Files
qbz/crates/qbz-qt/qml/settings/MigrationSetupModal.qml
T
vicrodh b1dba198c9 feat(qt): kiosk profile release hardening
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
2026-09-07 08:23:11 -06:00

370 lines
16 KiB
QML

// 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 {
property bool kioskHost: false
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: root.kioskHost ? (theme.fontHeading) * 1.2 : (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 { kioskHost: root.kioskHost;
id: closeButton
minWidth: 100
text: QbzSession.tr("Cancel", QbzSession.trRev)
onClicked: root.close()
}
SettingsButton { kioskHost: root.kioskHost;
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 { kioskHost: root.kioskHost; 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: root.kioskHost ? (theme.fontBody) * 1.2 : (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: root.kioskHost ? (theme.fontBody) * 1.2 : (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: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
elide: Text.ElideRight
}
}
Row {
id: actions
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton { kioskHost: root.kioskHost;
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 { kioskHost: root.kioskHost;
minWidth: 92
text: QbzSession.tr("Delete…", QbzSession.trRev)
danger: true
enabled: !root.ie.migrationBusy
onClicked: root.askDelete(modelData.path)
}
}
}
}
GroupHeader { kioskHost: root.kioskHost; 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: root.kioskHost ? (theme.fontBody) * 1.2 : (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: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
font.weight: theme.weightSemibold
wrapMode: Text.WordWrap
}
}
}
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("WHAT TO COPY", QbzSession.trRev) }
SettingRow { kioskHost: root.kioskHost;
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 { kioskHost: root.kioskHost;
checked: root.copyLocalProfile
onToggled: function (value) { root.copyLocalProfile = value }
}
}
SettingRow { kioskHost: root.kioskHost;
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 { kioskHost: root.kioskHost;
checked: root.copyMediaServers
enabled: root.copyLocalProfile
onToggled: function (value) { root.copyMediaServers = value }
}
}
SettingRow { kioskHost: root.kioskHost;
rowEnabled: root.copyLocalProfile
label: QbzSession.tr("Scrobbler accounts", QbzSession.trRev)
description: QbzSession.tr("Last.fm and ListenBrainz settings, including their credentials.", QbzSession.trRev)
QbzToggle { kioskHost: root.kioskHost;
checked: root.copyScrobblers
enabled: root.copyLocalProfile
onToggled: function (value) { root.copyScrobblers = value }
}
}
SettingRow { kioskHost: root.kioskHost;
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 { kioskHost: root.kioskHost;
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)
}
}
}
}
}