From 29710342a2ac62eb601d474864d18afe2bc1a0e2 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 13 Sep 2026 09:09:30 -0600 Subject: [PATCH] =?UTF-8?q?feat(qt):=20Settings=20>=20Storage=20=E2=80=94?= =?UTF-8?q?=20every=20on-disk=20cache=20in=20one=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Offline downloads, the artwork cache, the lyrics cache, the Plex mirror and the playback cache (memory profile + storage folder) move to a new Storage section, each row keeping the bridge verb it had. Offline keeps the mode rows, Playback keeps the behaviour rows, Plex keeps Disconnect. cc --- .../translations/de/LC_MESSAGES/qbz-ui.po | 12 + .../translations/en/LC_MESSAGES/qbz-ui.po | 12 + .../translations/es/LC_MESSAGES/qbz-ui.po | 12 + .../translations/fr/LC_MESSAGES/qbz-ui.po | 12 + .../translations/ja/LC_MESSAGES/qbz-ui.po | 12 + .../translations/nl/LC_MESSAGES/qbz-ui.po | 12 + .../translations/pt/LC_MESSAGES/qbz-ui.po | 12 + .../translations/ru/LC_MESSAGES/qbz-ui.po | 12 + crates/qbz-qt/build.rs | 1 + .../qbz-qt/qml/settings/OfflineSettings.qml | 79 +--- .../qbz-qt/qml/settings/PlaybackSettings.qml | 269 ----------- crates/qbz-qt/qml/settings/PlexSettings.qml | 21 - crates/qbz-qt/qml/settings/SettingsView.qml | 19 + .../qbz-qt/qml/settings/StorageSettings.qml | 426 ++++++++++++++++++ scripts/qml-tests/tst_playback_cache.qml | 8 +- 15 files changed, 551 insertions(+), 368 deletions(-) create mode 100644 crates/qbz-qt/qml/settings/StorageSettings.qml diff --git a/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po index ef6b292e8..e58e47ea7 100644 --- a/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po @@ -7543,3 +7543,15 @@ msgstr "Cover-Cache leeren" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} Bilder belegen {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Hängt \"feat. …\" mit den Gastinterpreten des Titels an die Künstlerzeile der Qobuz-Titelzeilen an. Standardmäßig aus." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Speicher" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "PLEX-CACHE" diff --git a/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po index a654c4aa3..2ed681047 100644 --- a/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po @@ -3169,3 +3169,15 @@ msgstr "Clear artwork cache" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} images using {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Storage" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "PLEX CACHE" diff --git a/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po index 2842d4530..3e101cfc1 100644 --- a/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po @@ -7549,3 +7549,15 @@ msgstr "Vaciar caché de portadas" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} imágenes ocupando {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Añade \"feat. …\" con los intérpretes invitados de la pista a la línea de artista en las filas de pistas de Qobuz. Desactivado por defecto." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Almacenamiento" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "CACHÉ DE PLEX" diff --git a/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po index 7b97fce6a..6c273456c 100644 --- a/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po @@ -7541,3 +7541,15 @@ msgstr "Vider le cache des pochettes" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} images occupant {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Ajoute « feat. … » avec les interprètes invités du morceau sur la ligne d'artiste des lignes de piste Qobuz. Désactivé par défaut." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Stockage" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "CACHE PLEX" diff --git a/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po index 86eaad161..d662e8b96 100644 --- a/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po @@ -7524,3 +7524,15 @@ msgstr "アートワークキャッシュを消去" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} 件の画像、使用量 {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Qobuz のトラック行のアーティスト欄に、フィーチャリングされた演奏者を「feat. …」として追加します。既定ではオフです。" + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "ストレージ" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "PLEX キャッシュ" diff --git a/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po index da583b94d..101d77725 100644 --- a/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po @@ -7531,3 +7531,15 @@ msgstr "Artworkcache wissen" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} afbeeldingen, {} in gebruik" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Voegt \"feat. …\" met de gastartiesten van de track toe aan de artiestregel van Qobuz-trackregels. Standaard uit." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Opslag" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "PLEX-CACHE" diff --git a/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po index 6ae2f4a48..e17545e47 100644 --- a/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po @@ -7541,3 +7541,15 @@ msgstr "Limpar cache de capas" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} imagens ocupando {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Acrescenta \"feat. …\" com os intérpretes convidados da faixa à linha do artista nas linhas de faixa do Qobuz. Desligado por padrão." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Armazenamento" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "CACHE DO PLEX" diff --git a/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po index cb9ac469a..9131d08dc 100644 --- a/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po @@ -7562,3 +7562,15 @@ msgstr "Очистить кэш обложек" #: qbz-qt/qml/settings/OfflineSettings.qml msgid "{} images using {}" msgstr "{} изображений занимают {}" + +#: qbz-qt/qml/settings/AppearanceSettings.qml +msgid "Append \"feat. …\" with the track's featured performers to the artist line of Qobuz track rows. Off by default." +msgstr "Добавляет «feat. …» с приглашёнными исполнителями трека к строке артиста в строках треков Qobuz. По умолчанию выключено." + +#: qbz-qt/qml/settings/SettingsView.qml +msgid "Storage" +msgstr "Хранилище" + +#: qbz-qt/qml/settings/StorageSettings.qml +msgid "PLEX CACHE" +msgstr "КЭШ PLEX" diff --git a/crates/qbz-qt/build.rs b/crates/qbz-qt/build.rs index 8a4382e77..4c62da8a2 100644 --- a/crates/qbz-qt/build.rs +++ b/crates/qbz-qt/build.rs @@ -851,6 +851,7 @@ fn main() { "qml/settings/LocalLibrarySettings.qml", "qml/settings/LocalTabsConfigModal.qml", "qml/settings/OfflineSettings.qml", + "qml/settings/StorageSettings.qml", "qml/settings/PlaybackSettings.qml", // ONE component instantiated twice (Jellyfin + Subsonic). "qml/settings/MediaServerSettings.qml", diff --git a/crates/qbz-qt/qml/settings/OfflineSettings.qml b/crates/qbz-qt/qml/settings/OfflineSettings.qml index caef52619..b998a8cd5 100644 --- a/crates/qbz-qt/qml/settings/OfflineSettings.qml +++ b/crates/qbz-qt/qml/settings/OfflineSettings.qml @@ -9,12 +9,10 @@ // snapshot). // // "Check now" IS shipped (`offline-recheck`); so is the live tri-state status -// line (QbzSession.offlineMode / captivePortal) and the lyrics-cache row. +// line (QbzSession.offlineMode / captivePortal). // -// Still missing: the whole OFFLINE CACHE group (Open manager / Open folder / -// Clear all). What blocks it is the MANAGER VIEW, which has no Qt counterpart -// yet — not the engine: `offline_cache_qt.rs` already runs the downloads from -// the album page and the track row. +// The on-disk half (offline downloads, artwork, lyrics) lives in Settings > +// Storage (StorageSettings.qml) since 2026-09-13, next to every other cache. import QtQuick import com.blitzfc.qbz @@ -100,75 +98,4 @@ Column { onClicked: QbzBridge.settingsString("offline-recheck", "") } } - - SettingsSpacer { } - SettingsDivider { } - SettingsSpacer { } - - // ========================== OFFLINE CACHE ============================ - // The downloads half: the manager view plus the two whole-cache actions. - // All three ride QbzOffline, the same bridge the manager view uses — - // "Open folder" and "Clear all" are the manager's own stats-bar buttons, - // offered here too exactly as the reference offers them - // (OfflineSettings.slint:135-167). - GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("OFFLINE CACHE", QbzSession.trRev) } - SettingRow { kioskHost: root.kioskHost; - label: QbzSession.tr("Manage offline cache", QbzSession.trRev) - description: QbzSession.tr("Browse and manage your downloaded tracks and albums.", QbzSession.trRev) - SettingsButton { kioskHost: root.kioskHost; - text: QbzSession.tr("Open manager", QbzSession.trRev) - onClicked: QbzOffline.openManager() - } - } - SettingRow { kioskHost: root.kioskHost; - label: QbzSession.tr("Cache folder", QbzSession.trRev) - description: QbzSession.tr("Open the folder where offline tracks are stored on disk.", QbzSession.trRev) - SettingsButton { kioskHost: root.kioskHost; - text: QbzSession.tr("Open folder", QbzSession.trRev) - onClicked: QbzOffline.openFolder() - } - } - SettingRow { kioskHost: root.kioskHost; - label: QbzSession.tr("Clear cache", QbzSession.trRev) - description: QbzSession.tr("Frees up cached data. Your downloaded albums are kept — remove those from the offline manager above.", QbzSession.trRev) - SettingsButton { kioskHost: root.kioskHost; - danger: true - text: QbzSession.tr("Clear all", QbzSession.trRev) - // ONE prompt before the purge. The reference fires straight from - // the button; this port confirms every destructive settings row, - // and undoing this one means re-downloading the whole cache. - onClicked: { - if (!root.confirmHost) { - QbzOffline.clearAll() - return - } - root.confirmHost.ask( - QbzSession.tr("Clear cache", QbzSession.trRev), - QbzSession.tr("Frees up cached data. Your downloaded albums are kept — remove those from the offline manager above.", QbzSession.trRev), - QbzSession.tr("Clear all", QbzSession.trRev), - function () { QbzOffline.clearAll() }) - } - } - } - - SettingsSpacer { } - SettingsDivider { } - SettingsSpacer { } - - // ============================= LYRICS ================================ - GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("LYRICS", QbzSession.trRev) } - SettingRow { kioskHost: root.kioskHost; - label: QbzSession.tr("Clear lyrics cache", QbzSession.trRev) - // "{} entries using {}" — the real per-user lyrics.db stats. - description: root.off.lyricsLoaded === true - ? QbzSession.tr("{} entries using {}", QbzSession.trRev) - .replace("{}", root.off.lyricsEntries) - .replace("{}", root.off.lyricsSize) - : "" - SettingsButton { kioskHost: root.kioskHost; - danger: true - text: QbzSession.tr("Clear", QbzSession.trRev) - onClicked: QbzBridge.settingsString("lyrics-cache-clear", "") - } - } } diff --git a/crates/qbz-qt/qml/settings/PlaybackSettings.qml b/crates/qbz-qt/qml/settings/PlaybackSettings.qml index f856b83df..fc2cd5bb6 100644 --- a/crates/qbz-qt/qml/settings/PlaybackSettings.qml +++ b/crates/qbz-qt/qml/settings/PlaybackSettings.qml @@ -16,34 +16,6 @@ Column { 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 } spacing: 4 @@ -187,245 +159,4 @@ 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 || "" - } } diff --git a/crates/qbz-qt/qml/settings/PlexSettings.qml b/crates/qbz-qt/qml/settings/PlexSettings.qml index 76dfd1ea4..d95a4fcf8 100644 --- a/crates/qbz-qt/qml/settings/PlexSettings.qml +++ b/crates/qbz-qt/qml/settings/PlexSettings.qml @@ -451,26 +451,5 @@ Column { } } } - SettingRow { kioskHost: root.kioskHost; - label: QbzSession.tr("Clear cache", QbzSession.trRev) - description: QbzSession.tr("Remove cached Plex libraries and tracks. Your sign-in is kept.", QbzSession.trRev) - SettingsButton { kioskHost: root.kioskHost; - danger: true - text: QbzSession.tr("Clear cache", QbzSession.trRev) - enabled: root.plex.hasToken === true - // plex_auth.rs:997-1001 — one prompt, sign-in kept. - onClicked: { - if (!root.confirmHost) { - QbzBridge.settingsString("plex-clear-cache", "") - return - } - root.confirmHost.ask( - QbzSession.tr("Clear Plex cache?", QbzSession.trRev), - QbzSession.tr("This removes cached Plex libraries and tracks. Your sign-in is kept.", QbzSession.trRev), - QbzSession.tr("Clear cache", QbzSession.trRev), - function () { QbzBridge.settingsString("plex-clear-cache", "") }) - } - } - } } } diff --git a/crates/qbz-qt/qml/settings/SettingsView.qml b/crates/qbz-qt/qml/settings/SettingsView.qml index 6e6248933..5d5ce6609 100644 --- a/crates/qbz-qt/qml/settings/SettingsView.qml +++ b/crates/qbz-qt/qml/settings/SettingsView.qml @@ -9,6 +9,8 @@ // 1 Playback -> PlaybackSettings.qml // 2 Appearance -> AppearanceSettings.qml // 3 Offline -> OfflineSettings.qml +// 13 Storage -> StorageSettings.qml (every on-disk cache; shown +// right after Offline) // 4 Local Library -> LocalLibrarySettings.qml (+ PlexSettings.qml) // 5 Blacklist -> BlacklistSettings.qml // 6 Integrations -> IntegrationsSettings.qml @@ -72,6 +74,7 @@ Item { { label: QbzSession.tr("Appearance", QbzSession.trRev), section: 2 }, { label: QbzSession.tr("Navigation", QbzSession.trRev), section: 10 }, { label: QbzSession.tr("Offline", QbzSession.trRev), section: 3 }, + { label: QbzSession.tr("Storage", QbzSession.trRev), section: 13 }, { label: QbzSession.tr("Local Library", QbzSession.trRev), section: 4 }, { label: QbzSession.tr("Blacklist", QbzSession.trRev), section: 5 }, { label: QbzSession.tr("Integrations", QbzSession.trRev), section: 6 }, @@ -258,6 +261,12 @@ Item { active: root.section === 3 onClicked: QbzBridge.settingsSetSection(3) } + SubNavItem { + name: "folder" + label: QbzSession.tr("Storage", QbzSession.trRev) + active: root.section === 13 + onClicked: QbzBridge.settingsSetSection(13) + } SubNavItem { name: "hard-drive" label: QbzSession.tr("Local Library", QbzSession.trRev) @@ -478,6 +487,16 @@ Item { } } } + Panel { + panelIndex: 13 + sourceComponent: Component { + StorageSettings { kioskHost: root.kioskHost; + width: parent.width + doc: root.doc + confirmHost: settingsConfirmHost + } + } + } Panel { panelIndex: 4 sourceComponent: Component { diff --git a/crates/qbz-qt/qml/settings/StorageSettings.qml b/crates/qbz-qt/qml/settings/StorageSettings.qml new file mode 100644 index 000000000..23471f25b --- /dev/null +++ b/crates/qbz-qt/qml/settings/StorageSettings.qml @@ -0,0 +1,426 @@ +// Settings > Storage — every cache QBZ keeps on disk, in one place: the +// offline downloads, the shared artwork cache, the lyrics cache, the Plex +// mirror and the playback cache (memory profile + storage folder). The rows +// moved here from Offline / Plex / Playback on 2026-09-13 unchanged: each one +// still rides the same bridge verb it always did, so "where is my disk +// going" has one answer without a second code path. + +import QtQuick +import QtQuick.Controls +import com.blitzfc.qbz +import "../controls" +import "../theme" + +Column { + property bool kioskHost: false + + id: root + + property var doc: ({}) + readonly property var off: doc.offline || ({}) + readonly property var plex: (doc.library || ({})).plex || ({}) + /// The view-level SettingsConfirmHost (SettingsView.qml). Null in previews + /// — every destructive row guards, so a preview degrades to the + /// unconfirmed call rather than swallowing the click. + property var confirmHost: null + + // --- playback cache (from PlaybackSettings.qml, verbatim) --------------- + 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 } + + spacing: 4 + + // ========================== OFFLINE CACHE ============================ + // The downloads half: the manager view plus the two whole-cache actions. + // All three ride QbzOffline, the same bridge the manager view uses — + // "Open folder" and "Clear all" are the manager's own stats-bar buttons, + // offered here too exactly as the reference offers them + // (OfflineSettings.slint:135-167). + GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("OFFLINE CACHE", QbzSession.trRev) } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Manage offline cache", QbzSession.trRev) + description: QbzSession.tr("Browse and manage your downloaded tracks and albums.", QbzSession.trRev) + SettingsButton { kioskHost: root.kioskHost; + text: QbzSession.tr("Open manager", QbzSession.trRev) + onClicked: QbzOffline.openManager() + } + } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Cache folder", QbzSession.trRev) + description: QbzSession.tr("Open the folder where offline tracks are stored on disk.", QbzSession.trRev) + SettingsButton { kioskHost: root.kioskHost; + text: QbzSession.tr("Open folder", QbzSession.trRev) + onClicked: QbzOffline.openFolder() + } + } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Clear cache", QbzSession.trRev) + description: QbzSession.tr("Frees up cached data. Your downloaded albums are kept — remove those from the offline manager above.", QbzSession.trRev) + SettingsButton { kioskHost: root.kioskHost; + danger: true + text: QbzSession.tr("Clear all", QbzSession.trRev) + // ONE prompt before the purge. The reference fires straight from + // the button; this port confirms every destructive settings row, + // and undoing this one means re-downloading the whole cache. + onClicked: { + if (!root.confirmHost) { + QbzOffline.clearAll() + return + } + root.confirmHost.ask( + QbzSession.tr("Clear cache", QbzSession.trRev), + QbzSession.tr("Frees up cached data. Your downloaded albums are kept — remove those from the offline manager above.", QbzSession.trRev), + QbzSession.tr("Clear all", QbzSession.trRev), + function () { QbzOffline.clearAll() }) + } + } + } + + SettingsSpacer { } + SettingsDivider { } + SettingsSpacer { } + + // ========================== ARTWORK CACHE ============================ + GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("ARTWORK CACHE", QbzSession.trRev) } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Maximum size", QbzSession.trRev) + description: QbzSession.tr("Album and artist images are kept on disk up to this budget; the least recently shown are dropped first, during use and at startup.", QbzSession.trRev) + QbzSelect { kioskHost: root.kioskHost; + menuWidth: 160 + options: root.off.imageCacheSizes || [] + currentIndex: root.off.imageCacheSizeIndex || 0 + onSelected: function (i) { QbzBridge.settingsSelect("image-cache-max", i) } + } + } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Clear artwork cache", QbzSession.trRev) + // "{} images using {}" — the shared image cache's own stats. + description: root.off.imageCacheLoaded === true + ? QbzSession.tr("{} images using {}", QbzSession.trRev) + .replace("{}", root.off.imageCacheFiles) + .replace("{}", root.off.imageCacheSize) + : "" + SettingsButton { kioskHost: root.kioskHost; + danger: true + text: QbzSession.tr("Clear", QbzSession.trRev) + onClicked: QbzBridge.settingsString("image-cache-clear", "") + } + } + + SettingsSpacer { } + SettingsDivider { } + SettingsSpacer { } + + // ============================= LYRICS ================================ + GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("LYRICS", QbzSession.trRev) } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Clear lyrics cache", QbzSession.trRev) + // "{} entries using {}" — the real per-user lyrics.db stats. + description: root.off.lyricsLoaded === true + ? QbzSession.tr("{} entries using {}", QbzSession.trRev) + .replace("{}", root.off.lyricsEntries) + .replace("{}", root.off.lyricsSize) + : "" + SettingsButton { kioskHost: root.kioskHost; + danger: true + text: QbzSession.tr("Clear", QbzSession.trRev) + onClicked: QbzBridge.settingsString("lyrics-cache-clear", "") + } + } + + SettingsSpacer { } + SettingsDivider { } + SettingsSpacer { } + + // ============================ PLEX CACHE ============================= + GroupHeader { kioskHost: root.kioskHost; text: QbzSession.tr("PLEX CACHE", QbzSession.trRev) } + SettingRow { kioskHost: root.kioskHost; + label: QbzSession.tr("Clear cache", QbzSession.trRev) + description: QbzSession.tr("Remove cached Plex libraries and tracks. Your sign-in is kept.", QbzSession.trRev) + SettingsButton { kioskHost: root.kioskHost; + danger: true + text: QbzSession.tr("Clear cache", QbzSession.trRev) + enabled: root.plex.hasToken === true + // plex_auth.rs:997-1001 — one prompt, sign-in kept. + onClicked: { + if (!root.confirmHost) { + QbzBridge.settingsString("plex-clear-cache", "") + return + } + root.confirmHost.ask( + QbzSession.tr("Clear Plex cache?", QbzSession.trRev), + QbzSession.tr("This removes cached Plex libraries and tracks. Your sign-in is kept.", QbzSession.trRev), + QbzSession.tr("Clear cache", QbzSession.trRev), + function () { QbzBridge.settingsString("plex-clear-cache", "") }) + } + } + } + + 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 || "" + } +} diff --git a/scripts/qml-tests/tst_playback_cache.qml b/scripts/qml-tests/tst_playback_cache.qml index 70ffa7856..d4137b786 100644 --- a/scripts/qml-tests/tst_playback_cache.qml +++ b/scripts/qml-tests/tst_playback_cache.qml @@ -5,7 +5,10 @@ import "../../crates/qbz-qt/qml/settings" as Settings Item { width: 1000; height: 2800 - Settings.PlaybackSettings { id: settings; width: 950 } + // The playback cache lives in Settings > Storage (2026-09-13); Playback + // keeps the behaviour rows (gapless). + Settings.StorageSettings { id: settings; width: 950 } + Settings.PlaybackSettings { id: playback; width: 950 } QtObject { id: confirmation property var pending: null @@ -23,7 +26,7 @@ Item { waitForRendering(settings) } function row(label) { - var pending = [settings] + var pending = [settings, playback] while (pending.length) { const item = pending.pop() if (item.label === label && item.control !== undefined) return item @@ -55,6 +58,7 @@ Item { } function test_streaming_only_keeps_gapless_available() { seed(false, null, null, true) + playback.doc = settings.doc const gapless = control("Gapless playback") verify(gapless.enabled) mouseClick(gapless)