Files
Plasma-Addon-Synology-NAS/package/contents/ui/main.qml
T
Jaroslav Beneš 47046665ed 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>
2026-07-15 14:24:42 +02:00

285 lines
9.8 KiB
QML

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()
}
}
}
}
}