Add tray UI: main.qml + ShareItem.qml
PlasmoidItem with a system-tray icon (active when a share is mounted) and a popup grouping shares per host. ExpandableListItem rows toggle mount/unmount and open the mounted folder; empty-state placeholder links to config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,16 @@ function unmountCmd(mountpoint) {
|
|||||||
return shCmd("exec pkexec " + HELPER + " unmount \"$1\"", [mountpoint]);
|
return shCmd("exec pkexec " + HELPER + " unmount \"$1\"", [mountpoint]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve the user's home directory.
|
||||||
|
function homeCmd() {
|
||||||
|
return shCmd("printf %s \"$HOME\"", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a path in the default file manager.
|
||||||
|
function openCmd(path) {
|
||||||
|
return shCmd("exec xdg-open \"$1\"", [path]);
|
||||||
|
}
|
||||||
|
|
||||||
// Save a password to KWallet (used by the config page). kwallet-query reads the
|
// Save a password to KWallet (used by the config page). kwallet-query reads the
|
||||||
// secret from stdin; we feed it via printf from a positional param. The value
|
// secret from stdin; we feed it via printf from a positional param. The value
|
||||||
// is in this process's argv for the brief write (unavoidable with a CLI-only
|
// is in this process's argv for the brief write (unavoidable with a CLI-only
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import org.kde.plasma.extras as PlasmaExtras
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One share row in the popup: title = share name, primary button toggles
|
||||||
|
* mount/unmount, context menu opens the mounted folder. Purely presentational —
|
||||||
|
* it emits signals; main.qml owns the mount logic.
|
||||||
|
*/
|
||||||
|
PlasmaExtras.ExpandableListItem {
|
||||||
|
id: item
|
||||||
|
|
||||||
|
property string shareName: ""
|
||||||
|
property string hostLabel: ""
|
||||||
|
property string comment: ""
|
||||||
|
property string mountpoint: ""
|
||||||
|
property bool mounted: false
|
||||||
|
property bool busy: false
|
||||||
|
|
||||||
|
signal mountToggled()
|
||||||
|
signal openRequested()
|
||||||
|
|
||||||
|
icon: "folder-network"
|
||||||
|
iconEmblem: mounted ? "emblem-mounted" : ""
|
||||||
|
title: shareName
|
||||||
|
subtitle: {
|
||||||
|
if (busy)
|
||||||
|
return mounted ? i18n("Unmounting…") : i18n("Mounting…");
|
||||||
|
if (mounted)
|
||||||
|
return mountpoint;
|
||||||
|
if (comment.length > 0)
|
||||||
|
return comment;
|
||||||
|
return i18n("Not mounted");
|
||||||
|
}
|
||||||
|
isBusy: busy
|
||||||
|
enabled: !busy
|
||||||
|
|
||||||
|
defaultActionButtonAction: QQC2.Action {
|
||||||
|
text: item.mounted ? i18n("Unmount") : i18n("Mount")
|
||||||
|
icon.name: item.mounted ? "media-eject" : "drive-harddisk"
|
||||||
|
onTriggered: item.mountToggled()
|
||||||
|
}
|
||||||
|
|
||||||
|
contextualActions: [
|
||||||
|
QQC2.Action {
|
||||||
|
text: i18n("Open in File Manager")
|
||||||
|
icon.name: "document-open-folder"
|
||||||
|
enabled: item.mounted
|
||||||
|
onTriggered: item.openRequested()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
|
||||||
|
import org.kde.plasma.plasmoid
|
||||||
|
import org.kde.plasma.core as PlasmaCore
|
||||||
|
import org.kde.plasma.components as PlasmaComponents3
|
||||||
|
import org.kde.plasma.extras as PlasmaExtras
|
||||||
|
import org.kde.plasma.plasma5support as Plasma5Support
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import "../code/nas.js" as Nas
|
||||||
|
|
||||||
|
PlasmoidItem {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// --- state ------------------------------------------------------------
|
||||||
|
property string homeDir: ""
|
||||||
|
property var mountedSet: ({}) // mountpoint -> true
|
||||||
|
property var hostShares: ({}) // hostId -> [{name, comment}]
|
||||||
|
property bool scanning: false
|
||||||
|
property bool anyMounted: false
|
||||||
|
|
||||||
|
readonly property var cfg: Plasmoid.configuration
|
||||||
|
|
||||||
|
Plasmoid.icon: "folder-network"
|
||||||
|
Plasmoid.status: anyMounted ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus
|
||||||
|
|
||||||
|
switchWidth: Kirigami.Units.gridUnit * 12
|
||||||
|
switchHeight: Kirigami.Units.gridUnit * 12
|
||||||
|
|
||||||
|
// --- helpers ----------------------------------------------------------
|
||||||
|
function settingsObj() {
|
||||||
|
return {
|
||||||
|
home: homeDir,
|
||||||
|
defaultMountRoot: cfg.defaultMountRoot,
|
||||||
|
smbVersion: cfg.smbVersion,
|
||||||
|
mountOptions: cfg.mountOptions,
|
||||||
|
fileMode: cfg.fileMode,
|
||||||
|
dirMode: cfg.dirMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hostById(id) {
|
||||||
|
var hosts = Nas.parseHosts(cfg.hosts);
|
||||||
|
for (var i = 0; i < hosts.length; ++i)
|
||||||
|
if (hosts[i].id === id) return hosts[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildModel() {
|
||||||
|
var hosts = Nas.parseHosts(cfg.hosts);
|
||||||
|
var s = settingsObj();
|
||||||
|
var mounted = false;
|
||||||
|
sharesModel.clear();
|
||||||
|
for (var i = 0; i < hosts.length; ++i) {
|
||||||
|
var h = hosts[i];
|
||||||
|
sharesModel.append({
|
||||||
|
isHeader: true, hostId: h.id,
|
||||||
|
hostLabel: (h.label && h.label.length ? h.label : h.host),
|
||||||
|
shareName: "", comment: "", mountpoint: "", mounted: false, busy: false
|
||||||
|
});
|
||||||
|
var shares = hostShares[h.id] || [];
|
||||||
|
for (var j = 0; j < shares.length; ++j) {
|
||||||
|
var mp = Nas.mountpointFor(s.defaultMountRoot, s.home, h, shares[j].name);
|
||||||
|
var isMounted = mountedSet[mp] === true;
|
||||||
|
if (isMounted) mounted = true;
|
||||||
|
sharesModel.append({
|
||||||
|
isHeader: false, hostId: h.id,
|
||||||
|
hostLabel: (h.label && h.label.length ? h.label : h.host),
|
||||||
|
shareName: shares[j].name, comment: shares[j].comment || "",
|
||||||
|
mountpoint: mp, mounted: isMounted, busy: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyMounted = mounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshMounts(then) {
|
||||||
|
exe.run(Nas.listMountedCmd(), function (code, out, err) {
|
||||||
|
mountedSet = Nas.parseMounted(out);
|
||||||
|
rebuildModel();
|
||||||
|
if (then) then();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanShares() {
|
||||||
|
var hosts = Nas.parseHosts(cfg.hosts);
|
||||||
|
if (hosts.length === 0) { rebuildModel(); return; }
|
||||||
|
scanning = true;
|
||||||
|
var pending = hosts.length;
|
||||||
|
hosts.forEach(function (h) {
|
||||||
|
exe.run(Nas.enumerateCmd(h), function (code, out, err) {
|
||||||
|
hostShares[h.id] = Nas.parseShares(out);
|
||||||
|
if (--pending === 0) scanning = false;
|
||||||
|
rebuildModel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAll() {
|
||||||
|
refreshMounts(function () { scanShares(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShare(index) {
|
||||||
|
var row = sharesModel.get(index);
|
||||||
|
if (!row || row.isHeader) return;
|
||||||
|
var host = hostById(row.hostId);
|
||||||
|
if (!host) return;
|
||||||
|
sharesModel.setProperty(index, "busy", true);
|
||||||
|
if (row.mounted) {
|
||||||
|
exe.run(Nas.unmountCmd(row.mountpoint), function () { refreshMounts(); });
|
||||||
|
} else {
|
||||||
|
var m = Nas.mountCmd(settingsObj(), host, row.shareName);
|
||||||
|
exe.run(m.cmd, function () { refreshMounts(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShare(index) {
|
||||||
|
var row = sharesModel.get(index);
|
||||||
|
if (row && row.mountpoint.length > 0)
|
||||||
|
exe.run(Nas.openCmd(row.mountpoint), function () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConfig() {
|
||||||
|
var a = Plasmoid.internalAction("configure");
|
||||||
|
if (a) a.trigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- data sources -----------------------------------------------------
|
||||||
|
ListModel { id: sharesModel }
|
||||||
|
|
||||||
|
Plasma5Support.DataSource {
|
||||||
|
id: exe
|
||||||
|
engine: "executable"
|
||||||
|
property var cbs: ({})
|
||||||
|
property int nonce: 0
|
||||||
|
onNewData: function (source, data) {
|
||||||
|
var cb = cbs[source];
|
||||||
|
disconnectSource(source);
|
||||||
|
if (cb) {
|
||||||
|
delete cbs[source];
|
||||||
|
cb(data["exit code"], data["stdout"], data["stderr"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function run(cmd, cb) {
|
||||||
|
var key = cmd + " tok" + (nonce++);
|
||||||
|
cbs[key] = cb;
|
||||||
|
connectSource(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
exe.run(Nas.homeCmd(), function (code, out) {
|
||||||
|
homeDir = (out || "").trim();
|
||||||
|
rebuildModel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onExpandedChanged: {
|
||||||
|
if (expanded && homeDir.length > 0)
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI ---------------------------------------------------------------
|
||||||
|
fullRepresentation: PlasmaExtras.Representation {
|
||||||
|
id: rep
|
||||||
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 19
|
||||||
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 22
|
||||||
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 22
|
||||||
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 26
|
||||||
|
|
||||||
|
collapseMarginsHint: true
|
||||||
|
|
||||||
|
header: PlasmaExtras.PlasmoidHeading {
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
Kirigami.Heading {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
level: 1
|
||||||
|
text: i18n("Network Shares")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
PlasmaComponents3.ToolButton {
|
||||||
|
icon.name: "view-refresh"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
text: i18n("Refresh")
|
||||||
|
enabled: !root.scanning
|
||||||
|
onClicked: root.refreshAll()
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.ScrollView {
|
||||||
|
id: scroll
|
||||||
|
anchors.fill: parent
|
||||||
|
contentWidth: availableWidth
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: shareList
|
||||||
|
model: sharesModel
|
||||||
|
currentIndex: -1
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
delegate: Column {
|
||||||
|
id: rowRoot
|
||||||
|
width: shareList.width
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property bool isHeader
|
||||||
|
required property string hostLabel
|
||||||
|
required property string shareName
|
||||||
|
required property string comment
|
||||||
|
required property string mountpoint
|
||||||
|
required property bool mounted
|
||||||
|
required property bool busy
|
||||||
|
|
||||||
|
// Host group header
|
||||||
|
PlasmaExtras.Heading {
|
||||||
|
visible: rowRoot.isHeader
|
||||||
|
width: parent.width
|
||||||
|
level: 3
|
||||||
|
type: Kirigami.Heading.Type.Primary
|
||||||
|
text: rowRoot.hostLabel
|
||||||
|
elide: Text.ElideRight
|
||||||
|
padding: Kirigami.Units.smallSpacing
|
||||||
|
leftPadding: Kirigami.Units.smallSpacing * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share row
|
||||||
|
ShareItem {
|
||||||
|
visible: !rowRoot.isHeader
|
||||||
|
width: parent.width
|
||||||
|
shareName: rowRoot.shareName
|
||||||
|
hostLabel: rowRoot.hostLabel
|
||||||
|
comment: rowRoot.comment
|
||||||
|
mountpoint: rowRoot.mountpoint
|
||||||
|
mounted: rowRoot.mounted
|
||||||
|
busy: rowRoot.busy
|
||||||
|
onMountToggled: root.toggleShare(rowRoot.index)
|
||||||
|
onOpenRequested: root.openShare(rowRoot.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaExtras.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||||
|
visible: sharesModel.count === 0 && !root.scanning
|
||||||
|
iconName: "network-server"
|
||||||
|
text: i18n("No NAS configured")
|
||||||
|
explanation: i18n("Add a Synology or SMB host to browse and mount its shared folders.")
|
||||||
|
helpfulAction: QQC2.Action {
|
||||||
|
text: i18n("Add NAS…")
|
||||||
|
icon.name: "list-add"
|
||||||
|
onTriggered: root.openConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: PlasmaExtras.PlasmoidHeading {
|
||||||
|
position: PlasmaExtras.PlasmoidHeading.Position.Footer
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
PlasmaComponents3.BusyIndicator {
|
||||||
|
visible: root.scanning
|
||||||
|
running: root.scanning
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
PlasmaComponents3.Button {
|
||||||
|
text: i18n("Add NAS…")
|
||||||
|
icon.name: "list-add"
|
||||||
|
onClicked: root.openConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user