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
This commit is contained in:
118 files changed
+7128
-883
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user