fix: stabilize playback memory, local library sync and Qt settings
Add shared playback memory profiles, bounded disk-backed playback and storage selection for desktop and daemon. Preserve gapless during streaming-only playback and provide explicit cache eviction with playback confirmation. Stop watcher read events and unchanged scans from rebuilding the local catalog. Restore playlist/sidebar updates, folder availability feedback and player layout. Add appearance presets and a radio-based cache settings panel with translated trade-offs, plus consistent clipboard menus across visible Qt text editors. Validate with Rust regressions, QML interaction tests, isolated release startup and idle-library checks; extend the shared local and CI gates. cx
No files matched your search
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#4285f4"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#ef4444"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#888888"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#cccccc"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fbbf24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -230,6 +230,8 @@ Item {
|
||||
border.color: searchInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
@@ -413,6 +415,8 @@ Item {
|
||||
border.color: nameInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: nameInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -197,6 +197,8 @@ Item {
|
||||
border.color: queryInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: queryInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -243,6 +243,7 @@ Item {
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
width: parent.width - 22
|
||||
height: parent.height
|
||||
|
||||
@@ -208,6 +208,7 @@ Popup {
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
TextEdit {
|
||||
QbzTextEditMenu { }
|
||||
id: factValue
|
||||
x: 154
|
||||
width: parent.width - x
|
||||
@@ -256,6 +257,7 @@ Popup {
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
TextEdit {
|
||||
QbzTextEditMenu { }
|
||||
x: 12
|
||||
y: 28
|
||||
width: parent.width - 92
|
||||
|
||||
@@ -339,6 +339,8 @@ Item {
|
||||
opacity: root.loading ? 0.5 : 1.0
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: urlInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
@@ -542,6 +544,8 @@ Item {
|
||||
opacity: root.loading ? 0.5 : 1.0
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: serviceInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
@@ -667,6 +671,8 @@ Item {
|
||||
opacity: nameInput.enabled ? 1.0 : 0.5
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: nameInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -258,6 +258,8 @@ Item {
|
||||
border.color: searchInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
@@ -359,6 +361,7 @@ Item {
|
||||
border.width: 1
|
||||
border.color: nameInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: nameInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Thin transport rail. The thumb extends outside the 3px layout slot; the
|
||||
// shell stacks its transport above the content and the time bubble uses Overlay.
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
height: 3
|
||||
property bool hasTrack: false
|
||||
property real progress: 0
|
||||
property real cacheProgress: 0
|
||||
property real seekableMax: 1
|
||||
property real durationSecs: 0
|
||||
signal seekRequested(real fraction)
|
||||
function clamp01(value) { return Math.min(Math.max(value, 0), 1) }
|
||||
function fmt(secs) {
|
||||
var whole = Math.floor(Math.max(0, secs))
|
||||
return Math.floor(whole / 60) + ":" + (whole % 60 < 10 ? "0" : "") + whole % 60
|
||||
}
|
||||
QbzTheme { id: theme }
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.fill: parent
|
||||
radius: 2
|
||||
color: theme.surfaceElevated
|
||||
Rectangle {
|
||||
width: parent.width * root.clamp01(root.cacheProgress)
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: Qt.rgba(theme.textMuted.r, theme.textMuted.g, theme.textMuted.b, 0.35)
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width * root.clamp01(root.progress)
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: theme.accent
|
||||
}
|
||||
Rectangle {
|
||||
objectName: "compactSeekThumb"
|
||||
width: 12; height: 12; radius: 6
|
||||
color: theme.textPrimary
|
||||
x: parent.width * root.clamp01(root.progress) - width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: area.containsMouse && root.hasTrack
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: area
|
||||
objectName: "compactSeekArea"
|
||||
width: parent.width
|
||||
height: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hoverEnabled: true
|
||||
enabled: root.hasTrack
|
||||
cursorShape: root.clamp01(mouseX / width) > root.seekableMax
|
||||
? Qt.ForbiddenCursor : Qt.PointingHandCursor
|
||||
onClicked: root.seekRequested(Math.min(root.clamp01(mouseX / width), root.seekableMax))
|
||||
}
|
||||
Item {
|
||||
id: tip
|
||||
objectName: "compactSeekTip"
|
||||
parent: Overlay.overlay
|
||||
z: 10000
|
||||
visible: root.visible && root.hasTrack && area.containsMouse
|
||||
readonly property real fraction: root.clamp01(area.mouseX / Math.max(1, area.width))
|
||||
// Explicit geometry dependencies also reposition a stationary tooltip
|
||||
// when the window resizes; mapping alone does not notify QML bindings.
|
||||
readonly property point anchor: {
|
||||
var geometry = root.x + root.y + root.width + root.height
|
||||
var windowHeight = parent ? parent.height : 0
|
||||
return parent ? root.mapToItem(parent, root.width * fraction, 0) : Qt.point(0, 0)
|
||||
}
|
||||
width: bubble.width
|
||||
height: bubble.height + 9
|
||||
x: Math.max(4, Math.min((parent ? parent.width : width) - width - 4, anchor.x - width / 2))
|
||||
y: anchor.y - height + 3
|
||||
Rectangle {
|
||||
id: bubble
|
||||
width: label.implicitWidth + 14
|
||||
height: 18
|
||||
radius: 4
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
Text {
|
||||
id: label
|
||||
objectName: "compactSeekTime"
|
||||
anchors.centerIn: parent
|
||||
text: root.fmt(tip.fraction * root.durationSecs)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: 9; height: 9
|
||||
rotation: 45
|
||||
color: theme.surfaceElevated
|
||||
anchors.horizontalCenter: bubble.horizontalCenter
|
||||
y: bubble.height - height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +224,7 @@ Rectangle {
|
||||
// No clip: TextInput scissors itself (:207) and the
|
||||
// placeholder elides. A redundant second scissor.
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -22,26 +22,38 @@ Item {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property bool kioskHost: false
|
||||
property bool selected: false
|
||||
signal clicked()
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
implicitWidth: row.implicitWidth
|
||||
implicitHeight: Math.max(18, lbl.implicitHeight)
|
||||
implicitWidth: lbl.implicitWidth + 24
|
||||
implicitHeight: Math.max(root.kioskHost ? 44 : 18, row.implicitHeight)
|
||||
activeFocusOnTab: enabled
|
||||
Accessible.role: Accessible.RadioButton
|
||||
Accessible.name: label
|
||||
Accessible.description: description
|
||||
Accessible.checked: selected
|
||||
Accessible.onPressAction: if (enabled) clicked()
|
||||
Keys.onSpacePressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
Keys.onEnterPressed: clicked()
|
||||
|
||||
Row {
|
||||
id: row
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
y: Math.max(0, (lbl.height - height) / 2)
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
border.width: root.selected ? 0 : 1.5
|
||||
border.color: theme.textMuted
|
||||
border.width: root.selected && !root.activeFocus ? 0 : 1.5
|
||||
border.color: root.activeFocus ? theme.accent : theme.textMuted
|
||||
color: root.selected ? theme.accent : "transparent"
|
||||
Rectangle {
|
||||
visible: root.selected
|
||||
@@ -52,19 +64,32 @@ Item {
|
||||
color: theme.accentGlyphColor
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: lbl
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.label
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Column {
|
||||
width: parent.width - 24
|
||||
spacing: 4
|
||||
Text {
|
||||
id: lbl
|
||||
width: parent.width
|
||||
text: root.label
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
visible: root.description !== ""
|
||||
width: parent.width
|
||||
text: root.description
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
onClicked: { root.forceActiveFocus(); root.clicked() }
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,7 @@ Rectangle {
|
||||
width: parent.width - 14 - 8
|
||||
height: parent.height
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.fill: parent
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -50,6 +50,8 @@ Rectangle {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: input
|
||||
width: flick.width
|
||||
text: root.text
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Pointer editing for both TextInput and TextEdit, including native TextFields.
|
||||
// Keep focus on the editor so form bindings and focus-loss commits stay intact.
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
readonly property var editor: parent
|
||||
readonly property bool secret: editor.echoMode !== undefined && editor.echoMode !== TextInput.Normal
|
||||
readonly property bool writable: editor.enabled && !editor.readOnly
|
||||
Component.onCompleted: editor.selectByMouse = true
|
||||
property int editRevision: 0
|
||||
Connections {
|
||||
target: root.editor
|
||||
ignoreUnknownSignals: true
|
||||
function onTextEdited() { root.editRevision += 1 }
|
||||
}
|
||||
|
||||
function perform(action) {
|
||||
editor.forceActiveFocus()
|
||||
const before = editor.text
|
||||
const revision = editRevision
|
||||
if (action === "copy" && !secret) editor.copy()
|
||||
else if (action === "cut" && writable && !secret) editor.cut()
|
||||
else if (action === "paste" && writable) editor.paste()
|
||||
else if (action === "select-all") editor.selectAll()
|
||||
// Drive the same callback as typing if this Qt version did not emit
|
||||
// textEdited for cut/paste. Avoid double requests on versions that do.
|
||||
// TextEdit already notifies its consumers via textChanged.
|
||||
if (editor.text !== before && editor instanceof TextInput && editRevision === revision)
|
||||
editor.textEdited()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: function(mouse) {
|
||||
root.editor.forceActiveFocus()
|
||||
menuLoader.active = true
|
||||
menuLoader.item.openAtCursor(root.editor, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
// Allocate menu rows/icons only when this field is first right-clicked.
|
||||
Loader {
|
||||
id: menuLoader
|
||||
active: false
|
||||
sourceComponent: CardMenu {
|
||||
id: menu
|
||||
objectName: "textEditContextMenu"
|
||||
menuWidth: 180
|
||||
focus: false
|
||||
entries: [
|
||||
{label: QbzSession.tr("Cut", QbzSession.trRev), icon: "scissors", action: "cut",
|
||||
enabled: root.writable && !root.secret && root.editor.selectedText !== ""},
|
||||
{label: QbzSession.tr("Copy", QbzSession.trRev), icon: "copy", action: "copy",
|
||||
enabled: !root.secret && root.editor.selectedText !== ""},
|
||||
{label: QbzSession.tr("Paste", QbzSession.trRev), icon: "clipboard", action: "paste",
|
||||
enabled: root.writable && root.editor.canPaste},
|
||||
{label: QbzSession.tr("Select all", QbzSession.trRev), icon: "square-check-big", action: "select-all",
|
||||
enabled: root.editor.text !== ""}
|
||||
]
|
||||
onPicked: function(action) { root.perform(action) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,14 @@ Item {
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property bool rowEnabled: true
|
||||
property bool fitDescription: false
|
||||
default property alias control: controlHost.data
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
height: kioskHost ? labelColumn.height + controlHost.height + 24 : (description === "" ? 52 : 64)
|
||||
height: kioskHost ? labelColumn.height + controlHost.height + 24
|
||||
: Math.max(description === "" ? 52 : 64, fitDescription ? labelColumn.height + 16 : 0)
|
||||
|
||||
Column {
|
||||
id: labelColumn
|
||||
|
||||
@@ -220,6 +220,7 @@ Item {
|
||||
border.width: input.activeFocus ? 1 : 0
|
||||
border.color: theme.focusRing
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
leftPadding: 9
|
||||
|
||||
@@ -190,6 +190,8 @@ Item {
|
||||
border.color: queryInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: queryInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -83,6 +83,7 @@ Rectangle {
|
||||
KioskTab { text: "+"; onClicked: QbzMyQbz.createOpen(root.kind) }
|
||||
}
|
||||
TextField {
|
||||
QbzTextEditMenu { }
|
||||
id: search
|
||||
anchors.top: root.compactHeader ? root.top : tabs.bottom
|
||||
anchors.left: root.compactHeader ? tabs.right : parent.left
|
||||
|
||||
@@ -154,6 +154,7 @@ Rectangle {
|
||||
TouchButton { text: "⋯" + (root.doc.selectedCount > 0 ? " " + root.doc.selectedCount : ""); onClicked: root.showMenu() }
|
||||
}
|
||||
TextField {
|
||||
QbzTextEditMenu { }
|
||||
y: root.compactHeader ? 70 : 124
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
@@ -854,7 +854,7 @@ Rectangle {
|
||||
// per-row batch-root cost as the quality cell above.
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.item.artPath || ""
|
||||
source: root.showArtwork ? (root.item.artPath || "") : ""
|
||||
radius: 4
|
||||
}
|
||||
// Per-item cover placeholder — clears when THIS row's cover lands,
|
||||
|
||||
@@ -56,6 +56,17 @@ Column {
|
||||
spacing: 4
|
||||
|
||||
// --- Theme row state (theme_qt.rs via the bridge) --------------------
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
fitDescription: true
|
||||
label: QbzSession.tr("Visual resource profile", QbzSession.trRev)
|
||||
description: QbzSession.tr("Low uses the Small player, compact headers, no dynamic background, header gradient, spectrum, animated play indicator, sidebar collages or optional track artwork. Default restores these visual defaults. Editing a managed option selects Custom. Audio quality and playback memory are independent.", QbzSession.trRev)
|
||||
QbzSelect { kioskHost: root.kioskHost;
|
||||
menuWidth: 240
|
||||
options: [QbzSession.tr("Low", QbzSession.trRev), QbzSession.tr("Default", QbzSession.trRev), QbzSession.tr("Custom", QbzSession.trRev)]
|
||||
currentIndex: Math.max(0, ["low", "default", "custom"].indexOf(root.doc.appearanceProfile || "default"))
|
||||
onSelected: function(i) { QbzBridge.settingsString("appearance-profile", ["low", "default", "custom"][i]) }
|
||||
}
|
||||
}
|
||||
readonly property var themeEntries: {
|
||||
try {
|
||||
return JSON.parse(QbzShell.themeListJson)
|
||||
|
||||
@@ -668,6 +668,8 @@ Item {
|
||||
border.color: manualInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
|
||||
TextInput {
|
||||
|
||||
QbzTextEditMenu { }
|
||||
id: manualInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// (settings_qt/library.rs).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
@@ -51,11 +52,43 @@ Column {
|
||||
if (!ts) return QbzSession.tr("Never", QbzSession.trRev)
|
||||
return Qt.formatDateTime(new Date(ts * 1000), "yyyy-MM-dd hh:mm")
|
||||
}
|
||||
// Shared column geometry (header + rows MUST agree):
|
||||
// checkbox 20 · folder (rest) · last-scan 120 · status 84 · actions 110.
|
||||
readonly property int actionsW: root.kioskHost ? 200 : 110
|
||||
readonly property var statusLabels: ({
|
||||
active: QbzSession.tr("Active", QbzSession.trRev),
|
||||
hidden: QbzSession.tr("Hidden", QbzSession.trRev),
|
||||
missing: QbzSession.tr("Missing", QbzSession.trRev),
|
||||
disconnected: QbzSession.tr("Disconnected", QbzSession.trRev),
|
||||
denied: QbzSession.tr("Access denied", QbzSession.trRev),
|
||||
unavailable: QbzSession.tr("Unavailable", QbzSession.trRev),
|
||||
checking: QbzSession.tr("Checking...", QbzSession.trRev)
|
||||
})
|
||||
readonly property var statusDescriptions: ({
|
||||
active: QbzSession.tr("The folder is accessible and visible in the library.", QbzSession.trRev),
|
||||
hidden: QbzSession.tr("The folder is accessible but hidden by your preference.", QbzSession.trRev),
|
||||
missing: QbzSession.tr("The folder path no longer exists. Reconnect it, change its path or remove it from the library.", QbzSession.trRev),
|
||||
disconnected: QbzSession.tr("A connection failure prevents access to this storage.", QbzSession.trRev),
|
||||
denied: QbzSession.tr("QBZ does not have permission to access this folder.", QbzSession.trRev),
|
||||
unavailable: QbzSession.tr("The folder could not be accessed. The check timed out or returned another access error.", QbzSession.trRev),
|
||||
checking: QbzSession.tr("Folder access is being checked or has not been checked yet.", QbzSession.trRev)
|
||||
})
|
||||
FontMetrics {
|
||||
id: statusMetrics
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
font.weight: theme.weightMedium
|
||||
}
|
||||
readonly property int statusW: Math.ceil(Math.max(110,
|
||||
...Object.values(root.statusLabels).map(function(s) { return statusMetrics.advanceWidth(s) + 20 })))
|
||||
readonly property int actionButtonW: root.kioskHost ? 44 : 40
|
||||
readonly property int actionsW: 4 * actionButtonW + 3 * 4
|
||||
function folderColW(rowWidth) {
|
||||
return Math.max(60, rowWidth - (root.kioskHost ? 44 : 20) - 120 - 84 - actionsW - 4 * 12)
|
||||
return Math.max(100, rowWidth - (root.kioskHost ? 44 : 20) - 120 - statusW - actionsW - 4 * 12)
|
||||
}
|
||||
property string lastPickedPath: ""
|
||||
onLibChanged: {
|
||||
const picked = root.lib.picked_path || ""
|
||||
if (picked !== lastPickedPath) {
|
||||
lastPickedPath = picked
|
||||
if (picked !== "") { root.pendingPath = picked; pathField.text = picked }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -103,10 +136,7 @@ Column {
|
||||
onEdited: function (v) { root.pendingPath = v }
|
||||
onCommitted: function (v) { root.pendingPath = v }
|
||||
}
|
||||
// The native chooser (settings_qt/library.rs pick_and_add_folder).
|
||||
// It adds the folder itself, so there is nothing to clear here —
|
||||
// the typed field is the OTHER route to the same insert, kept
|
||||
// because pasting a path is faster for network mounts.
|
||||
// Browse fills the same path field as typing; Add is the single commit.
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
iconName: "folder"
|
||||
text: QbzSession.tr("Browse...", QbzSession.trRev)
|
||||
@@ -160,6 +190,20 @@ Column {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// Keep every column reachable on narrow windows; never paint actions over status.
|
||||
Flickable {
|
||||
id: folderTable
|
||||
width: root.width
|
||||
height: tableRows.height
|
||||
contentWidth: Math.max(width, 100 + (root.kioskHost ? 44 : 20) + 120 + root.statusW + root.actionsW + 70)
|
||||
contentHeight: height
|
||||
clip: true
|
||||
flickableDirection: Flickable.HorizontalFlick
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
Column {
|
||||
id: tableRows
|
||||
width: folderTable.contentWidth
|
||||
spacing: 4
|
||||
// Table header (columns MUST match the rows below).
|
||||
Item {
|
||||
visible: root.folders.length > 0
|
||||
@@ -190,7 +234,15 @@ Column {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
width: 84
|
||||
width: root.actionsW
|
||||
height: parent.height
|
||||
text: QbzSession.tr("Actions", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
width: root.statusW
|
||||
height: parent.height
|
||||
text: QbzSession.tr("STATUS", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
@@ -198,7 +250,7 @@ Column {
|
||||
font.letterSpacing: 0.5
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Item { width: root.actionsW; height: 1 }
|
||||
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
@@ -214,7 +266,7 @@ Column {
|
||||
id: folderRow
|
||||
required property var modelData
|
||||
|
||||
width: root.width
|
||||
width: folderTable.contentWidth
|
||||
height: root.kioskHost ? 64 : 40
|
||||
radius: theme.radiusSm
|
||||
color: root.isSelected(modelData.id) ? theme.surfaceElevated
|
||||
@@ -249,14 +301,22 @@ Column {
|
||||
spacing: 8
|
||||
QbzIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// POC-NOTE: no `network` glyph in this icon set — a
|
||||
// network folder uses `link`, a local one hard-drive.
|
||||
name: folderRow.modelData.isNetwork ? "link" : "hard-drive"
|
||||
objectName: "folderType-" + folderRow.modelData.id
|
||||
name: folderRow.modelData.isNetwork ? "server" : "hard-drive"
|
||||
ToolTip.visible: typeHover.containsMouse
|
||||
ToolTip.delay: 400
|
||||
ToolTip.text: folderRow.modelData.isNetwork
|
||||
? QbzSession.tr("Network folder", QbzSession.trRev)
|
||||
: QbzSession.tr("Local folder", QbzSession.trRev)
|
||||
MouseArea {
|
||||
id: typeHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
width: 16
|
||||
height: 16
|
||||
tintName: folderRow.modelData.isNetwork
|
||||
? (folderRow.modelData.accessible ? "accent" : "favorite")
|
||||
: "secondary"
|
||||
tintName: folderRow.modelData.isNetwork ? "accent" : "secondary"
|
||||
}
|
||||
Text {
|
||||
width: parent.width - 24
|
||||
@@ -277,38 +337,25 @@ Column {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
// STATUS (read-only, like the Slint).
|
||||
Text {
|
||||
width: 84
|
||||
height: parent.height
|
||||
text: !folderRow.modelData.enabled
|
||||
? QbzSession.tr("Disabled", QbzSession.trRev)
|
||||
: (folderRow.modelData.isNetwork && !folderRow.modelData.accessible)
|
||||
? QbzSession.tr("Unavailable", QbzSession.trRev)
|
||||
: QbzSession.tr("Active", QbzSession.trRev)
|
||||
color: (folderRow.modelData.isNetwork && !folderRow.modelData.accessible
|
||||
&& folderRow.modelData.enabled) ? theme.danger : theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
||||
font.weight: theme.weightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
// ACTIONS: settings · enable/disable · scan this folder ·
|
||||
// remove it. The eye button duplicates the edit modal's
|
||||
// "Enabled" toggle on purpose — it predates the modal and is
|
||||
// one click instead of three for the thing people do most.
|
||||
Row {
|
||||
objectName: "folderActions-" + folderRow.modelData.id
|
||||
width: root.actionsW
|
||||
height: parent.height
|
||||
spacing: 4
|
||||
layoutDirection: Qt.RightToLeft
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
width: root.actionButtonW
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "trash-2"
|
||||
onClicked: QbzBridge.settingsString("library-remove-folders",
|
||||
JSON.stringify([folderRow.modelData.id]))
|
||||
}
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
width: root.actionButtonW
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "refresh-cw"
|
||||
enabled: folderRow.modelData.enabled && root.lib.scanning !== true
|
||||
@@ -316,6 +363,7 @@ Column {
|
||||
String(folderRow.modelData.id))
|
||||
}
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
width: root.actionButtonW
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: folderRow.modelData.enabled ? "eye" : "eye-off"
|
||||
onClicked: QbzBridge.settingsString("library-folder-enabled",
|
||||
@@ -326,16 +374,41 @@ Column {
|
||||
// the same place; this port had no affordance at all
|
||||
// until the modal landed.
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
width: root.actionButtonW
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "settings-2"
|
||||
onClicked: QbzBridge.settingsString("library-folder-edit-open",
|
||||
String(folderRow.modelData.id))
|
||||
}
|
||||
}
|
||||
Text {
|
||||
objectName: "folderStatus-" + folderRow.modelData.id
|
||||
width: root.statusW
|
||||
height: parent.height
|
||||
text: root.statusLabels[folderRow.modelData.status || "checking"] || root.statusLabels.unavailable
|
||||
color: ["missing", "disconnected", "denied", "unavailable"].indexOf(folderRow.modelData.status) >= 0
|
||||
? theme.danger : theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
font.weight: theme.weightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
ToolTip.visible: statusHover.containsMouse
|
||||
ToolTip.delay: 400
|
||||
ToolTip.text: root.statusDescriptions[folderRow.modelData.status || "checking"] || root.statusDescriptions.unavailable
|
||||
MouseArea {
|
||||
id: statusHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------- scan progress ---------------------------
|
||||
Item {
|
||||
visible: root.lib.scanning === true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// every control rides the settingsJson document and the settings invokables.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
@@ -13,6 +14,35 @@ Column {
|
||||
id: root
|
||||
|
||||
property var doc: ({})
|
||||
property var confirmHost: null
|
||||
|
||||
function applyMemory() {
|
||||
if (root.doc.playbackMemoryApplyBusy === true) return
|
||||
const track = QbzPlayer.npHasTrack ? QbzPlayer.npTrackId : ""
|
||||
if (QbzPlayer.npPlaying || QbzPlayer.npLoading) {
|
||||
if (!root.confirmHost) return
|
||||
root.confirmHost.ask(
|
||||
QbzSession.tr("Apply playback memory profile?", QbzSession.trRev),
|
||||
QbzSession.tr("Playback buffers will be released. If playing, the current track will stop and restart from the beginning. If paused, it will stay stopped at 0:00. Your queue and disk cache will be kept.", QbzSession.trRev),
|
||||
QbzSession.tr("Apply & free memory", QbzSession.trRev),
|
||||
function () { QbzBridge.settingsString("playback-cache-apply", "restart:" + track) })
|
||||
} else {
|
||||
QbzBridge.settingsString("playback-cache-apply", "idle:" + track)
|
||||
}
|
||||
}
|
||||
readonly property var cachePolicy: doc.playbackCache || ({})
|
||||
readonly property var cacheUsage: doc.playbackCacheUsage || ({})
|
||||
readonly property var storage: doc.playbackStorage || ({})
|
||||
readonly property var memoryProfileIds: ["auto", "high", "desktop", "low", "custom"]
|
||||
readonly property string memoryProfile: doc.playbackMemoryProfile ||
|
||||
(cachePolicy.profile || ((cachePolicy.dynamic || cachePolicy.min_mib != null || cachePolicy.max_mib != null) ? "custom" : "auto"))
|
||||
readonly property var memoryProfileDescriptions: [
|
||||
QbzSession.tr("Auto selects Desktop or Low from the computer's RAM. Fixed cache budget; dynamic growth is off. Recommended default.", QbzSession.trRev),
|
||||
QbzSession.tr("High starts with a 400 MiB cache budget and grows up to 1600 MiB when memory is available. It shrinks after inactivity or memory pressure. For long Hi-Res tracks and computers with spare RAM.", QbzSession.trRev),
|
||||
QbzSession.tr("Desktop uses a fixed 400 MiB cache budget and normal prefetch. Larger tracks use disk-backed buffers.", QbzSession.trRev),
|
||||
QbzSession.tr("Low uses a 50 MiB cache budget, smaller startup buffers and reduced prefetch. Large tracks use disk; speculative Hi-Res prefetch is skipped. Useful on Raspberry Pi or when saving RAM.", QbzSession.trRev),
|
||||
QbzSession.tr("Custom uses your cache limits and growth preference below. Prefetch follows the computer's detected memory class. Editing a preset switches to Custom.", QbzSession.trRev)
|
||||
]
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
@@ -39,11 +69,8 @@ Column {
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
label: QbzSession.tr("Gapless playback", QbzSession.trRev)
|
||||
description: QbzSession.tr("Play consecutive same-format tracks without a gap.", QbzSession.trRev)
|
||||
// Disabled while Streaming only is on — gapless needs the local cache.
|
||||
rowEnabled: root.doc.streamingOnly !== true
|
||||
QbzToggle { kioskHost: root.kioskHost;
|
||||
checked: root.doc.gapless === true
|
||||
enabled: root.doc.streamingOnly !== true
|
||||
onToggled: function (v) { QbzBridge.settingsBool("gapless", v) }
|
||||
}
|
||||
}
|
||||
@@ -143,7 +170,8 @@ Column {
|
||||
}
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
label: QbzSession.tr("Streaming only", QbzSession.trRev)
|
||||
description: QbzSession.tr("Skip writing tracks to the local cache while streaming.", QbzSession.trRev)
|
||||
fitDescription: true
|
||||
description: QbzSession.tr("Mimics official clients’ streaming behavior with lower memory usage. Skips the playback cache and its protection against network interruptions: QBZ’s cache keeps upcoming audio ready for more reliable playback and instant replay.", QbzSession.trRev)
|
||||
QbzToggle { kioskHost: root.kioskHost;
|
||||
checked: root.doc.streamingOnly === true
|
||||
onToggled: function (v) { QbzBridge.settingsBool("streaming-only", v) }
|
||||
@@ -159,4 +187,245 @@ Column {
|
||||
onSelected: function (i) { QbzBridge.settingsSelect("retry-behavior", i) }
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSpacer { }
|
||||
SettingsDivider { }
|
||||
SettingsSpacer { }
|
||||
GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("PLAYBACK CACHE", QbzSession.trRev) }
|
||||
GroupBox {
|
||||
id: memoryProfiles
|
||||
objectName: "playbackMemoryFieldset"
|
||||
width: root.width
|
||||
padding: 16
|
||||
topPadding: label.implicitHeight + 28
|
||||
title: QbzSession.tr("Playback memory profile", QbzSession.trRev)
|
||||
Accessible.role: Accessible.Grouping
|
||||
Accessible.name: title
|
||||
label: Text {
|
||||
x: memoryProfiles.leftPadding
|
||||
y: 12
|
||||
text: memoryProfiles.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
|
||||
font.weight: theme.weightMedium
|
||||
}
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
border.color: theme.borderSubtle
|
||||
radius: 6
|
||||
}
|
||||
contentItem: Column {
|
||||
spacing: 16
|
||||
Text {
|
||||
objectName: "playbackMemoryStreamingNotice"
|
||||
width: parent.width
|
||||
visible: root.doc.streamingOnly === true
|
||||
text: QbzSession.tr("These options do not apply while Streaming only is enabled. Turn it off to choose a playback memory profile and apply changes.", QbzSession.trRev)
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
Repeater {
|
||||
id: profileOptions
|
||||
model: root.memoryProfileIds
|
||||
delegate: QbzRadioOption {
|
||||
required property int index
|
||||
required property string modelData
|
||||
objectName: "memoryProfile-" + modelData
|
||||
width: parent.width
|
||||
kioskHost: root.kioskHost
|
||||
label: [QbzSession.tr("Auto — recommended", QbzSession.trRev),
|
||||
QbzSession.tr("High — dynamic", QbzSession.trRev),
|
||||
QbzSession.tr("Desktop", QbzSession.trRev),
|
||||
QbzSession.tr("Low — Pi style", QbzSession.trRev),
|
||||
QbzSession.tr("Custom", QbzSession.trRev)][index]
|
||||
description: root.memoryProfileDescriptions[index]
|
||||
selected: root.memoryProfile === modelData
|
||||
enabled: root.doc.streamingOnly !== true
|
||||
opacity: enabled ? 1 : 0.45
|
||||
onClicked: QbzBridge.settingsString("playback-memory-profile", modelData)
|
||||
function step(delta) {
|
||||
const next = profileOptions.itemAt((index + delta + 5) % 5)
|
||||
next.forceActiveFocus()
|
||||
next.clicked()
|
||||
}
|
||||
Keys.onUpPressed: step(-1)
|
||||
Keys.onDownPressed: step(1)
|
||||
Keys.onLeftPressed: step(-1)
|
||||
Keys.onRightPressed: step(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
objectName: "playbackMemoryCustomOptions"
|
||||
width: parent.width
|
||||
visible: root.memoryProfile === "custom"
|
||||
spacing: 4
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
fitDescription: true
|
||||
label: QbzSession.tr("Grow cache when memory is available", QbzSession.trRev)
|
||||
description: QbzSession.tr("Allow the cache to grow within your maximum when RAM is available. High enables this automatically. Editing this option switches to Custom.", QbzSession.trRev)
|
||||
rowEnabled: root.doc.streamingOnly !== true
|
||||
QbzToggle { kioskHost: root.kioskHost;
|
||||
checked: root.cachePolicy.dynamic === true
|
||||
enabled: root.doc.streamingOnly !== true
|
||||
onToggled: function (v) { QbzBridge.settingsBool("playback-cache-dynamic", v) }
|
||||
}
|
||||
}
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
fitDescription: true
|
||||
label: QbzSession.tr("Minimum cache budget (MiB)", QbzSession.trRev)
|
||||
description: QbzSession.tr("Base budget, not reserved RAM. Memory pressure can reduce it. Leave blank for the recommended value.", QbzSession.trRev)
|
||||
rowEnabled: root.doc.streamingOnly !== true
|
||||
QbzLineEdit { kioskHost: root.kioskHost;
|
||||
width: 240
|
||||
enabled: root.doc.streamingOnly !== true
|
||||
text: root.cachePolicy.min_mib == null ? "" : String(root.cachePolicy.min_mib)
|
||||
placeholder: String(Math.round((root.cacheUsage.base_size_bytes || root.cacheUsage.recommended_size_bytes || 419430400) / 1048576))
|
||||
onCommitted: function (s) { QbzBridge.settingsString("playback-cache-min", s) }
|
||||
}
|
||||
}
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
fitDescription: true
|
||||
label: QbzSession.tr("Maximum cache budget (MiB)", QbzSession.trRev)
|
||||
description: QbzSession.tr("Growth ceiling; available memory may impose a lower limit. Leave blank for automatic.", QbzSession.trRev)
|
||||
rowEnabled: root.doc.streamingOnly !== true && root.cachePolicy.dynamic === true
|
||||
QbzLineEdit { kioskHost: root.kioskHost;
|
||||
width: 240
|
||||
enabled: root.doc.streamingOnly !== true && root.cachePolicy.dynamic === true
|
||||
text: root.cachePolicy.max_mib == null ? "" : String(root.cachePolicy.max_mib)
|
||||
placeholder: String(Math.round((root.cacheUsage.ceiling_size_bytes || 419430400) / 1048576))
|
||||
onCommitted: function (s) { QbzBridge.settingsString("playback-cache-max", s) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
objectName: "playbackMemoryTradeoff"
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("RAM and storage trade-off", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
|
||||
font.weight: theme.weightMedium
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("All profiles preserve audio quality and can use disk cache. Lower memory budgets rely more on storage speed and writes; more RAM can reduce that work. Cached disk tracks are read from disk even in High. Choose storage appropriate for your listening workload.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("The profile already applies to new buffers. Apply now to release existing playback buffers and the memory cache. Memory use can grow again as playback continues.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Row {
|
||||
objectName: "playbackMemoryActions"
|
||||
anchors.right: parent.right
|
||||
spacing: 8
|
||||
SettingsButton {
|
||||
objectName: "playbackMemoryReset"
|
||||
kioskHost: root.kioskHost
|
||||
text: QbzSession.tr("Restore defaults", QbzSession.trRev)
|
||||
enabled: root.doc.streamingOnly !== true && root.doc.playbackMemoryApplyBusy !== true
|
||||
onClicked: QbzBridge.settingsString("playback-cache-reset", "")
|
||||
}
|
||||
SettingsButton {
|
||||
objectName: "playbackMemoryApply"
|
||||
kioskHost: root.kioskHost
|
||||
text: QbzSession.tr("Apply & free memory", QbzSession.trRev)
|
||||
busy: root.doc.playbackMemoryApplyBusy === true
|
||||
enabled: root.doc.streamingOnly !== true && !busy
|
||||
onClicked: root.applyMemory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
label: QbzSession.tr("Memory cache usage", QbzSession.trRev)
|
||||
description: QbzSession.tr("Used / current budget: %1 / %2 MiB. This excludes decoder memory and offline downloads.", QbzSession.trRev)
|
||||
.arg(Math.round((root.cacheUsage.current_size_bytes || 0) / 1048576))
|
||||
.arg(Math.round((root.cacheUsage.max_size_bytes || 0) / 1048576))
|
||||
SettingsButton { kioskHost: root.kioskHost;
|
||||
text: QbzSession.tr("Refresh", QbzSession.trRev)
|
||||
onClicked: QbzBridge.settingsString("playback-cache-refresh", "")
|
||||
}
|
||||
}
|
||||
Column {
|
||||
objectName: "playbackStorageSetting"
|
||||
width: root.width
|
||||
spacing: 8
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("Playback storage folder", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: root.kioskHost ? theme.fontBody * 1.2 : theme.fontBody
|
||||
font.weight: theme.weightMedium
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("Choose an existing folder on the drive you want to use. QBZ stores its playback cache and temporary track buffers in a qbz-playback subfolder. Offline downloads are separate. Leave blank for the default location.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
QbzLineEdit {
|
||||
objectName: "playbackStoragePath"
|
||||
kioskHost: root.kioskHost
|
||||
width: Math.max(0, Math.min(storageBrowse.width * 2, parent.width - storageBrowse.width - parent.spacing))
|
||||
text: root.storage.candidate || ""
|
||||
onCommitted: function(path) { QbzBridge.settingsString("playback-storage-folder", path) }
|
||||
}
|
||||
SettingsButton {
|
||||
id: storageBrowse
|
||||
objectName: "playbackStorageBrowse"
|
||||
kioskHost: root.kioskHost
|
||||
text: QbzSession.tr("Browse...", QbzSession.trRev)
|
||||
onClicked: QbzBridge.settingsString("playback-storage-browse", "")
|
||||
}
|
||||
}
|
||||
Text {
|
||||
objectName: "playbackStorageActiveHint"
|
||||
width: parent.width
|
||||
text: QbzSession.tr("Current playback storage", QbzSession.trRev) + ": " + (root.storage.active || "—")
|
||||
+ "\n" + QbzSession.tr("Changing the folder takes effect after restarting QBZ. Existing cache files stay in the old location; they are not moved or deleted automatically.", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: root.kioskHost ? theme.fontLegal * 1.2 : theme.fontLegal
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: root.width
|
||||
visible: (root.storage.error || "") !== ""
|
||||
text: QbzSession.tr("Could not use this playback storage folder:", QbzSession.trRev) + " " + (root.storage.error || "")
|
||||
wrapMode: Text.Wrap
|
||||
color: theme.danger
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
SettingRow { kioskHost: root.kioskHost;
|
||||
visible: (root.storage.sandbox || "") !== ""
|
||||
fitDescription: true
|
||||
label: (root.storage.sandbox || "") + " — " + QbzSession.tr("Folder access", QbzSession.trRev)
|
||||
description: root.storage.sandbox === "Flatpak"
|
||||
? QbzSession.tr("If the sandbox blocks this folder, run the command below in a host terminal, restart QBZ, then select the folder again. Only grant access to a folder you intend to use.", QbzSession.trRev)
|
||||
: QbzSession.tr("Snap can access supported home folders and removable storage under /mnt, /media or /run/media. For removable storage, connect the interface below and retry. Snap cannot grant arbitrary folder access with a Flatpak-style override.", QbzSession.trRev)
|
||||
}
|
||||
CommandBlock {
|
||||
visible: (root.storage.command || "") !== ""
|
||||
width: root.width
|
||||
command: root.storage.command || ""
|
||||
}
|
||||
}
|
||||
@@ -446,6 +446,7 @@ Item {
|
||||
PlaybackSettings { kioskHost: root.kioskHost;
|
||||
width: parent.width
|
||||
doc: root.doc
|
||||
confirmHost: settingsConfirmHost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,10 @@ Rectangle {
|
||||
|
||||
NowPlayingBar {
|
||||
id: npb
|
||||
// The small seek thumb/hit area extends above the bar by a few pixels.
|
||||
// Only Small needs this overlap. Large's cover dock is a later
|
||||
// sibling and must paint over the full-width player background.
|
||||
z: QbzShell.npbMode === 2 ? 1 : 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
@@ -175,23 +175,15 @@ Rectangle {
|
||||
try { return JSON.parse(QbzBridge.settingsJson) } catch (e) { return ({}) }
|
||||
}
|
||||
|
||||
/// Purchases in the TITLE BAR — the third row of the §7.1 truth table.
|
||||
///
|
||||
/// This is the EXACT COMPLEMENT of `Sidebar.qml`'s `purchasesVisible`, and
|
||||
/// it has to be, because that property WITHDRAWS the sidebar row for this
|
||||
/// configuration. Until this existed the entry was withdrawn from one host
|
||||
/// and offered by none: with `show_purchases` and `nav_tb_purchases` both on
|
||||
/// and a custom title bar, Purchases simply had no way in.
|
||||
///
|
||||
/// `nav_tb_purchases` only RELOCATES; `show_purchases` is the master gate.
|
||||
/// Under system chrome or with no title bar there is nowhere to relocate TO,
|
||||
/// so the sidebar keeps it and this stays false.
|
||||
// Follow navigation into the header, including the closed-sidebar fallback.
|
||||
// The separate relocation preference also works while navigation stays in
|
||||
// the sidebar; only that explicit title-bar placement needs custom chrome.
|
||||
readonly property bool purchasesInHeader:
|
||||
root.settingsDoc.showPurchases === true
|
||||
&& !QbzSession.offline
|
||||
&& root.settingsDoc.navTbPurchases === true
|
||||
&& !QbzShell.systemTitleBar
|
||||
&& !QbzShell.hideTitleBar
|
||||
&& (root.headerTabsOn || root.headerCompactOn
|
||||
|| (root.settingsDoc.navTbPurchases === true
|
||||
&& !QbzShell.systemTitleBar && !QbzShell.hideTitleBar))
|
||||
|
||||
// Highlighted section — derived from the live view (see NavFlyout), OR'd
|
||||
// in the triggers with "my menu is open" (Slint highlights off
|
||||
@@ -256,17 +248,17 @@ Rectangle {
|
||||
visible: purchaseTab.showIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "shopping-bag"
|
||||
width: 16
|
||||
height: 16
|
||||
tintName: purchaseTab.isActive ? "primary" : "muted"
|
||||
width: purchaseTab.compact ? 16 : 14
|
||||
height: width
|
||||
tintName: purchaseTab.isActive ? "textPrimary" : "secondary"
|
||||
}
|
||||
Text {
|
||||
visible: !purchaseTab.compact
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: QbzSession.tr("Purchases", QbzSession.trRev)
|
||||
color: purchaseTab.isActive ? theme.textPrimary : theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
font.weight: theme.weightMedium
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 11
|
||||
font.weight: purchaseTab.isActive ? theme.weightSemibold : theme.weightRegular
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +452,13 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit Purchases relocation with the other navigation in the sidebar.
|
||||
PurchaseTab {
|
||||
visible: root.purchasesInHeader && !root.headerTabsOn && !root.headerCompactOn
|
||||
showIcon: root.width >= 1140
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Compact section nav — while the sidebar is fully closed (so the
|
||||
// sections stay reachable), or always when the nav is in the header
|
||||
// and "Compact header navigation" is ON (HeaderBar.slint:974).
|
||||
@@ -618,6 +617,7 @@ Rectangle {
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: plSearch
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
@@ -880,6 +880,7 @@ Rectangle {
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -898,10 +899,8 @@ Rectangle {
|
||||
// the reference opens on the FIRST keystroke >= 2 chars and
|
||||
// debounces only the LOAD. Rust's version guard is what discards
|
||||
// the superseded loads.
|
||||
// Shared by keyboard edits (onTextEdited) and the edit menu's
|
||||
// cut/paste, which are PROGRAMMATIC changes — TextInput only
|
||||
// emits textEdited for user input, so a menu paste would
|
||||
// otherwise change the text without ever driving the cortinilla.
|
||||
// The shared edit menu dispatches the same textEdited callback
|
||||
// as typing, once, including on Qt versions where paste does not.
|
||||
function applyLiveQuery() {
|
||||
if (text.trim().length < 2) {
|
||||
QbzSearch.cortinillaDismiss()
|
||||
@@ -911,38 +910,6 @@ Rectangle {
|
||||
}
|
||||
onTextEdited: applyLiveQuery()
|
||||
|
||||
// QoL round: clipboard access by pointer, not only Ctrl+C/X/V —
|
||||
// QML TextInput has no system edit menu of its own. RightButton
|
||||
// only, so left clicks keep placing the cursor.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: function (mouse) {
|
||||
searchEditMenu.openAtCursor(searchInput, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
CardMenu {
|
||||
id: searchEditMenu
|
||||
menuWidth: 160
|
||||
entries: {
|
||||
var t = QbzSession.tr
|
||||
var r = QbzSession.trRev
|
||||
var hasSel = searchInput.selectedText !== ""
|
||||
return [
|
||||
{ "label": t("Cut", r), "icon": "scissors", "action": "cut", "enabled": hasSel },
|
||||
{ "label": t("Copy", r), "icon": "copy", "action": "copy", "enabled": hasSel },
|
||||
{ "label": t("Paste", r), "icon": "clipboard", "action": "paste", "enabled": searchInput.canPaste },
|
||||
{ "label": t("Select all", r), "icon": "square-check-big", "action": "select-all", "enabled": searchInput.text !== "" },
|
||||
]
|
||||
}
|
||||
onPicked: function (a) {
|
||||
if (a === "cut") { searchInput.cut(); searchInput.applyLiveQuery() }
|
||||
else if (a === "copy") searchInput.copy()
|
||||
else if (a === "paste") { searchInput.paste(); searchInput.applyLiveQuery() }
|
||||
else if (a === "select-all") searchInput.selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
// The Enter rule (HeaderBar.slint on-enter): cortinilla open +
|
||||
// a keyboard selection -> activate the row; open + none -> full
|
||||
// search; closed -> plain submit (also Search > All).
|
||||
|
||||
@@ -261,6 +261,7 @@ Item {
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.left: searchGlyph.right
|
||||
anchors.leftMargin: 8
|
||||
@@ -488,6 +489,7 @@ Item {
|
||||
tintName: "accent"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
anchors.left: linkGlyph.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.right: urlCopy.left
|
||||
|
||||
@@ -44,9 +44,6 @@ Rectangle {
|
||||
// PlayerBarSmall.slint).
|
||||
color: ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
|
||||
readonly property bool ambientOn: theme.ambientOn
|
||||
// Feature parked for a later release. Keep the compact renderer in place
|
||||
// but use the classic seekbar consistently with the other NPB modes.
|
||||
readonly property bool waveformVisible: false
|
||||
/// AppShell's one shared hover-tooltip overlay.
|
||||
property Item tooltip: null
|
||||
|
||||
@@ -142,27 +139,6 @@ Rectangle {
|
||||
return m + ":" + (s < 10 ? "0" : "") + s
|
||||
}
|
||||
|
||||
// --- Seek clamp (closes PARITY-DEBT #15, QML half) -------------------
|
||||
// Same SeekBar the full bar mounts (PlayerBarSmall.slint:186 passes
|
||||
// seekable-max: NowPlayingState.seekable-max into SeekBar.slint), so the
|
||||
// behaviour is identical here: while a track is still downloading the
|
||||
// seek target is LOCKED to the furthest fraction that has arrived and the
|
||||
// cursor turns not-allowed past it. Source: state.slint:4402, fed by
|
||||
// playback.rs:5304 `buffer_progress.clamp(0,1)`, published as
|
||||
// QbzPlayer.npSeekableMax. A fully-available track reports 1.0, so the
|
||||
// Math.min() is a no-op and local/cached seeking stays free.
|
||||
function clamp01(v) {
|
||||
return Math.min(Math.max(v, 0), 1)
|
||||
}
|
||||
// SeekBar.slint:98 — Math.min(clamp01(mouse-x / width), seekable-max).
|
||||
function seekTarget(fraction) {
|
||||
return Math.min(root.clamp01(fraction), QbzPlayer.npSeekableMax)
|
||||
}
|
||||
// SeekBar.slint:93 — clamp01(mouse-x / width) > seekable-max.
|
||||
function beyondSeekable(fraction) {
|
||||
return root.clamp01(fraction) > QbzPlayer.npSeekableMax
|
||||
}
|
||||
|
||||
// --- Track Info (album/TrackInfoModal.slint) -------------------------
|
||||
// The (i) button and the song-card title open the MODAL (scrim + centered
|
||||
// card) — PlayerBarSmall.slint fires media-action("track", id,
|
||||
@@ -214,81 +190,14 @@ Rectangle {
|
||||
spacing: 0
|
||||
|
||||
// === A. TOP full-width seekbar (the content/bar divider) ========
|
||||
Item {
|
||||
QbzCompactSeekBar {
|
||||
width: parent.width
|
||||
height: 3
|
||||
|
||||
Rectangle {
|
||||
id: seekTrack
|
||||
width: parent.width
|
||||
height: root.waveformVisible ? 13 : 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 2
|
||||
color: root.waveformVisible ? "transparent" : theme.surfaceElevated
|
||||
|
||||
SeekWaveformItem {
|
||||
anchors.fill: parent
|
||||
visible: root.waveformVisible
|
||||
values: QbzPlayer.npSeekWaveform
|
||||
playedProgress: QbzPlayer.npProgress
|
||||
cacheProgress: QbzPlayer.npCacheProgress
|
||||
baseColor: theme.surfaceElevated
|
||||
cacheColor: Qt.rgba(theme.textMuted.r, theme.textMuted.g,
|
||||
theme.textMuted.b, 0.35)
|
||||
playedColor: theme.accent
|
||||
renderMode: 1
|
||||
}
|
||||
|
||||
// Buffered / cache line.
|
||||
Rectangle {
|
||||
visible: !root.waveformVisible
|
||||
width: parent.width * Math.min(Math.max(QbzPlayer.npCacheProgress, 0), 1)
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: Qt.rgba(theme.textMuted.r, theme.textMuted.g,
|
||||
theme.textMuted.b, 0.35)
|
||||
// Alpha in the material, not a container `opacity`: an
|
||||
// always-on opacity node pins this quad in its own batch,
|
||||
// and the three seek quads use compatible materials
|
||||
// (QSGSmoothColorMaterial::compare() returns 0), so folding
|
||||
// it lets rail + cache + progress merge.
|
||||
}
|
||||
// Playback progress line.
|
||||
Rectangle {
|
||||
visible: !root.waveformVisible
|
||||
width: parent.width * Math.min(Math.max(QbzPlayer.npProgress, 0), 1)
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: theme.accent
|
||||
}
|
||||
// Hover thumb.
|
||||
Rectangle {
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: theme.textPrimary
|
||||
x: parent.width * Math.min(Math.max(QbzPlayer.npProgress, 0), 1) - width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: seekArea.containsMouse ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
}
|
||||
}
|
||||
// Tall, easy-to-grab hit area over the thin line.
|
||||
MouseArea {
|
||||
id: seekArea
|
||||
width: parent.width
|
||||
height: 18
|
||||
anchors.verticalCenter: seekTrack.verticalCenter
|
||||
hoverEnabled: true
|
||||
// No-drop cursor over the not-yet-downloaded region
|
||||
// (SeekBar.slint:93-95).
|
||||
cursorShape: root.beyondSeekable(mouseX / width) ? Qt.ForbiddenCursor
|
||||
: Qt.PointingHandCursor
|
||||
enabled: QbzPlayer.npHasTrack
|
||||
// Lock the seek target to what has downloaded while streaming
|
||||
// (SeekBar.slint:96-99).
|
||||
onClicked: QbzPlayer.seek(root.seekTarget(mouseX / width))
|
||||
}
|
||||
hasTrack: QbzPlayer.npHasTrack
|
||||
progress: QbzPlayer.npProgress
|
||||
cacheProgress: QbzPlayer.npCacheProgress
|
||||
seekableMax: QbzPlayer.npSeekableMax
|
||||
durationSecs: QbzPlayer.npDurationSecs
|
||||
onSeekRequested: function (fraction) { QbzPlayer.seek(fraction) }
|
||||
}
|
||||
|
||||
// === B. CONTROLS ROW — symmetric 3 columns =======================
|
||||
|
||||
@@ -144,6 +144,7 @@ Popup {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
clip: true
|
||||
TextEdit {
|
||||
QbzTextEditMenu { }
|
||||
id: statusText
|
||||
width: statusFlick.width
|
||||
readOnly: true
|
||||
@@ -214,6 +215,7 @@ Popup {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
clip: true
|
||||
TextEdit {
|
||||
QbzTextEditMenu { }
|
||||
id: logText
|
||||
width: logFlick.width
|
||||
readOnly: true
|
||||
|
||||
@@ -434,7 +434,7 @@ Rectangle {
|
||||
// visible queue row.
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[row.artUrl] || ""
|
||||
source: qrRoot.artworkLeading ? (root.coverMap[row.artUrl] || "") : ""
|
||||
radius: 4
|
||||
}
|
||||
// Per-item: this thumbnail clears when ITS cover lands, so
|
||||
@@ -1358,6 +1358,7 @@ Rectangle {
|
||||
border.color: customInput.activeFocus ? theme.accent : theme.borderSubtle
|
||||
color: theme.surfaceElevated
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: customInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 9
|
||||
|
||||
@@ -1016,6 +1016,7 @@ Rectangle {
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: myqbzSearchEdit
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
@@ -1292,6 +1293,7 @@ Rectangle {
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchEdit
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
|
||||
@@ -121,7 +121,7 @@ Item {
|
||||
RoundedImage {
|
||||
visible: QbzPlayer.npHasTrack
|
||||
anchors.fill: parent
|
||||
source: QbzPlayer.npArtworkPath
|
||||
source: root.visible && QbzPlayer.npHasTrack ? QbzPlayer.npArtworkPath : ""
|
||||
radius: theme.radiusMd
|
||||
}
|
||||
QbzIcon {
|
||||
|
||||
@@ -66,7 +66,8 @@ Rectangle {
|
||||
readonly property bool pulled: root.item.qobuzUnavailable === true
|
||||
readonly property int cacheStatus: root.item.cacheStatus !== undefined
|
||||
? root.item.cacheStatus : 0
|
||||
readonly property bool pulledDead: root.pulled && root.cacheStatus !== 3
|
||||
readonly property bool pulledDead: (root.pulled && root.cacheStatus !== 3)
|
||||
|| root.item.sourceUnavailable === true
|
||||
/// `modifiers` rides straight off the mouse event: Shift is what turns
|
||||
/// a click into a range (controls/SelectionModel.qml).
|
||||
signal toggleSelect(int modifiers)
|
||||
@@ -123,7 +124,7 @@ Rectangle {
|
||||
/// below all resolve a catalog id.
|
||||
readonly property bool catalogRow: {
|
||||
var src = root.item.source || ""
|
||||
return src !== "local" && src !== "plex"
|
||||
return src === "" || src === "qobuz"
|
||||
}
|
||||
|
||||
function menuEntries() {
|
||||
|
||||
@@ -1484,6 +1484,7 @@ Rectangle {
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
width: parent.width - 21
|
||||
height: parent.height
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -148,10 +148,12 @@ Rectangle {
|
||||
it._albumLc = String(it.album || "").toLowerCase()
|
||||
it._genreLc = String(it.genre || "").toLowerCase()
|
||||
if (it.kind === "track" || it.kind === "album") {
|
||||
if (it.group === "favorites" || it.group === "purchases") {
|
||||
var tabKey = it.kind + ":" + it.id
|
||||
if (it.group === "favorites" || it.group === "purchases"
|
||||
|| (it.kind === "album" && isLocalFeedItem(it))) {
|
||||
var tabKey = membershipKey
|
||||
if (it.group === "purchases") tabPurchased[tabKey] = true
|
||||
else tabFavorites[tabKey] = true
|
||||
else if (it.group === "favorites" || it.isFavorite === true)
|
||||
tabFavorites[tabKey] = true
|
||||
if (!tabSeen[tabKey]) {
|
||||
tabSeen[tabKey] = true
|
||||
totals[it.kind + "s"]++
|
||||
@@ -674,13 +676,15 @@ Rectangle {
|
||||
var x = feed[i]
|
||||
var keep = false
|
||||
if (sourceTab && x.kind === sourceKind) {
|
||||
var sourceKey = sourceKind + ":" + x.id
|
||||
var sourceKey = x._membershipKey
|
||||
var isPurchased = purchased[sourceKey] === true
|
||||
var isFavorite = favorites[sourceKey] === true
|
||||
var included = matchesSources(isPurchased, isFavorite, false)
|
||||
var localRelease = sourceKind === "album" && isLocalFeedItem(x)
|
||||
if (localRelease && !showLocal && hideableLocalAlbum(x)) included = false
|
||||
// One representative row per included entity. A purchase row
|
||||
// wins even in Favorites-only so its quality badge survives.
|
||||
keep = included && ((isPurchased && x.group === "purchases")
|
||||
keep = included && (localRelease || (isPurchased && x.group === "purchases")
|
||||
|| (!isPurchased && x.group === "favorites"))
|
||||
if (keep) {
|
||||
if (tabSeen[sourceKey]) keep = false
|
||||
|
||||
@@ -556,6 +556,7 @@ Rectangle {
|
||||
height: parent.height
|
||||
clip: true
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
anchors.fill: parent
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -122,6 +122,7 @@ Rectangle {
|
||||
height: parent.height
|
||||
clip: true
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: mpSearch
|
||||
anchors.fill: parent
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -737,6 +737,7 @@ Rectangle {
|
||||
spacing: 6
|
||||
QbzIcon { name: "search"; width: 13; height: 13; anchors.verticalCenter: parent.verticalCenter; tintName: "muted" }
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
width: parent.width - 19
|
||||
height: parent.height
|
||||
color: theme.textPrimary
|
||||
|
||||
@@ -692,6 +692,7 @@ Rectangle {
|
||||
border.color: customMinutes.activeFocus
|
||||
? theme.accent : theme.borderSubtle
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: customMinutes
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 9
|
||||
|
||||
@@ -114,6 +114,19 @@ Item {
|
||||
// may additionally show the catalog mark in the mixed feed.
|
||||
source: cell.item.source
|
||||
sources: cell.item.sources || []
|
||||
catalogAffordances: !cell.view.isLocalFeedItem(cell.item)
|
||||
localMode: cell.view.isLocalFeedItem(cell.item)
|
||||
localPlaylistAffordance: localMode
|
||||
hostFavorite: localMode && badgeSources.some(function (source) {
|
||||
return ["local", "plex", "jellyfin", "subsonic", "navidrome",
|
||||
"gonic", "airsonic", "astiga"].indexOf(source) >= 0
|
||||
})
|
||||
onFavoriteRequested: QbzLocal.albumToggleFavorite(cell.item.id,
|
||||
cell.item.title || "", cell.item.artist || "", artworkUrl,
|
||||
JSON.stringify(badgeSources))
|
||||
onOpenRequested: QbzAlbum.openAlbum(cell.item.id)
|
||||
onPlayRequested: QbzPlayer.playAlbum(cell.item.id)
|
||||
onEnqueueRequested: function (mode) { QbzPlayer.enqueueAlbum(cell.item.id, mode) }
|
||||
showSourceBadge: cell.view.showSourceBadges
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ Rectangle {
|
||||
border.color: nameField.activeFocus
|
||||
? theme.accent : theme.surfaceElevated
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: nameField
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
// consumer.
|
||||
|
||||
import QtQuick
|
||||
import "../../controls"
|
||||
import com.blitzfc.qbz
|
||||
import "../../theme"
|
||||
|
||||
@@ -267,6 +268,7 @@ Item {
|
||||
tintName: "muted"
|
||||
}
|
||||
TextInput {
|
||||
QbzTextEditMenu { }
|
||||
id: searchInput
|
||||
width: parent.width - 22 - (clearBtn.visible ? clearBtn.width + 8 : 0)
|
||||
height: 21
|
||||
|
||||