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:
@@ -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