From fab2d65dd265904240f2c7aea962ca9e41a47ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Wed, 15 Jul 2026 14:27:01 +0200 Subject: [PATCH] Add config pages: ConfigHosts + ConfigGeneral Host manager (add/edit/remove Synology/SMB hosts) that stores non-secret fields as JSON and writes passwords to KWallet via kwallet-query; general page for default mount folder, SMB version, mount options and permissions. Co-Authored-By: Claude Opus 4.8 (1M context) --- package/contents/ui/config/ConfigGeneral.qml | 69 +++++ package/contents/ui/config/ConfigHosts.qml | 253 +++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 package/contents/ui/config/ConfigGeneral.qml create mode 100644 package/contents/ui/config/ConfigHosts.qml diff --git a/package/contents/ui/config/ConfigGeneral.qml b/package/contents/ui/config/ConfigGeneral.qml new file mode 100644 index 0000000..e3a4384 --- /dev/null +++ b/package/contents/ui/config/ConfigGeneral.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami + +KCM.SimpleKCM { + id: page + + // Bound automatically by the Plasma config framework (main.xml keys). + property alias cfg_defaultMountRoot: mountRootField.text + property alias cfg_smbVersion: smbVersionField.editText + property alias cfg_mountOptions: mountOptionsField.text + property alias cfg_fileMode: fileModeField.text + property alias cfg_dirMode: dirModeField.text + + Kirigami.FormLayout { + + QQC2.TextField { + id: mountRootField + Kirigami.FormData.label: i18n("Default mount folder:") + placeholderText: i18n("Leave empty for ~/NAS") + Layout.minimumWidth: Kirigami.Units.gridUnit * 16 + } + QQC2.Label { + Kirigami.FormData.isSection: false + text: i18n("Shares mount under //.") + font: Kirigami.Theme.smallFont + opacity: 0.7 + } + + Item { Kirigami.FormData.isSection: true } + + QQC2.ComboBox { + id: smbVersionField + Kirigami.FormData.label: i18n("SMB protocol version:") + editable: true + model: ["default", "3.1.1", "3.0", "2.1", "1.0"] + } + QQC2.Label { + text: i18n("Passed to mount.cifs as vers=. Use 3.1.1 or 3.0 for modern Synology DSM.") + font: Kirigami.Theme.smallFont + opacity: 0.7 + wrapMode: Text.WordWrap + Layout.maximumWidth: Kirigami.Units.gridUnit * 20 + } + + Item { Kirigami.FormData.isSection: true } + + QQC2.TextField { + id: mountOptionsField + Kirigami.FormData.label: i18n("Extra mount options:") + placeholderText: "iocharset=utf8" + Layout.minimumWidth: Kirigami.Units.gridUnit * 16 + } + QQC2.TextField { + id: fileModeField + Kirigami.FormData.label: i18n("File permission mode:") + inputMask: "0999" + Layout.maximumWidth: Kirigami.Units.gridUnit * 6 + } + QQC2.TextField { + id: dirModeField + Kirigami.FormData.label: i18n("Folder permission mode:") + inputMask: "0999" + Layout.maximumWidth: Kirigami.Units.gridUnit * 6 + } + } +} diff --git a/package/contents/ui/config/ConfigHosts.qml b/package/contents/ui/config/ConfigHosts.qml new file mode 100644 index 0000000..fdc414a --- /dev/null +++ b/package/contents/ui/config/ConfigHosts.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami +import org.kde.plasma.plasma5support as Plasma5Support + +import "../../code/nas.js" as Nas + +KCM.ScrollViewKCM { + id: page + + // The hosts JSON is bound to the config framework through a hidden holder, + // so load/save "just work"; we mirror it into the visible ListModel. + property alias cfg_hosts: store.json + + QtObject { + id: store + property string json: "[]" + onJsonChanged: page.rebuildModel() + } + + ListModel { id: hostsModel } + + property bool building: false + + function rebuildModel() { + if (building) return; + var hosts = Nas.parseHosts(store.json); + hostsModel.clear(); + for (var i = 0; i < hosts.length; ++i) { + var h = hosts[i]; + hostsModel.append({ + id: h.id || "", label: h.label || "", host: h.host || "", + username: h.username || "", domain: h.domain || "", + mountRoot: h.mountRoot || "", vers: h.vers || "" + }); + } + } + + function persist() { + var arr = []; + for (var i = 0; i < hostsModel.count; ++i) { + var r = hostsModel.get(i); + arr.push({ + id: r.id, label: r.label, host: r.host, username: r.username, + domain: r.domain, mountRoot: r.mountRoot, vers: r.vers + }); + } + building = true; + store.json = JSON.stringify(arr); + building = false; + } + + function newId() { + return "h" + Date.now().toString(36) + Math.floor(Math.random() * 100000).toString(36); + } + + Component.onCompleted: rebuildModel() + + // Runs kwallet-query to store/remove per-host passwords. + 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"]); } + } + function run(cmd, cb) { + var key = cmd + " tok" + (nonce++); + cbs[key] = cb || function () {}; + connectSource(key); + } + } + + header: Kirigami.InlineMessage { + Layout.fillWidth: true + visible: hostsModel.count === 0 + type: Kirigami.MessageType.Information + text: i18n("No NAS hosts yet. Add a Synology or SMB server to browse its shares.") + } + + view: ListView { + id: hostsView + model: hostsModel + delegate: QQC2.ItemDelegate { + id: hostDelegate + width: ListView.view.width + hoverEnabled: true + + required property int index + required property string label + required property string host + required property string username + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.Icon { + source: "network-server" + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + QQC2.Label { + Layout.fillWidth: true + text: hostDelegate.label.length ? hostDelegate.label : hostDelegate.host + elide: Text.ElideRight + } + QQC2.Label { + Layout.fillWidth: true + text: { + var u = hostDelegate.username.length ? hostDelegate.username + "@" : ""; + return u + hostDelegate.host; + } + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.7 + } + } + QQC2.ToolButton { + icon.name: "entry-edit" + QQC2.ToolTip.text: i18n("Edit") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + onClicked: hostEditor.openFor(hostDelegate.index) + } + QQC2.ToolButton { + icon.name: "list-remove" + QQC2.ToolTip.text: i18n("Remove") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + onClicked: { + var r = hostsModel.get(hostDelegate.index); + exe.run(Nas.deletePasswordCmd({ id: r.id, username: r.username })); + hostsModel.remove(hostDelegate.index); + page.persist(); + } + } + } + } + } + + footer: RowLayout { + Item { Layout.fillWidth: true } + QQC2.Button { + text: i18n("Add NAS…") + icon.name: "list-add" + onClicked: hostEditor.openFor(-1) + } + } + + // --- add / edit dialog --------------------------------------------------- + Kirigami.Dialog { + id: hostEditor + title: editIndex === -1 ? i18n("Add NAS") : i18n("Edit NAS") + preferredWidth: Kirigami.Units.gridUnit * 24 + standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + + property int editIndex: -1 + property string editId: "" + + function openFor(index) { + editIndex = index; + if (index === -1) { + editId = ""; + labelField.text = ""; + hostField.text = ""; + userField.text = ""; + passField.text = ""; + domainField.text = ""; + mountRootField.text = ""; + versField.text = ""; + } else { + var r = hostsModel.get(index); + editId = r.id; + labelField.text = r.label; + hostField.text = r.host; + userField.text = r.username; + passField.text = ""; + domainField.text = r.domain; + mountRootField.text = r.mountRoot; + versField.text = r.vers; + } + open(); + } + + onAccepted: { + if (hostField.text.trim().length === 0) + return; + var host = { + id: editIndex === -1 ? page.newId() : editId, + label: labelField.text.trim(), + host: hostField.text.trim(), + username: userField.text.trim(), + domain: domainField.text.trim(), + mountRoot: mountRootField.text.trim(), + vers: versField.text.trim() + }; + if (editIndex === -1) + hostsModel.append(host); + else + hostsModel.set(editIndex, host); + page.persist(); + // Only touch KWallet when a password was actually entered. + if (passField.text.length > 0) + exe.run(Nas.savePasswordCmd(host, passField.text)); + passField.text = ""; + } + + Kirigami.FormLayout { + QQC2.TextField { + id: labelField + Kirigami.FormData.label: i18n("Display name:") + placeholderText: i18n("e.g. Home NAS (optional)") + } + QQC2.TextField { + id: hostField + Kirigami.FormData.label: i18n("Host or IP:") + placeholderText: i18n("e.g. 192.168.1.10 or nas.local") + } + QQC2.TextField { + id: userField + Kirigami.FormData.label: i18n("Username:") + } + Kirigami.PasswordField { + id: passField + Kirigami.FormData.label: i18n("Password:") + placeholderText: hostEditor.editIndex === -1 ? "" : i18n("Leave blank to keep current") + } + QQC2.TextField { + id: domainField + Kirigami.FormData.label: i18n("Domain:") + placeholderText: i18n("optional") + } + Item { Kirigami.FormData.isSection: true } + QQC2.TextField { + id: mountRootField + Kirigami.FormData.label: i18n("Mount folder override:") + placeholderText: i18n("optional") + } + QQC2.TextField { + id: versField + Kirigami.FormData.label: i18n("SMB version override:") + placeholderText: i18n("optional, e.g. 3.0") + } + } + } +}