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,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 <mount folder>/<host>/<share>.")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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