diff --git a/tray/gui/qml/AdvancedConfigPage.qml b/tray/gui/qml/AdvancedConfigPage.qml index bf6e992c..8ad992e3 100644 --- a/tray/gui/qml/AdvancedConfigPage.qml +++ b/tray/gui/qml/AdvancedConfigPage.qml @@ -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") diff --git a/tray/gui/qml/AdvancedPage.qml b/tray/gui/qml/AdvancedPage.qml index f5323ecf..becdf175 100644 --- a/tray/gui/qml/AdvancedPage.qml +++ b/tray/gui/qml/AdvancedPage.qml @@ -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) } } diff --git a/tray/gui/qml/DevConfigPage.qml b/tray/gui/qml/DevConfigPage.qml index 49804fc9..af278605 100644 --- a/tray/gui/qml/DevConfigPage.qml +++ b/tray/gui/qml/DevConfigPage.qml @@ -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).")}, + ] + }) } diff --git a/tray/gui/qml/ObjectConfigDelegate.qml b/tray/gui/qml/ObjectConfigDelegate.qml index 7eaf81bd..3a31a08b 100644 --- a/tray/gui/qml/ObjectConfigDelegate.qml +++ b/tray/gui/qml/ObjectConfigDelegate.qml @@ -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 @@ -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, diff --git a/tray/gui/qml/ObjectConfigPage.qml b/tray/gui/qml/ObjectConfigPage.qml index 447f4597..52a88f7c 100644 --- a/tray/gui/qml/ObjectConfigPage.qml +++ b/tray/gui/qml/ObjectConfigPage.qml @@ -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); @@ -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 @@ -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 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 @@ -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); @@ -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; } } @@ -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 : ""); @@ -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];