Skip to content

Commit

Permalink
Allow managing ignored devices and folders in Qt Quick GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
Martchus committed Jan 4, 2025
1 parent eb67bfd commit b25e461
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 27 deletions.
4 changes: 0 additions & 4 deletions tray/gui/qml/AdvancedConfigPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ ObjectConfigPage {
id: advancedConfigPage
isDangerous: true
Component.onCompleted: configObject = findConfigObject()
configTemplates: {
"devices": {deviceID: "", introducedBy: "", encryptionPassword: ""},
"addresses": "dynamic",
}
actions: [
Action {
text: qsTr("Apply")
Expand Down
15 changes: 14 additions & 1 deletion tray/gui/qml/AdvancedPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,44 @@ StackView {
anchors.fill: parent
model: ListModel {
id: model
ListElement {
key: "remoteIgnoredDevices"
label: qsTr("Ignored devices")
title: qsTr("Ignored devices")
itemLabel: qsTr("Ignored device without ID/name")
desc: qsTr("Contains the IDs of the devices that should be ignored. Connection attempts from these devices are logged to the console but never displayed in the UI.")
isDangerous: false
helpUrl: "https://docs.syncthing.net/users/config#config-option-configuration.remoteignoreddevice"
}
ListElement {
key: "gui"
label: qsTr("Syncthing API and web-based GUI")
title: qsTr("Advanced Syncthing API and GUI configuration")
isDangerous: true
}
ListElement {
key: "options"
label: qsTr("Various options")
title: qsTr("Various advanced options")
isDangerous: true
}
ListElement {
key: "defaults"
label: qsTr("Templates for new devices and folders")
title: qsTr("Templates configuration")
isDangerous: true
}
ListElement {
key: "ldap"
label: qsTr("LDAP")
title: qsTr("LDAP configuration")
isDangerous: false
}
}
delegate: ItemDelegate {
width: listView.width
text: label
onClicked: stackView.push("ObjectConfigPage.qml", {title: title, isDangerous: true, configObject: advancedPage.config[key], configCategory: `config-option-${key}`, stackView: stackView}, StackView.PushTransition)
onClicked: stackView.push("ObjectConfigPage.qml", {title: title, isDangerous: isDangerous, configObject: advancedPage.config[key], path: key, configCategory: `config-option-${key}`, itemLabel: itemLabel, helpUrl: helpUrl, stackView: stackView}, StackView.PushTransition)
}
}

Expand Down
8 changes: 8 additions & 0 deletions tray/gui/qml/DevConfigPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,13 @@ AdvancedDevConfigPage {
]},
{key: "maxRecvKbps", label: qsTr("Incoming Rate Limit (KiB/s)"), desc: qsTr("Maximum receive rate to use for this device.")},
{key: "maxSendKbps", label: qsTr("Outgoing Rate Limit (KiB/s)"), desc: qsTr("Maximum send rate to use for this device.")},
{key: "ignoredFolders", label: qsTr("Ignored folders"), itemLabel: qsTr("Ignored folder without ID/label"), desc: qsTr("The list of the folders that should be ignored. These folders will always be skipped when advertised from this remote device, i.e. they will be logged, but there will be no dialog shown."), helpUrl: "https://docs.syncthing.net/users/config#config-option-device.ignoredfolder"},
]
specialEntriesByKey: ({
"ignoredFolders.*": [
{key: "id", label: qsTr("Folder ID"), desc: qsTr("The ID of the folder to be ignored.")},
{key: "label", label: qsTr("Folder Label"), desc: qsTr("The label of the folder being ignored (for informative purposes).")},
{key: "time", label: qsTr("Time"), init: () => new Date().toISOString(), desc: qsTr("The time when this entry was added (for informative purposes).")},
]
})
}
11 changes: 6 additions & 5 deletions tray/gui/qml/ObjectConfigDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,7 @@ DelegateChooser {
text: modelData.label
elide: Text.ElideRight
font.weight: Font.Medium
readonly property string key: modelData.key
readonly property string labelKey: modelData.labelKey ?? ""
readonly property int modelIndex: modelData.index
}
ArrayElementButtons {
page: objectConfigPage
Expand All @@ -495,11 +494,13 @@ DelegateChooser {
}
onClicked: {
const currentPath = objectConfigPage.path;
const neestedPath = currentPath.length > 0 ? `${currentPath}.${modelData.key}` : modelData.key;
const configObject = objectConfigPage.configObject;
const pathKey = Array.isArray(configObject) ? "*" : modelData.key;
const neestedPath = currentPath.length > 0 ? `${currentPath}.${pathKey}` : pathKey;
objectConfigPage.stackView.push("ObjectConfigPage.qml", {
title: objNameLabel.text,
configObject: objectConfigPage.configObject[modelData.key],
parentObject: objectConfigPage.configObject,
configObject: configObject[modelData.key],
parentObject: configObject,
isDangerous: objectConfigPage.isDangerous,
readOnly: objectConfigPage.readOnly,
stackView: objectConfigPage.stackView,
Expand Down
65 changes: 48 additions & 17 deletions tray/gui/qml/ObjectConfigPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Page {
const key = specialEntry.key;
const cond = specialEntry.cond;
if ((typeof cond !== "function") || cond(objectConfigPage)) {
const init = specialEntry.init;
if (typeof init === "function") {
configObject[key] = init();
}
listModel.append(objectConfigPage.makeConfigRowForSpecialEntry(specialEntry, configObject[key], index++));
}
handledKeys.add(key);
Expand Down Expand Up @@ -78,7 +82,14 @@ Page {

property alias model: objectListView.model
property bool specialEntriesOnly: false
property var specialEntriesByKey: ({})
property var specialEntriesByKey: ({
"remoteIgnoredDevices.*": [
{key: "deviceID", label: qsTr("Device ID"), type: "deviceid", desc: qsTr("The ID of the device to be ignored.")},
{key: "name", label: qsTr("Device Name"), desc: qsTr("The name of the device being ignored (for informative purposes).")},
{key: "address", label: qsTr("Address"), desc: qsTr("The address of the device being ignored (for informative purposes).")},
{key: "time", label: qsTr("Time"), init: () => new Date().toISOString(), desc: qsTr("The time when this entry was added (for informative purposes).")},
]
})
property var specialEntries: []
property var configObject: undefined
property var parentObject: undefined
Expand All @@ -92,7 +103,13 @@ Page {
property string path: ""
property string configCategory
property string helpUrl
property var configTemplates: ({})
property var configTemplates: ({
"devices": {deviceID: "", introducedBy: "", encryptionPassword: ""},
"addresses": "dynamic",
"ignoredFolders": {id: "", label: "", time: ""},
"remoteIgnoredDevices": {deviceID: "", name: "", time: "", address: ""},
})
property list<var> labelKeys: ["label", "name", "id", "deviceID"]
readonly property int standardButtons: (configCategory.length > 0) ? (Dialog.Ok | Dialog.Cancel | Dialog.Help) : (Dialog.Ok | Dialog.Cancel)
required property StackView stackView
property Page parentPage
Expand All @@ -118,21 +135,23 @@ Page {
const value = configEntry[1];
const isArray = Array.isArray(objectConfigPage.configObject);
const row = {key: key, value: value, type: typeof value, index: index, isArray: isArray, desc: ""};
if (!isArray) {
row.label = uncamel(key);
row.labelKey = "";
} else {
const nestedKey = "deviceID";
const nestedValue = value[nestedKey];
const nestedType = typeof nestedValue;
const hasNestedValue = nestedType === "string" || nestedType === "number";
row.label = hasNestedValue ? nestedValue : (itemLabel.length ? itemLabel : uncamel(typeof value));
row.labelKey = hasNestedValue ? nestedKey : "";
}
isArray ? computeArrayElementLabel(row) : (row.label = uncamel(key));
handleReadOnlyMode(row);
return row;
}

function computeArrayElementLabel(row) {
for (const nestedKey of objectConfigPage.labelKeys) {
const nestedValue = row.value[nestedKey];
const nestedType = typeof nestedValue;
if ((nestedType === "string" && nestedValue.length > 0) || nestedType === "number") {
row.label = nestedValue;
return;
}
}
row.label = itemLabel.length ? itemLabel : uncamel(typeof row.value);
}

function makeConfigRowForSpecialEntry(specialEntry, value, index) {
specialEntry.index = index;
specialEntry.isArray = Array.isArray(objectConfigPage.configObject);
Expand All @@ -156,12 +175,21 @@ Page {
if (currentValue === value) {
return;
}

// update config object and list model, flag unsaved changes
configObject[key] = value;
objectConfigPage.hasUnsavedChanges = true;
listModel.setProperty(index, "value", value);
if (Array.isArray(objectConfigPage.parentPage?.configObject) && objectConfigPage.objectNameLabel?.labelKey === key) {
objectConfigPage.title = value;
objectConfigPage.objectNameLabel.text = value;

// update object name label (in parent page) and title for array elements
const parentPage = objectConfigPage.parentPage;
if (Array.isArray(parentPage?.configObject)) {
const parentRow = parentPage.model.get(objectConfigPage.objectNameLabel.modelIndex);
const parentValue = parentRow.value ?? {};
parentValue[key] = value;
parentRow.value = parentValue;
parentPage.computeArrayElementLabel(parentRow);
objectConfigPage.objectNameLabel.text = objectConfigPage.title = parentRow.label;
}
}

Expand Down Expand Up @@ -233,7 +261,7 @@ Page {
function showNewValueDialog(key) {
const template = objectConfigPage.childObjectTemplate;
if (template !== undefined) {
const newValue = template === "object" ? Object.assign({}, template) : template;
const newValue = typeof template === "object" ? Object.assign({}, template) : template;
objectConfigPage.addObject(key ?? objectConfigPage.configObject.length, newValue);
} else {
newValueDialog.key = key ?? (Array.isArray(objectConfigPage.configObject) ? objectConfigPage.configObject.length : "");
Expand All @@ -242,6 +270,9 @@ Page {
}

function uncamel(input) {
if (input === "id") {
return "ID";
}
input = input.replace(/(.)([A-Z][a-z]+)/g, '$1 $2').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
const parts = input.split(' ');
const lastPart = parts.splice(-1)[0];
Expand Down

0 comments on commit b25e461

Please sign in to comment.