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) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-15 14:27:01 +02:00
parent 47046665ed
commit fab2d65dd2
2 changed files with 322 additions and 0 deletions
+253
View File
@@ -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")
}
}
}
}