Files
qbz/crates/qbz-qt/qml/views/local/LocalFoldersTab.qml
T
vicrodh cb4c03ca46 feat(qt): one Open menu and one Refresh menu in the Local Library header
The ephemeral pane used to be a third arm of the Folders tab, reachable only
through a folder-open icon buried in the tree rail — one arm of one mode of
one tab. It also HID the folder browser while it was open. Neither survives
the media this pane is about to hold (a CD, a SACD image).

Row 1 of the header now carries two menus, both `CardMenu` (the shared menu
surface, no new component):

  - `Open`, always present, is the one door for every medium. Only "Open
    folder…" is wired here; the disc entries land with their readers.
  - `Refresh` replaces the Plex-only re-sync icon (#573). Rows are APPENDED
    per configured source rather than rendered disabled, so a user with no
    media servers sees local folders and nothing else. It needed no new Rust:
    `plexAvailable`, `mediaHasJellyfin`, `mediaHasSubsonic`, `syncPlex()`,
    `mediaSync()` and the settings' own "library-scan" all already existed.

The ephemeral session becomes a tab of its own, present only while a session
is. Two failure modes are handled because the tab can VANISH under the user:
closing the session falls back to Albums instead of leaving `activeTab` on a
tab with no body, and opening one navigates to it — while the pane was an arm
of Folders that was implicit, and with a tab of its own, opening a folder from
Albums would otherwise do nothing visible.

`load_tab_impl` gains an "ephemeral" arm: nothing to load (the session is in
memory), but without the arm every switch to the tab logged "unknown tab",
which is what a real routing bug looks like.

Driven under Xvfb, not inferred: both menus open, the gated rows match the
configured servers, opening lands on the tab, the badges read DSD64, closing
removes the tab and falls back to Albums, and the folder tree is now visible
WITH a session open.

cc
2026-08-20 21:14:36 -06:00

166 lines
5.7 KiB
QML

// Folders tab body (LocalLibraryView.slint:1531) — two mutually exclusive
// arms: FLAT mode (the album collection grouped by directory) and TREE mode
// (the two-pane filesystem browser).
//
// The ephemeral pane USED to be a third arm here, gated on
// `view.ephemeralActive`. It is now a tab of its own (`"ephemeral"`), so an
// open folder or disc no longer hides the folder browser you were using —
// and a medium that comes and goes cannot take a permanent tab hostage.
//
// The tree divider is also the drag handle that resizes the rail: min 136px
// (half the 272px default), max half the content area — the Slint's own
// clamp at :1907.
import QtQuick
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
Item {
id: root
property var view: null
QbzTheme { id: theme }
// Loading = the shape of the arm that is coming. FLAT mode is the same
// collection the Albums tab renders; TREE mode is the rail + detail pair
// (the rail rows are 34px folder rows with a leading glyph, so the
// placeholder uses the small square art cell).
QbzSkeleton {
visible: QbzLocal.localFoldersLoading
&& root.view.foldersMode === "flat"
variant: root.view.foldersGridView === "grid" ? "cardGrid" : "rowList"
anchors.fill: parent
anchors.leftMargin: 32
anchors.rightMargin: 32
anchors.topMargin: 16
cellW: 220
cellH: 266
rowH: 56
rowGap: 0
rowArtSize: 40
phase: root.view.skelPhase
}
Row {
visible: QbzLocal.localFoldersLoading
&& root.view.foldersMode !== "flat"
anchors.fill: parent
anchors.topMargin: 12
spacing: 0
Item { width: 12; height: 1 }
QbzSkeleton {
variant: "rowList"
width: root.view.treeRailWidth - 24
height: parent.height
// TreeRow is 26px with a small leading glyph (TreeRow.qml:34).
rowH: 26
rowGap: 0
rowArtSize: 14
phase: root.view.skelPhase
}
Item { width: 19; height: 1 }
QbzSkeleton {
variant: "cardGrid"
width: parent.width - root.view.treeRailWidth - 7
height: parent.height
cellW: 220
cellH: 266
phase: root.view.skelPhase
}
}
LocalNote {
visible: !QbzLocal.localFoldersLoading
&& root.view.folders.length === 0 && root.view.foldersSearch === ""
text: QbzSession.tr("No folders in your local library yet.", QbzSession.trRev)
}
// ---------------------------- FLAT MODE ------------------------------
Item {
anchors.fill: parent
visible: !QbzLocal.localFoldersLoading
&& root.view.folders.length > 0 && root.view.foldersMode === "flat"
LocalNote {
visible: root.view.foldersVisible.length === 0
text: QbzSession.tr("No folders match your search.", QbzSession.trRev)
}
LocalAlbumCollection {
scrollScope: "local:folders"
anchors.fill: parent
anchors.leftMargin: 32
anchors.rightMargin: 32
anchors.topMargin: 16
visible: root.view.foldersVisible.length > 0
view: root.view
surface: "folders-flat"
rows: root.view.foldersVisible
groups: root.view.foldersGrouped
grouped: root.view.foldersGroup !== "off"
viewMode: root.view.foldersGridView
// Slint mounts the flat folders collection with show-source
// false but show-source-badge true (:1606) — the badge on the
// card, no source COLUMN in the list arm.
showSource: true
onOpenRequested: function (id) { root.view.openAlbum(id) }
onPlayRequested: function (id) { QbzLocal.playAlbum(id, false) }
onEnqueueRequested: function (id, m) { QbzLocal.enqueue("album", id, m) }
}
}
// ---------------------------- TREE MODE ------------------------------
Item {
anchors.fill: parent
visible: !QbzLocal.localFoldersLoading
&& root.view.folders.length > 0 && root.view.foldersMode === "tree"
LocalTreeRail {
id: rail
x: 0
width: root.view.treeRailWidth
height: parent.height
view: root.view
}
// Divider + drag handle (7px grab area, 1px visible line).
Item {
id: handle
x: rail.width
width: 7
height: parent.height
Rectangle {
x: 3
width: 1
height: parent.height
color: dragArea.containsMouse || dragArea.pressed
? theme.accent : theme.borderSubtle
}
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
property real startW: 272
property real startX: 0
onPressed: function (mouse) {
startW = root.view.treeRailWidth
startX = mapToItem(root, mouse.x, mouse.y).x
}
onPositionChanged: function (mouse) {
if (!pressed) return
var dx = mapToItem(root, mouse.x, mouse.y).x - startX
root.view.treeRailWidth = Math.max(136,
Math.min(root.width / 2, startW + dx))
}
}
}
LocalFolderDetail {
x: rail.width + 7
width: parent.width - rail.width - 7
height: parent.height
view: root.view
}
}
}