aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sonix/SonixFolders.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-07 12:04:31 +0100
committerDanilo M. <danix@danix.xyz>2026-11-07 12:04:31 +0100
commit479a771d61ea29ff77286bb7ed3a41e37579aa34 (patch)
tree67a329f55bb14688faa7e07597cb6aa680894b74 /desktop/modules/sonix/SonixFolders.qml
parent4ed1eef421fa3af5aa25d231cc3ad36e8cdf4419 (diff)
downloadquickshell-479a771d61ea29ff77286bb7ed3a41e37579aa34.tar.gz
quickshell-479a771d61ea29ff77286bb7ed3a41e37579aa34.zip
sonix: the Folders tab, with add to playlist
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/sonix/SonixFolders.qml')
-rw-r--r--desktop/modules/sonix/SonixFolders.qml194
1 files changed, 194 insertions, 0 deletions
diff --git a/desktop/modules/sonix/SonixFolders.qml b/desktop/modules/sonix/SonixFolders.qml
new file mode 100644
index 0000000..ad7ae88
--- /dev/null
+++ b/desktop/modules/sonix/SonixFolders.qml
@@ -0,0 +1,194 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+import "../.."
+import "../appearance"
+import "../sound"
+import "Sonix.js" as S
+
+// The card's folders: browse, play a folder or a file, add either to a
+// playlist (an existing one, a draft from Playlists, or a new name).
+Column {
+ id: fo
+ required property var mod
+ property string root: ""
+ property string path: ""
+ property var entries: []
+ property string error: ""
+ property string notice: ""
+ // What + was pressed on: { label, folder: bool, path }; null when closed.
+ property var adding: null
+ spacing: 8
+
+ function glyph(cp) { return String.fromCodePoint(cp); }
+ function report(ok, d, err) { fo.error = ok ? "" : err; }
+
+ function go(p) {
+ Sonix.get("/api/browse?" + S.query({ path: p }), (ok, d, err) => {
+ fo.report(ok, d, err);
+ if (!ok) return;
+ fo.root = d.root;
+ fo.path = d.path;
+ fo.entries = d.entries || [];
+ fo.adding = null;
+ });
+ }
+ Component.onCompleted: go("")
+
+ // The files directly inside a folder, for playing or adding it whole.
+ function filesIn(folder, cb) {
+ Sonix.get("/api/browse?" + S.query({ path: folder }), (ok, d, err) => {
+ if (!ok) { fo.report(ok, d, err); return; }
+ cb((d.entries || []).filter(e => !e.dir).map(e => e.path));
+ });
+ }
+
+ function playFolder(folder) {
+ fo.filesIn(folder, files => {
+ if (files.length === 0) { fo.error = "no tracks directly in this folder"; return; }
+ Sonix.command({ do: "play_path", path: files[0], list: "folder", value: folder }, null, fo.report);
+ });
+ }
+
+ function addTo(name) {
+ const a = fo.adding;
+ if (!a || name === "") return;
+ const send = paths => {
+ if (paths.length === 0) { fo.error = "no tracks directly in this folder"; return; }
+ Sonix.command({ do: "playlist_add", value: name }, paths.join("\n"), (ok, d, err) => {
+ fo.report(ok, d, err);
+ if (ok) fo.notice = `Added ${paths.length} to ${name}`;
+ });
+ };
+ fo.adding = null;
+ if (a.folder) fo.filesIn(a.path, send); else send([a.path]);
+ }
+
+ property var playlists: []
+ function loadPlaylists() {
+ Sonix.get("/api/playlists", (ok, d, err) => { if (ok) fo.playlists = (d.playlists || []).map(l => l.name); });
+ }
+
+ // --- breadcrumbs ------------------------------------------------------
+
+ Flow {
+ width: fo.width
+ spacing: 6
+ Repeater {
+ model: S.crumbs(fo.path, fo.root)
+ delegate: Text {
+ required property var modelData
+ required property int index
+ text: (index > 0 ? "› " : "") + modelData.name
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
+ color: modelData.path === fo.path ? Theme.text : Theme.accent
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: fo.go(modelData.path)
+ }
+ }
+ }
+ }
+
+ Button {
+ text: "Up"
+ icon: ""
+ enabled: fo.path !== fo.root
+ onClicked: fo.go(S.parentOf(fo.path, fo.root))
+ }
+
+ // --- the add-to chooser -----------------------------------------------
+
+ Rectangle {
+ visible: fo.adding !== null
+ width: fo.width
+ height: chooser.implicitHeight + 16
+ radius: 8
+ color: Qt.alpha(Theme.surface, 0.8)
+ border.width: 1
+ border.color: Qt.alpha(Theme.accent, 0.5)
+
+ Column {
+ id: chooser
+ x: 8; y: 8
+ width: parent.width - 16
+ spacing: 6
+
+ Text {
+ text: fo.adding ? `Add “${fo.adding.label}” to:` : ""
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
+ color: Theme.text
+ }
+ Flow {
+ width: parent.width
+ spacing: 6
+ Repeater {
+ model: fo.playlists.concat(fo.mod.draftPlaylists.filter(n => fo.playlists.indexOf(n) < 0))
+ delegate: Button {
+ required property var modelData
+ text: modelData
+ onClicked: fo.addTo(modelData)
+ }
+ }
+ }
+ Row {
+ spacing: 6
+ Field { id: newName; width: 200; placeholder: "New playlist name" }
+ Button { text: "Add"; onClicked: fo.addTo(newName.value.trim()) }
+ Button { text: "Cancel"; onClicked: fo.adding = null }
+ }
+ }
+ }
+
+ // --- entries ----------------------------------------------------------
+
+ Repeater {
+ model: fo.entries
+ delegate: SonixRow {
+ required property var modelData
+ width: fo.width
+ text: modelData.name
+ sub: modelData.dir ? "folder" : ""
+ onClicked: if (modelData.dir) fo.go(modelData.path)
+ TransportButton {
+ glyph: fo.glyph(0xF040A) // md-play
+ onClicked: modelData.dir
+ ? fo.playFolder(modelData.path)
+ : Sonix.command({ do: "play_path", path: modelData.path, list: "folder", value: fo.path }, null, fo.report)
+ }
+ TransportButton {
+ glyph: fo.glyph(0xF0415) // md-plus
+ onClicked: {
+ fo.notice = "";
+ fo.loadPlaylists();
+ fo.adding = { label: modelData.name, folder: modelData.dir, path: modelData.path };
+ }
+ }
+ }
+ }
+
+ Text {
+ visible: fo.notice !== "" && fo.error === ""
+ text: fo.notice
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.green
+ }
+ Text {
+ visible: fo.error !== ""
+ width: fo.width
+ wrapMode: Text.Wrap
+ text: fo.error
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.red
+ }
+}