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") } } } }