47046665ed
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>
285 lines
9.8 KiB
QML
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|