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