fab2d65dd2
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>
254 lines
8.7 KiB
QML
254 lines
8.7 KiB
QML
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")
|
|
}
|
|
}
|
|
}
|
|
}
|