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:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user