From 9ea290d58bad405a20f7a45767f8cd6a3e0411ab Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:03:36 -0600 Subject: [PATCH] feat(qt): the shared text modal fills the window, copies and scrolls visibly The artist biography (and the album, label and playlist descriptions that share this modal) sat in a 560 x 460 card with dense body text. The card is now at least 75% x 65% of the window, the body is selectable rich text one size up with 150% line height and spaced paragraphs, the scrollbar stays painted, and a copy button in the header puts the whole text on the clipboard with a check as the receipt. cc --- crates/qbz-qt/qml/shell/AppShell.qml | 110 ++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/crates/qbz-qt/qml/shell/AppShell.qml b/crates/qbz-qt/qml/shell/AppShell.qml index d2a2ee174..e2e5a84e5 100644 --- a/crates/qbz-qt/qml/shell/AppShell.qml +++ b/crates/qbz-qt/qml/shell/AppShell.qml @@ -687,6 +687,32 @@ Rectangle { textModalOpen = true } + // Bodies arrive as plain text with blank lines between paragraphs (the + // biography and review sources). Rendered as rich text so the line height + // can breathe — TextEdit has no lineHeight — and the paragraphs keep + // their gap; single newlines stay line breaks. + function textModalHtml(body) { + var esc = function (s) { + return String(s).replace(/&/g, "&").replace(//g, ">") + } + var paras = String(body || "").replace(/\r\n/g, "\n").split(/\n{2,}/) + var out = [] + for (var i = 0; i < paras.length; i++) { + var p = paras[i].trim() + if (p === "") continue + out.push("

" + + esc(p).replace(/\n/g, "
") + "

") + } + return out.join("") + } + property bool textModalCopied: false + Timer { + id: textModalCopiedTimer + interval: 1400 + onTriggered: root.textModalCopied = false + } + QbzClipboard { id: textModalClipboard } + Rectangle { visible: root.textModalOpen anchors.fill: parent @@ -699,8 +725,11 @@ Rectangle { } Rectangle { anchors.centerIn: parent - width: Math.min(root.width - 80, 560) - height: Math.min(root.height - 120, 460) + // At least 75% x 65% of the window (2026-09-13): the old 560 x 460 + // card crammed a biography into a column the reader had to squint + // at. Floors for small windows, ceilings that keep a margin. + width: Math.min(root.width - 32, Math.max(Math.round(root.width * 0.75), 560)) + height: Math.min(root.height - 40, Math.max(Math.round(root.height * 0.65), 400)) radius: theme.radiusMd color: theme.surfaceCard border.width: 1 @@ -712,23 +741,55 @@ Rectangle { } Column { anchors.fill: parent - anchors.margins: 24 - spacing: 14 + anchors.margins: 28 + spacing: 16 Row { width: parent.width + spacing: 8 Text { - width: parent.width - 28 + width: parent.width - 28 - 28 - 16 text: root.textModalTitle color: theme.textPrimary font.pixelSize: theme.fontHeading font.weight: theme.weightSemibold + elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter } + // Copy the whole body (plain text) to the clipboard; the + // glyph turns into a check for a moment as the receipt. + Rectangle { + width: 28 + height: 28 + color: tmCopyArea.containsMouse ? theme.surfaceHover : "transparent" + radius: 6 + anchors.verticalCenter: parent.verticalCenter + QbzIcon { + name: root.textModalCopied ? "check" : "copy" + width: 16 + height: 16 + anchors.centerIn: parent + tintName: root.textModalCopied ? "accent" + : (tmCopyArea.containsMouse ? "textPrimary" : "muted") + } + MouseArea { + id: tmCopyArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (textModalClipboard.copy(root.textModalBody)) { + root.textModalCopied = true + textModalCopiedTimer.restart() + } + } + } + } Rectangle { width: 28 height: 28 color: tmCloseArea.containsMouse ? theme.surfaceHover : "transparent" radius: 6 + anchors.verticalCenter: parent.verticalCenter QbzIcon { name: "x" width: 18 @@ -745,19 +806,34 @@ Rectangle { } } } - Flickable { + Item { width: parent.width - height: parent.height - 42 - clip: true - contentWidth: width - contentHeight: tmText.implicitHeight - Text { - id: tmText - width: parent.width - text: root.textModalBody - color: theme.textSecondary - font.pixelSize: theme.fontBody - wrapMode: Text.WordWrap + height: parent.height - 28 - 16 + Flickable { + id: tmFlick + anchors.fill: parent + anchors.rightMargin: 22 + clip: true + contentWidth: width + contentHeight: tmText.implicitHeight + boundsBehavior: Flickable.StopAtBounds + // Selectable, one size up from body text, paragraphs + // with room between them. + QbzSelectableText { + id: tmText + width: tmFlick.width + rich: true + text: root.textModalHtml(root.textModalBody) + color: theme.textPrimary + pixelSize: theme.fontBody + 1 + } + } + QbzScrollBar { + target: tmFlick + alwaysShown: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom } } }