merge: integrate the visual polish batch (text modal, selectable info modals, copy flyouts) into pre-release
cc
This commit is contained in:
18 files changed
+612
-189
No files matched your search
@@ -7639,3 +7639,11 @@ msgstr "Playlist löschen?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "„{}“ wird gelöscht. Das lässt sich nicht rückgängig machen."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Basisdaten kopieren"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Alles kopieren"
|
||||
@@ -3294,3 +3294,11 @@ msgstr "Delete"
|
||||
#: crates/qbz-qt/qml/settings/NavigationSettings.qml
|
||||
msgid "Always use the icon-only section navigation in the header. When off, the text tabs turn into icons only when they no longer fit next to the search box."
|
||||
msgstr "Always use the icon-only section navigation in the header. When off, the text tabs turn into icons only when they no longer fit next to the search box."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Copy basic data"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Copy full"
|
||||
@@ -7645,3 +7645,11 @@ msgstr "¿Eliminar la lista de reproducción?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "«{}» se eliminará. Esto no se puede deshacer."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Copiar datos básicos"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Copiar todo"
|
||||
@@ -7637,3 +7637,11 @@ msgstr "Supprimer la playlist ?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "« {} » sera supprimée. Cette action est irréversible."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Copier les données de base"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Tout copier"
|
||||
@@ -7620,3 +7620,11 @@ msgstr "プレイリストを削除しますか?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "「{}」を削除します。この操作は取り消せません。"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "基本情報をコピー"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "すべてコピー"
|
||||
@@ -7627,3 +7627,11 @@ msgstr "Afspeellijst verwijderen?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "“{}” wordt verwijderd. Dit kan niet ongedaan worden gemaakt."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Basisgegevens kopiëren"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Alles kopiëren"
|
||||
@@ -7637,3 +7637,11 @@ msgstr "Excluir a playlist?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "“{}” será excluída. Isso não pode ser desfeito."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Copiar dados básicos"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Copiar tudo"
|
||||
@@ -7658,3 +7658,11 @@ msgstr "Удалить плейлист?"
|
||||
#: qbz-qt/qml/controls/PlaylistDeleteConfirm.qml
|
||||
msgid "“{}” will be deleted. This cannot be undone."
|
||||
msgstr "«{}» будет удалён. Это действие нельзя отменить."
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy basic data"
|
||||
msgstr "Копировать основные данные"
|
||||
|
||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||
msgid "Copy full"
|
||||
msgstr "Копировать всё"
|
||||
@@ -575,6 +575,7 @@ fn main() {
|
||||
"qml/controls/PlaylistPickerModal.qml",
|
||||
"qml/controls/PmFolderIcon.qml",
|
||||
"qml/controls/QbzCircleAction.qml",
|
||||
"qml/controls/QbzClipboard.qml",
|
||||
"qml/controls/QbzColorPicker.qml",
|
||||
"qml/controls/QbzConfirmModal.qml",
|
||||
"qml/controls/QbzContextMenu.qml",
|
||||
@@ -597,6 +598,7 @@ fn main() {
|
||||
"qml/controls/ScopePanel.qml",
|
||||
"qml/controls/QbzSegToggle.qml",
|
||||
"qml/controls/QbzSelect.qml",
|
||||
"qml/controls/QbzSelectableText.qml",
|
||||
"qml/controls/QbzSplitButton.qml",
|
||||
"qml/controls/QbzSlider.qml",
|
||||
"qml/controls/QbzTabBar.qml",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// QbzClipboard — the one clipboard write QML offers without a C++ seam: an
|
||||
// off-screen TextEdit's copy(). Mount one where a copy action lives and call
|
||||
// `copy(text)`; the carrier is emptied right after, so the text never lingers
|
||||
// in an invisible editor. One primitive instead of the carriers CommandBlock,
|
||||
// SandboxSettings and the lyrics flyout each grew on their own.
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
visible: false
|
||||
width: 0
|
||||
height: 0
|
||||
|
||||
/// Put `text` on the system clipboard. False for an empty text.
|
||||
function copy(text) {
|
||||
var s = String(text || "")
|
||||
if (s === "")
|
||||
return false
|
||||
carrier.text = s
|
||||
carrier.selectAll()
|
||||
carrier.copy()
|
||||
carrier.deselect()
|
||||
carrier.text = ""
|
||||
return true
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: carrier
|
||||
visible: false
|
||||
width: 0
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// QbzSelectableText — a Text-shaped label the user can select and copy.
|
||||
//
|
||||
// Qt Quick's Text cannot be selected; a read-only TextEdit can, so the
|
||||
// default arm is one: mouse and keyboard selection, the right-click Copy
|
||||
// menu (QbzTextEditMenu) and a rich-text link for the fields that navigate
|
||||
// (artist, label, musician). The `raised` arm keeps a plain Text with the
|
||||
// Raised shadow style for the hosts that sit on the ambient field (the
|
||||
// immersive TrackInfoPanel): TextEdit has no `style`, and legibility there
|
||||
// beats selection. Same props on both arms, so a host switches with one flag.
|
||||
//
|
||||
// Sizing: hosts bind `width`; the height follows the laid-out text through
|
||||
// implicitHeight, exactly like the Text it replaces.
|
||||
|
||||
import QtQuick
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property color color: theme.textPrimary
|
||||
property int pixelSize: theme.fontBody
|
||||
property int weight: theme.weightRegular
|
||||
property real letterSpacing: 0
|
||||
property int wrapMode: Text.WordWrap
|
||||
property int horizontalAlignment: Text.AlignLeft
|
||||
/// `text` is already HTML (paragraph and line-height markup). Not
|
||||
/// combined with `linkHref`.
|
||||
property bool rich: false
|
||||
/// Plain Text with the Raised shadow style, no selection (ambient hosts).
|
||||
property bool raised: false
|
||||
property color raisedShadow: "#b0000000"
|
||||
/// When set, the whole text is one link: a click raises linkActivated
|
||||
/// with it, hovering paints linkHoverColor and the pointing cursor.
|
||||
property string linkHref: ""
|
||||
property color linkHoverColor: theme.accent
|
||||
signal linkActivated(string href)
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
implicitWidth: raised ? plain.implicitWidth : edit.implicitWidth
|
||||
implicitHeight: raised ? plain.implicitHeight : edit.implicitHeight
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, """)
|
||||
}
|
||||
readonly property bool linkHovered: root.linkHref !== ""
|
||||
&& (root.raised ? plainHover.hovered : editHover.hovered)
|
||||
readonly property color liveColor: linkHovered ? root.linkHoverColor : root.color
|
||||
|
||||
TextEdit {
|
||||
id: edit
|
||||
visible: !root.raised
|
||||
width: root.width
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
cursorVisible: false
|
||||
persistentSelection: false
|
||||
activeFocusOnTab: false
|
||||
textFormat: (root.linkHref !== "" || root.rich) ? TextEdit.RichText : TextEdit.PlainText
|
||||
// The link colour rides in the markup (TextEdit has no linkColor);
|
||||
// it is rebuilt only when the hover flips, not per mouse move.
|
||||
text: root.linkHref !== ""
|
||||
? "<a href=\"" + root.esc(root.linkHref) + "\" style=\"text-decoration:none; color:"
|
||||
+ root.liveColor + "\">" + root.esc(root.text) + "</a>"
|
||||
: root.text
|
||||
color: root.color
|
||||
font.pixelSize: root.pixelSize
|
||||
font.weight: root.weight
|
||||
font.letterSpacing: root.letterSpacing
|
||||
wrapMode: root.wrapMode
|
||||
horizontalAlignment: root.horizontalAlignment
|
||||
selectionColor: Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.35)
|
||||
selectedTextColor: theme.textPrimary
|
||||
onLinkActivated: function (link) { root.linkActivated(link) }
|
||||
HoverHandler {
|
||||
id: editHover
|
||||
cursorShape: (root.linkHref !== "" && edit.hoveredLink !== "")
|
||||
? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
QbzTextEditMenu { }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: plain
|
||||
visible: root.raised
|
||||
width: root.width
|
||||
text: root.text
|
||||
textFormat: root.rich ? Text.RichText : Text.PlainText
|
||||
color: root.liveColor
|
||||
style: Text.Raised
|
||||
styleColor: root.raisedShadow
|
||||
font.pixelSize: root.pixelSize
|
||||
font.weight: root.weight
|
||||
font.letterSpacing: root.letterSpacing
|
||||
wrapMode: root.wrapMode
|
||||
horizontalAlignment: root.horizontalAlignment
|
||||
HoverHandler {
|
||||
id: plainHover
|
||||
enabled: root.linkHref !== ""
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
enabled: root.linkHref !== ""
|
||||
onTapped: root.linkActivated(root.linkHref)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Popup {
|
||||
@@ -60,6 +61,64 @@ Popup {
|
||||
// that to the API's id: any divergence blanks this card with no log.
|
||||
property string requestedId: ""
|
||||
|
||||
// --- Copy (2026-09-13) --------------------------------------------------
|
||||
// Basic = "{Album} - {Artist}"; full = the header lines plus the ACTIVE
|
||||
// tab as plain text: every track with its performers (Credits) or the
|
||||
// review. The glyph turns into a check for a moment as the receipt.
|
||||
QbzClipboard { id: clipboard }
|
||||
property bool justCopied: false
|
||||
Timer {
|
||||
id: copiedTimer
|
||||
interval: 1400
|
||||
onTriggered: root.justCopied = false
|
||||
}
|
||||
function nonEmpty(s) { return (s || "") !== "" }
|
||||
function basicText() {
|
||||
return [root.doc.title, root.doc.artist].filter(root.nonEmpty).join(" - ")
|
||||
}
|
||||
function fullText() {
|
||||
var d = root.doc
|
||||
var lines = [d.title, d.artist].filter(root.nonEmpty)
|
||||
var meta = [d.label, d.releaseDate, d.metaLine, d.quality].filter(root.nonEmpty)
|
||||
if (meta.length > 0) lines.push(meta.join(" · "))
|
||||
if (root.activeTab === "review") {
|
||||
if (root.nonEmpty(d.review)) { lines.push(""); lines.push(d.review) }
|
||||
return lines.join("\n")
|
||||
}
|
||||
var tracks = d.tracks || []
|
||||
if (tracks.length > 0) lines.push("")
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
var tr = tracks[i]
|
||||
var num = (tr.number === undefined || tr.number === null) ? "" : String(tr.number)
|
||||
var head = (num !== "" ? num + ". " : "") + (tr.title || "")
|
||||
if (root.nonEmpty(tr.artist)) head += " — " + tr.artist
|
||||
lines.push(head)
|
||||
var perf = tr.performers || []
|
||||
for (var j = 0; j < perf.length; j++) {
|
||||
var p = perf[j]
|
||||
lines.push(" " + (p.name || "") + (root.nonEmpty(p.roles) ? ": " + p.roles : ""))
|
||||
}
|
||||
if (root.nonEmpty(tr.copyright)) lines.push(" " + tr.copyright)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
function copy(which) {
|
||||
var text = which === "full" ? root.fullText() : root.basicText()
|
||||
if (clipboard.copy(text)) {
|
||||
root.justCopied = true
|
||||
copiedTimer.restart()
|
||||
}
|
||||
}
|
||||
CardMenu {
|
||||
id: copyMenu
|
||||
menuWidth: 200
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Copy basic data", QbzSession.trRev), "icon": "copy", "action": "basic" },
|
||||
{ "label": QbzSession.tr("Copy full", QbzSession.trRev), "icon": "clipboard", "action": "full" }
|
||||
]
|
||||
onPicked: function (a) { root.copy(a) }
|
||||
}
|
||||
|
||||
// --- Actions ---------------------------------------------------------
|
||||
|
||||
/// Header info button. The view gates local/Plex albums (no button);
|
||||
@@ -220,26 +279,20 @@ Popup {
|
||||
spacing: 0
|
||||
topPadding: 4
|
||||
bottomPadding: 4
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
id: perfName
|
||||
text: modelData.name
|
||||
color: perfArea.containsMouse ? theme.accent : theme.textPrimary
|
||||
font.pixelSize: 13
|
||||
font.weight: theme.weightMedium
|
||||
MouseArea {
|
||||
id: perfArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.openMusician(modelData.name, modelData.primaryRole)
|
||||
}
|
||||
color: theme.textPrimary
|
||||
linkHref: "musician"
|
||||
pixelSize: 13
|
||||
weight: theme.weightMedium
|
||||
onLinkActivated: root.openMusician(modelData.name, modelData.primaryRole)
|
||||
}
|
||||
Text {
|
||||
width: parent.width - perfName.implicitWidth
|
||||
text: modelData.roles
|
||||
QbzSelectableText {
|
||||
width: Math.max(0, parent.width - perfName.width)
|
||||
text: String(modelData.roles || "")
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.WordWrap
|
||||
pixelSize: 13
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,23 +356,45 @@ Popup {
|
||||
spacing: 16
|
||||
Item { width: 8; height: 1 }
|
||||
Column {
|
||||
width: parent.width - 82
|
||||
width: parent.width - 130
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
Text {
|
||||
// Selectable (2026-09-13), like every field below.
|
||||
QbzSelectableText {
|
||||
width: parent.width
|
||||
text: root.doc.title || ""
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 16
|
||||
font.weight: theme.weightSemibold
|
||||
elide: Text.ElideRight
|
||||
pixelSize: 16
|
||||
weight: theme.weightSemibold
|
||||
}
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: parent.width
|
||||
text: root.doc.artist || ""
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
elide: Text.ElideRight
|
||||
pixelSize: theme.fontLegal
|
||||
}
|
||||
}
|
||||
// Copy: basic line or the active tab, from the flyout.
|
||||
Rectangle {
|
||||
id: copyBtn
|
||||
width: 32
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
QbzIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.justCopied ? "check" : "copy"
|
||||
width: 17
|
||||
height: 17
|
||||
tintName: root.justCopied ? "accent"
|
||||
: (copyArea.containsMouse ? "primary" : "muted")
|
||||
}
|
||||
MouseArea {
|
||||
id: copyArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyMenu.openBelowLeft(copyBtn)
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
@@ -376,43 +451,38 @@ Popup {
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 13
|
||||
}
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: parent.width
|
||||
text: root.doc.label || ""
|
||||
color: (labelArea.containsMouse && (root.doc.labelId || "") !== "")
|
||||
? theme.accent : theme.textPrimary
|
||||
font.pixelSize: 13
|
||||
font.weight: theme.weightSemibold
|
||||
MouseArea {
|
||||
id: labelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: (root.doc.labelId || "") !== ""
|
||||
? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.openLabel(root.doc.labelId || "")
|
||||
}
|
||||
color: theme.textPrimary
|
||||
linkHref: (root.doc.labelId || "") !== "" ? "label" : ""
|
||||
pixelSize: 13
|
||||
weight: theme.weightSemibold
|
||||
onLinkActivated: root.openLabel(root.doc.labelId || "")
|
||||
}
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: (root.doc.releaseDate || "") !== ""
|
||||
width: parent.width
|
||||
text: QbzSession.tr("on", QbzSession.trRev) + " " + (root.doc.releaseDate || "")
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 13
|
||||
pixelSize: 13
|
||||
}
|
||||
}
|
||||
Item { visible: (root.doc.label || "") !== ""; width: 1; height: 8 }
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: (root.doc.metaLine || "") !== ""
|
||||
width: parent.width
|
||||
text: root.doc.metaLine || ""
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.WordWrap
|
||||
pixelSize: 13
|
||||
}
|
||||
Item { visible: (root.doc.quality || "") !== ""; width: 1; height: 12 }
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: (root.doc.quality || "") !== ""
|
||||
width: parent.width
|
||||
text: root.doc.quality || ""
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,13 +572,12 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: root.activeTab === "review"
|
||||
width: parent.width
|
||||
text: root.doc.review || ""
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 14
|
||||
wrapMode: Text.WordWrap
|
||||
pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "<").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("<p style=\"line-height:150%; margin-top:0; margin-bottom:14px\">"
|
||||
+ esc(p).replace(/\n/g, "<br>") + "</p>")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// { "role": "PRODUCER", "roleRaw": "Producer", "names": ["A", "B"] }
|
||||
|
||||
import QtQuick
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
@@ -33,46 +34,34 @@ Column {
|
||||
width: colW
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: cc.colW
|
||||
text: cc.cell ? (cc.cell.role || "") : ""
|
||||
color: cc.overAmbient ? "#b3ffffff" : theme.textMuted
|
||||
style: cc.overAmbient ? Text.Raised : Text.Normal
|
||||
styleColor: "#b0000000"
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
wrapMode: Text.WordWrap
|
||||
raised: cc.overAmbient
|
||||
pixelSize: 11
|
||||
weight: theme.weightSemibold
|
||||
letterSpacing: 0.5
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: cc.cell ? (cc.cell.names || []) : []
|
||||
delegate: Item {
|
||||
// Selectable AND a link: a click lands on the musician, a drag
|
||||
// selects the name (QbzSelectableText, rich-text anchor).
|
||||
delegate: QbzSelectableText {
|
||||
id: nameRow
|
||||
required property string modelData
|
||||
width: cc.colW
|
||||
height: nameText.implicitHeight
|
||||
Text {
|
||||
id: nameText
|
||||
width: cc.colW
|
||||
text: nameRow.modelData
|
||||
color: nameArea.containsMouse ? cc.accentColor
|
||||
: (cc.overAmbient ? "#f2ffffff" : theme.textPrimary)
|
||||
style: cc.overAmbient ? Text.Raised : Text.Normal
|
||||
styleColor: "#b0000000"
|
||||
font.pixelSize: 14
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
MouseArea {
|
||||
id: nameArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: cc.nameClicked(nameRow.modelData,
|
||||
cc.cell ? (cc.cell.roleRaw || "") : "")
|
||||
}
|
||||
text: nameRow.modelData
|
||||
color: cc.overAmbient ? "#f2ffffff" : theme.textPrimary
|
||||
linkHref: "musician"
|
||||
linkHoverColor: cc.accentColor
|
||||
raised: cc.overAmbient
|
||||
pixelSize: 14
|
||||
onLinkActivated: cc.nameClicked(nameRow.modelData,
|
||||
cc.cell ? (cc.cell.roleRaw || "") : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// }
|
||||
|
||||
import QtQuick
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
@@ -28,15 +29,13 @@ Column {
|
||||
width: cellWidth
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: mc.cellWidth
|
||||
text: mc.label
|
||||
color: mc.overAmbient ? "#b3ffffff" : theme.textMuted
|
||||
style: mc.overAmbient ? Text.Raised : Text.Normal
|
||||
styleColor: "#b0000000"
|
||||
font.pixelSize: 11
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
elide: Text.ElideRight
|
||||
raised: mc.overAmbient
|
||||
pixelSize: 11
|
||||
weight: theme.weightSemibold
|
||||
letterSpacing: 0.5
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
@@ -42,6 +43,59 @@ Column {
|
||||
|
||||
readonly property var doc: host ? host.doc : ({})
|
||||
|
||||
// --- Copy (2026-09-13) -------------------------------------------------
|
||||
// Basic = "{Track} - {Album} - {Artist}"; full = everything the modal
|
||||
// shows, as plain text. The glyph turns into a check for a moment.
|
||||
QbzClipboard { id: clipboard }
|
||||
property bool justCopied: false
|
||||
Timer {
|
||||
id: copiedTimer
|
||||
interval: 1400
|
||||
onTriggered: body.justCopied = false
|
||||
}
|
||||
function nonEmpty(s) { return (s || "") !== "" }
|
||||
function basicText() {
|
||||
return [body.doc.title, body.doc.album, body.doc.artist].filter(body.nonEmpty).join(" - ")
|
||||
}
|
||||
function fullText() {
|
||||
var t = QbzSession.tr
|
||||
var r = QbzSession.trRev
|
||||
var d = body.doc
|
||||
var lines = [d.title, d.album, d.artist].filter(body.nonEmpty)
|
||||
var meta = []
|
||||
if (body.nonEmpty(d.duration)) meta.push(t("Duration", r) + ": " + d.duration)
|
||||
if (body.nonEmpty(d.quality)) meta.push(t("Quality", r) + ": " + d.quality)
|
||||
if (body.nonEmpty(d.isrc)) meta.push("ISRC: " + d.isrc)
|
||||
if (body.nonEmpty(d.label)) meta.push(t("Label", r) + ": " + d.label)
|
||||
if (meta.length > 0) { lines.push(""); lines = lines.concat(meta) }
|
||||
var credits = body.host ? body.host.credits : []
|
||||
if (credits.length > 0) {
|
||||
lines.push("")
|
||||
for (var i = 0; i < credits.length; i++) {
|
||||
var c = credits[i]
|
||||
lines.push((c.role || c.roleRaw || "") + ": " + (c.names || []).join(", "))
|
||||
}
|
||||
}
|
||||
if (body.nonEmpty(d.copyright)) { lines.push(""); lines.push(d.copyright) }
|
||||
return lines.join("\n")
|
||||
}
|
||||
function copy(which) {
|
||||
var text = which === "full" ? body.fullText() : body.basicText()
|
||||
if (clipboard.copy(text)) {
|
||||
body.justCopied = true
|
||||
copiedTimer.restart()
|
||||
}
|
||||
}
|
||||
CardMenu {
|
||||
id: copyMenu
|
||||
menuWidth: 200
|
||||
entries: [
|
||||
{ "label": QbzSession.tr("Copy basic data", QbzSession.trRev), "icon": "copy", "action": "basic" },
|
||||
{ "label": QbzSession.tr("Copy full", QbzSession.trRev), "icon": "clipboard", "action": "full" }
|
||||
]
|
||||
onPicked: function (a) { body.copy(a) }
|
||||
}
|
||||
|
||||
// ---- Loading ---------------------------------------------------------
|
||||
Item {
|
||||
visible: body.host && body.host.loading && body.host.errorText === ""
|
||||
@@ -115,53 +169,71 @@ Column {
|
||||
x: 24
|
||||
y: 16
|
||||
// 24 L + 24 R padding, 16 gap, 32 close X.
|
||||
width: Math.max(0, parent.width - 24 - 24 - 16 - 32)
|
||||
width: Math.max(0, parent.width - 24 - 24 - 16 - 32 - 8 - 32)
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
// Every field is selectable (2026-09-13): QbzSelectableText,
|
||||
// raised (plain, shadowed) on the ambient host.
|
||||
QbzSelectableText {
|
||||
width: parent.width
|
||||
text: body.doc.title || ""
|
||||
color: body.cPrimary
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
// The immersive panel reads at a distance — its header
|
||||
// steps up (owner, 2026-08-31 round 2).
|
||||
font.pixelSize: body.overAmbient ? 24 : 16
|
||||
font.weight: theme.weightSemibold
|
||||
wrapMode: Text.WordWrap
|
||||
pixelSize: body.overAmbient ? 24 : 16
|
||||
weight: theme.weightSemibold
|
||||
}
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: (body.doc.album || "") !== ""
|
||||
width: parent.width
|
||||
text: body.doc.album || ""
|
||||
color: body.cMuted
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: body.overAmbient ? 15 : theme.fontLegal
|
||||
elide: Text.ElideRight
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: body.overAmbient ? 15 : theme.fontLegal
|
||||
}
|
||||
// Artist — a link only when an id exists.
|
||||
Item {
|
||||
QbzSelectableText {
|
||||
visible: (body.doc.artist || "") !== ""
|
||||
width: parent.width
|
||||
height: visible ? artistText.implicitHeight : 0
|
||||
Text {
|
||||
id: artistText
|
||||
text: body.doc.artist || ""
|
||||
color: (body.doc.artistId || "") !== ""
|
||||
? body.cAccent : body.cPrimary
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: body.overAmbient ? 20 : 16
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
MouseArea {
|
||||
width: Math.min(artistText.implicitWidth, parent.width)
|
||||
height: artistText.implicitHeight
|
||||
cursorShape: (body.doc.artistId || "") !== ""
|
||||
? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: body.host.openArtist(body.doc.artistId || "")
|
||||
}
|
||||
text: body.doc.artist || ""
|
||||
color: (body.doc.artistId || "") !== "" ? body.cAccent : body.cPrimary
|
||||
linkHref: (body.doc.artistId || "") !== "" ? "artist" : ""
|
||||
linkHoverColor: body.cAccent
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: body.overAmbient ? 20 : 16
|
||||
weight: theme.weightSemibold
|
||||
onLinkActivated: body.host.openArtist(body.doc.artistId || "")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy — basic line or the full sheet, from the flyout; the
|
||||
// glyph turns into a check for a moment as the receipt.
|
||||
Item {
|
||||
id: copyBtn
|
||||
width: 32
|
||||
height: 32
|
||||
x: parent.width - 24 - 32 - 8 - 32
|
||||
y: 16
|
||||
QbzIcon {
|
||||
name: body.justCopied ? "check" : "copy"
|
||||
width: 17
|
||||
height: 17
|
||||
anchors.centerIn: parent
|
||||
tintName: body.justCopied ? "accent"
|
||||
: body.overAmbient
|
||||
? (copyArea.containsMouse ? "white" : "muted")
|
||||
: (copyArea.containsMouse ? "textPrimary" : "muted")
|
||||
}
|
||||
MouseArea {
|
||||
id: copyArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyMenu.openBelowLeft(copyBtn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,28 +283,26 @@ Column {
|
||||
cellWidth: loadedCol.metaColW
|
||||
overAmbient: body.overAmbient
|
||||
label: QbzSession.tr("Duration", QbzSession.trRev)
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: loadedCol.metaColW
|
||||
text: body.doc.duration || ""
|
||||
color: body.cPrimary
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: 14
|
||||
}
|
||||
}
|
||||
InfoMetaCell {
|
||||
cellWidth: loadedCol.metaColW
|
||||
overAmbient: body.overAmbient
|
||||
label: QbzSession.tr("Quality", QbzSession.trRev)
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: loadedCol.metaColW
|
||||
text: body.doc.quality || ""
|
||||
color: body.cPrimary
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: 14
|
||||
}
|
||||
}
|
||||
// ISRC — the cell is dropped (not blanked) when absent;
|
||||
@@ -242,14 +312,13 @@ Column {
|
||||
cellWidth: loadedCol.metaColW
|
||||
overAmbient: body.overAmbient
|
||||
label: "ISRC"
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
width: loadedCol.metaColW
|
||||
text: body.doc.isrc || ""
|
||||
color: body.cMuted
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: 14
|
||||
}
|
||||
}
|
||||
Item {
|
||||
@@ -271,30 +340,16 @@ Column {
|
||||
cellWidth: loadedCol.metaColW
|
||||
overAmbient: body.overAmbient
|
||||
label: QbzSession.tr("Label", QbzSession.trRev)
|
||||
Item {
|
||||
QbzSelectableText {
|
||||
width: loadedCol.metaColW
|
||||
height: labelText.implicitHeight
|
||||
Text {
|
||||
id: labelText
|
||||
width: loadedCol.metaColW
|
||||
text: body.doc.label || ""
|
||||
color: (labelArea.containsMouse
|
||||
&& (body.doc.labelId || "") !== "")
|
||||
? body.cAccent : body.cPrimary
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
MouseArea {
|
||||
id: labelArea
|
||||
width: Math.min(labelText.implicitWidth, loadedCol.metaColW)
|
||||
height: labelText.implicitHeight
|
||||
hoverEnabled: true
|
||||
cursorShape: (body.doc.labelId || "") !== ""
|
||||
? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: body.host.openLabel(body.doc.labelId || "")
|
||||
}
|
||||
text: body.doc.label || ""
|
||||
color: body.cPrimary
|
||||
linkHref: (body.doc.labelId || "") !== "" ? "label" : ""
|
||||
linkHoverColor: body.cAccent
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: 14
|
||||
onLinkActivated: body.host.openLabel(body.doc.labelId || "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,15 +424,14 @@ Column {
|
||||
width: 1
|
||||
height: 20
|
||||
}
|
||||
Text {
|
||||
QbzSelectableText {
|
||||
visible: (body.doc.copyright || "") !== ""
|
||||
width: parent.width
|
||||
text: body.doc.copyright || ""
|
||||
color: body.cMuted
|
||||
style: body.cStyle
|
||||
styleColor: body.cShadow
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
raised: body.overAmbient
|
||||
raisedShadow: body.cShadow
|
||||
pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ Item {
|
||||
readonly property real travel: Math.max(0, height - thumbH)
|
||||
// Shown while scrolling, hovering, or dragging (auto-hide).
|
||||
property bool scrollActive: false
|
||||
readonly property bool shown: scrollActive || barArea.containsMouse || barArea.pressed
|
||||
/// Keep the track and thumb painted while there is anything to scroll
|
||||
/// (the shared text modal: a long biography must read as scrollable).
|
||||
property bool alwaysShown: false
|
||||
readonly property bool shown: alwaysShown || scrollActive || barArea.containsMouse || barArea.pressed
|
||||
|
||||
Connections {
|
||||
target: root.target
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.blitzfc.qbz
|
||||
import "../cards"
|
||||
import "../controls"
|
||||
import "../rows"
|
||||
import "../shell"
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
@@ -865,6 +866,28 @@ Rectangle {
|
||||
// paint. Arrivals are coalesced into ONE rebind per frame — the same fix
|
||||
// LocalLibraryView carries, at O(n) instead of O(n²), with the covers
|
||||
// still appearing progressively (16ms granularity is invisible).
|
||||
// --- Track info for the Popular Tracks rows (2026-09-13) ---------------
|
||||
// ONE lazy modal per artist view (the full Popup tree must not exist per
|
||||
// row). Activated on demand, torn down on close (Qt.callLater so the
|
||||
// Popup is not destroyed mid-signal) — the same shape rows/TrackRow.qml
|
||||
// carries per row.
|
||||
Loader {
|
||||
id: artistTrackInfo
|
||||
active: false
|
||||
sourceComponent: TrackInfoModal { }
|
||||
}
|
||||
Connections {
|
||||
target: artistTrackInfo.item
|
||||
ignoreUnknownSignals: true
|
||||
function onClosed() { Qt.callLater(function () { artistTrackInfo.active = false }) }
|
||||
}
|
||||
function openTrackInfo(trackId) {
|
||||
if (!trackId || trackId === "")
|
||||
return
|
||||
artistTrackInfo.active = true
|
||||
artistTrackInfo.item.openFor(trackId)
|
||||
}
|
||||
|
||||
property var _coverInbox: ({})
|
||||
Timer {
|
||||
id: coverFlush
|
||||
@@ -1688,11 +1711,11 @@ Rectangle {
|
||||
// `controls/CardMenu.qml`, the same primitive rows/TrackRow.qml opens.
|
||||
//
|
||||
// ABSENT, not dead (the same discipline TrackRow applies): the radio
|
||||
// pair, Share Qobuz link / Song.link and Track info. The first two
|
||||
// need bridge seams that do not exist; the last needs the shared
|
||||
// row's lazy Loader (a TrackInfoModal), and duplicating those per
|
||||
// artist row is exactly the fork-drift this file already paid for
|
||||
// once. (The offline block IS wired — it needs no Loader.)
|
||||
// pair and Share Qobuz link / Song.link, which need bridge seams that
|
||||
// do not exist. Track info IS wired (2026-09-13) through ONE lazy
|
||||
// TrackInfoModal per artist view (`artistTrackInfo` below), not one
|
||||
// per row — the per-row Loader rows/TrackRow.qml carries is what this
|
||||
// comment used to refuse to duplicate.
|
||||
CardMenu {
|
||||
id: popMenu
|
||||
menuWidth: 224
|
||||
@@ -1749,6 +1772,8 @@ Rectangle {
|
||||
m.push({ "label": t("Go to album", r), "icon": "disc-3", "action": "go-album" })
|
||||
if ((popRow.row.artistId || "") !== "")
|
||||
m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
||||
if (!popRow.pulled)
|
||||
m.push({ "label": t("Track info", r), "icon": "info", "action": "track-info" })
|
||||
return m
|
||||
}
|
||||
onPicked: function (a) {
|
||||
@@ -1781,6 +1806,7 @@ Rectangle {
|
||||
else if (a === "recache") QbzPlayer.recacheTrack(id)
|
||||
else if (a === "go-album") QbzAlbum.openAlbum(popRow.row.albumId)
|
||||
else if (a === "go-artist") QbzArtist.openArtist(popRow.row.artistId)
|
||||
else if (a === "track-info") root.openTrackInfo(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2759,29 +2785,26 @@ Rectangle {
|
||||
onAction: function (id) { root.bulkAction(id) }
|
||||
}
|
||||
|
||||
// Air between the sticky JUMP TO bar's rule and the row.
|
||||
Item { visible: topTracks.length > 0; width: 1; height: 8 }
|
||||
Row {
|
||||
property string anchorId: "popular-tracks"
|
||||
visible: topTracks.length > 0
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
Text {
|
||||
width: parent.width - 44 - 32 - 32 - 3 * 12
|
||||
width: parent.width - 32 - 32 - 32 - 3 * 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("Popular Tracks", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
// ArtistPageView.slint:732-737 mounts the SHARED
|
||||
// CircleAction here — `primary: true` plus an explicit
|
||||
// `on-surface: true` with the .slint's own reason on the
|
||||
// line above it: "Plain page background (below the header
|
||||
// divider) — theme-aware variant so it reads on light
|
||||
// themes." The port hand-rolled a 44px accent disc
|
||||
// instead, which duplicated the control AND bypassed that
|
||||
// arm. `overlay` defaults false = the on-surface arm.
|
||||
// The SHARED CircleAction, in the same plain 32px outlined
|
||||
// form as its two siblings (2026-09-13): the filled 44px
|
||||
// accent disc the .slint carried here clashed with them
|
||||
// and butted the JUMP TO bar's rule.
|
||||
QbzCircleAction {
|
||||
primary: true
|
||||
name: "play-fill"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzPlayer.playArtistTop(false)
|
||||
|
||||
Reference in new issue
Block a user