Skip to content

Commit bf4132a

Browse files
committed
Improve importing/changing config in Qt Quick GUI
1 parent ce67bbb commit bf4132a

12 files changed

+118
-35
lines changed

syncthingconnector/syncthingconfig.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ static void xmlAttributesToJsonObject(QXmlStreamReader &xmlReader, QJsonObject &
113113
*/
114114
static void xmlElementToJsonValue(QXmlStreamReader &xmlReader, QJsonObject &object)
115115
{
116-
static const auto arrayElements = QHash<QString, QString>{ { QStringLiteral("device"), QStringLiteral("devices") } };
116+
static const auto arrayElements = QHash<QString, QString>{
117+
{ QStringLiteral("device"), QStringLiteral("devices") },
118+
{ QStringLiteral("address"), QStringLiteral("addresses") },
119+
};
117120
auto name = xmlReader.name().toString();
118121
auto arrayName = arrayElements.find(name);
119122
auto text = QString();

syncthingconnector/syncthingconnection.h

+1
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ public Q_SLOTS:
308308
#endif
309309
bool applySettings(Data::SyncthingConnectionSettings &connectionSettings);
310310
void applyRawConfig();
311+
void postRawConfig(const QByteArray &rawConfig);
311312

312313
// methods to initiate/close connection
313314
void connect();

syncthingconnector/syncthingconnection_requests.cpp

+11
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,17 @@ SyncthingConnection::QueryResult SyncthingConnection::postConfigFromJsonObject(
19341934
return postConfigFromByteArray(QJsonDocument(rawConfig).toJson(QJsonDocument::Compact), std::move(callback));
19351935
}
19361936

1937+
/*!
1938+
* \brief Posts the specified \a rawConfig.
1939+
* \remarks
1940+
* This function is a slot (in contrast to postConfigFromJsonObject()) and thus may be invoked via QMetaObject::invokeMethod() from
1941+
* another thread.
1942+
*/
1943+
void Data::SyncthingConnection::postRawConfig(const QByteArray &rawConfig)
1944+
{
1945+
postConfigFromByteArray(rawConfig, std::function<void(QString &&)>());
1946+
}
1947+
19371948
/*!
19381949
* \brief Posts the specified \a rawConfig.
19391950
* \param rawConfig A valid JSON document containing the configuration. It is directly passed to Syncthing.

syncthingconnector/testfiles/testconfig/config.xml

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
</folder>
4747
<device id="MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7" name="Test dev 2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
4848
<address>tcp://192.168.2.2:22001</address>
49+
<address>tcp://192.168.2.2:22002</address>
4950
<paused>true</paused>
5051
<autoAcceptFolders>false</autoAcceptFolders>
5152
<maxSendKbps>0</maxSendKbps>

syncthingconnector/tests/connectiontests.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,9 @@ void ConnectionTests::checkDevices()
447447
CPPUNIT_ASSERT_EQUAL_MESSAGE("paused device", QStringLiteral("Paused"), dev.statusString());
448448
CPPUNIT_ASSERT_EQUAL_MESSAGE("name", QStringLiteral("Test dev 2"), dev.name);
449449
CPPUNIT_ASSERT_MESSAGE("no introducer", !dev.introducer);
450-
CPPUNIT_ASSERT_EQUAL(static_cast<decltype(dev.addresses.size())>(1), dev.addresses.size());
450+
CPPUNIT_ASSERT_EQUAL(static_cast<decltype(dev.addresses.size())>(2), dev.addresses.size());
451451
CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22001"), dev.addresses.front());
452+
CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22002"), dev.addresses.back());
452453
dev2 = &dev;
453454
dev2Index = index;
454455
} else if (dev.id == QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4")) {

syncthingconnector/tests/misctests.cpp

+9-2
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,22 @@ void MiscTests::testParsingConfigWithDetails()
148148
CPPUNIT_ASSERT_EQUAL(static_cast<QJsonArray::size_type>(2), devices.size());
149149

150150
const auto device1 = devices.at(0).toObject();
151+
const auto device1Addresses = device1.value(QLatin1String("addresses")).toArray();
151152
CPPUNIT_ASSERT_EQUAL(
152153
QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"), device1.value(QLatin1String("deviceID")).toString());
153-
CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22001"), device1.value(QLatin1String("address")).toString());
154+
CPPUNIT_ASSERT_EQUAL(static_cast<QJsonArray::size_type>(2), device1Addresses.size());
155+
CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22001"), device1Addresses.first().toString());
156+
CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22002"), device1Addresses.last().toString());
157+
CPPUNIT_ASSERT_EQUAL(false, device1.contains(QStringLiteral("address")));
154158
CPPUNIT_ASSERT_EQUAL(true, device1.value(QLatin1String("paused")).toBool());
155159

156160
const auto device2 = devices.at(1).toObject();
161+
const auto device2Addresses = device2.value(QLatin1String("addresses")).toArray();
157162
CPPUNIT_ASSERT_EQUAL(
158163
QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4"), device2.value(QLatin1String("deviceID")).toString());
159-
CPPUNIT_ASSERT_EQUAL(QStringLiteral("dynamic"), device2.value(QLatin1String("address")).toString());
164+
CPPUNIT_ASSERT_EQUAL(static_cast<QJsonArray::size_type>(1), device2Addresses.size());
165+
CPPUNIT_ASSERT_EQUAL(QStringLiteral("dynamic"), device2Addresses.first().toString());
166+
CPPUNIT_ASSERT_EQUAL(false, device2.contains(QStringLiteral("address")));
160167
CPPUNIT_ASSERT_EQUAL(false, device2.value(QLatin1String("paused")).toBool());
161168
}
162169

tray/gui/qml/AdvancedConfigPage.qml

+11-5
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,13 @@ ObjectConfigPage {
7676
App.showError("Can't apply, key is already used.");
7777
return false;
7878
}
79-
advancedConfigPage.configObjectExists = true;
80-
advancedConfigPage.disableInitialProperties();
81-
App.postSyncthingConfig(cfg, (error) => (error.length === 0) && (advancedConfigPage.hasUnsavedChanges = false));
79+
App.postSyncthingConfig(cfg, (error) => {
80+
if (error.length === 0) {
81+
advancedConfigPage.configObjectExists = true;
82+
advancedConfigPage.hasUnsavedChanges = false;
83+
advancedConfigPage.disableInitialProperties();
84+
}
85+
});
8286
return true;
8387
}
8488

@@ -93,8 +97,10 @@ ObjectConfigPage {
9397
return false;
9498
}
9599
entries.splice(index, 1);
96-
advancedConfigPage.configObjectExists = false;
97-
App.postSyncthingConfig(cfg);
100+
App.postSyncthingConfig(cfg, (error) => {
101+
advancedConfigPage.configObjectExists = false;
102+
advancedConfigPage.hasUnsavedChanges = false;
103+
});
98104
return true;
99105
}
100106
}

tray/gui/qml/ImportPage.qml

+24-12
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ Page {
6161
text: qsTr("App configuration")
6262
elide: Text.ElideRight
6363
font.weight: Font.Medium
64+
wrapMode: Text.WordWrap
6465
}
6566
Label {
6667
Layout.fillWidth: true
67-
text: qsTr("Replace the app configuration with the one from the selected directory")
68+
text: qsTr("Replace the app configuration with the one from the selected directory.")
6869
elide: Text.ElideRight
6970
font.weight: Font.Light
7071
wrapMode: Text.WordWrap
@@ -95,10 +96,11 @@ Page {
9596
text: qsTr("Full Syncthing configuration and database")
9697
elide: Text.ElideRight
9798
font.weight: Font.Medium
99+
wrapMode: Text.WordWrap
98100
}
99101
Label {
100102
Layout.fillWidth: true
101-
text: qsTr("Replace entire (existing) Syncthing configuration and database with the one from the selected directory")
103+
text: qsTr("Replace entire (existing) Syncthing configuration and database with the one from the selected directory. Use this with care as restoring the database is potentially dangerous.")
102104
elide: Text.ElideRight
103105
font.weight: Font.Light
104106
wrapMode: Text.WordWrap
@@ -108,53 +110,56 @@ Page {
108110
id: fullImport
109111
onToggled: {
110112
if (fullImport.checked) {
111-
selectedFolders.checked = false;
112-
selectedDevices.checked = false;
113+
folderSelection.selectionEnabled = false;
114+
deviceSelection.selectionEnabled = false;
113115
}
114116
}
115117
}
116118
}
117119
}
118120
SelectiveImportDelegate {
121+
id: folderSelection
119122
enabled: availableSettings.folders !== undefined && !fullImport.checked
120123
iconName: "folder"
121124
text: qsTr("Selected folders")
122-
description: qsTr("Merge the selected folders into the existing Syncthing configuration - make sure paths are valid when importing folders from another physical device!")
125+
description: qsTr("Merge the selected folders into the existing Syncthing configuration. You can change paths in case they differ on this device.")
123126
dialogTitle: qsTr("Select folders to import")
124127
model: ListModel {
125128
id: foldersModel
126129
Component.onCompleted: {
127130
const folders = importPage.availableSettings.folders;
128131
if (Array.isArray(folders)) {
129-
folders.forEach((folder, index) => foldersModel.append({index: index, displayName: folder.label?.length > 0 ? folder.label : folder.id, checked: false}));
132+
folders.forEach((folder, index) => foldersModel.append({index: index, displayName: folder.label?.length > 0 ? folder.label : folder.id, path: folder.path, checked: false}));
130133
}
131134
}
132135
}
133136
}
134137
SelectiveImportDelegate {
138+
id: deviceSelection
135139
enabled: availableSettings.devices !== undefined && !fullImport.checked
136140
iconName: "sitemap"
137141
text: qsTr("Selected devices")
138-
description: qsTr("Merge the selected devices into the existing Syncthing configuration")
142+
description: qsTr("Merge the selected devices into the existing Syncthing configuration.")
139143
dialogTitle: qsTr("Select devices to import")
140144
model: ListModel {
141145
id: devicesModel
142146
Component.onCompleted: {
143147
const devices = importPage.availableSettings.devices;
144148
if (Array.isArray(devices)) {
145-
devices.forEach((device, index) => devicesModel.append({index: index, displayName: device.name?.length > 0 ? device.name : device.deviceID, checked: false}));
149+
devices.forEach((device, index) => devicesModel.append({index: index, displayName: device.name?.length > 0 ? `${device.name}\n${device.deviceID}` : device.deviceID, checked: false}));
146150
}
147151
}
148152
}
149153
}
150154
}
151155
}
152156
required property var availableSettings
157+
readonly property bool isDangerous: fullImport.checked
153158
property var selectedConfig: ({
154159
appConfig: appConfig.checked,
155160
syncthingHome: fullImport.checked,
156-
selectedFolders: getSelectedIndexes(foldersModel),
157-
selectedDevices: getSelectedIndexes(devicesModel),
161+
selectedFolders: folderSelection.selectionEnabled ? handleSelectedIndexes(foldersModel) : [],
162+
selectedDevices: deviceSelection.selectionEnabled ? handleSelectedIndexes(devicesModel) : [],
158163
})
159164
property list<Action> actions: [
160165
Action {
@@ -163,10 +168,17 @@ Page {
163168
onTriggered: (source) => App.importSettings(importPage.availableSettings, importPage.selectedConfig)
164169
}
165170
]
166-
function getSelectedIndexes(model) {
171+
function handleSelectedIndexes(model) {
167172
const indexes = [];
173+
const folders = importPage.availableSettings.folders;
168174
for (let i = 0, count = model.count; i !== count; ++i) {
169-
if (model.get(i).checked) {
175+
const modelData = model.get(i);
176+
if (modelData.checked) {
177+
// update paths
178+
const path = modelData.path;
179+
if (path !== undefined && Array.isArray(folders) && i < folders.length) {
180+
folders[i].path = path;
181+
}
170182
indexes.push(i);
171183
}
172184
}

tray/gui/qml/Main.qml

+1-1
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ ApplicationWindow {
508508
if (App.showToast(message)) {
509509
return;
510510
}
511-
notifictionToolTip.text = message;
511+
notifictionToolTip.text = notifictionToolTip.visible ? `${notifictionToolTip.text}\n${message}` : message;
512512
notifictionToolTip.open();
513513
}
514514
}

tray/gui/qml/SelectiveImportDelegate.qml

+36-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ItemDelegate {
2525
Layout.fillWidth: true
2626
elide: Text.ElideRight
2727
font.weight: Font.Medium
28+
wrapMode: Text.WordWrap
2829
}
2930
Label {
3031
id: descriptionLabel
@@ -46,10 +47,42 @@ ItemDelegate {
4647
contentItem: CustomListView {
4748
id: selectionView
4849
height: availableHeight
49-
delegate: CheckBox {
50+
delegate: ItemDelegate {
5051
width: selectionView.width
51-
text: modelData.displayName
52-
onToggled: selectionView.model.setProperty(modelData.index, "checked", checked)
52+
onClicked: {
53+
selectionCheckBox.toggle();
54+
selectionCheckBox.toggled();
55+
}
56+
contentItem: RowLayout {
57+
CheckBox {
58+
id: selectionCheckBox
59+
onToggled: selectionView.model.setProperty(modelData.index, "checked", checked)
60+
}
61+
ColumnLayout {
62+
Layout.fillWidth: true
63+
Label {
64+
Layout.fillWidth: true
65+
text: modelData.displayName
66+
font.weight: Font.Medium
67+
}
68+
ItemDelegate {
69+
Layout.fillWidth: true
70+
visible: modelData.path !== undefined
71+
height: visible ? implicitHeight : 0
72+
text: modelData.path ?? ""
73+
icon.source: App.faUrlBase + "folder-open-o"
74+
icon.width: App.iconSize
75+
icon.height: App.iconSize
76+
onClicked: folderDlg.open()
77+
}
78+
}
79+
}
80+
FolderDialog {
81+
id: folderDlg
82+
title: qsTr("Set folder path of %1").arg(modelData.displayName)
83+
currentFolder: "file://" + encodeURIComponent(modelData.path ?? "")
84+
onAccepted: selectionView.model.setProperty(modelData.index, "path", App.resolveUrl(folderDlg.selectedFolder))
85+
}
5386
required property var modelData
5487
}
5588
}

tray/gui/qml/StartPage.qml

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Page {
7979
value: remoteCompletion.percentage
8080
}
8181
Label {
82-
text: remoteProgressBar.position >= 1 ? qsTr("Up to Date") : (Number.isNan(remoteCompletion.percentage) ? qsTr("Not available") : qsTr("%1 %").arg(Math.round(remoteCompletion.percentage)))
82+
text: remoteProgressBar.position >= 1 ? qsTr("Up to Date") : (Number.isNaN(remoteCompletion.percentage) ? qsTr("Not available") : qsTr("%1 %").arg(Math.round(remoteCompletion.percentage)))
8383
font.weight: Font.Light
8484
}
8585
}

tray/gui/quick/app.cpp

+17-9
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ bool App::importSettings(const QVariantMap &availableSettings, const QVariantMap
11031103

11041104
setImportExportStatus(ImportExportStatus::Importing);
11051105

1106-
QtConcurrent::run([this, importSyncthingHome, availableSettings, selectedSettings] {
1106+
QtConcurrent::run([this, importSyncthingHome, availableSettings, selectedSettings, rawConfig = m_connection.rawConfig()] () mutable {
11071107
// copy selected files from import directory to settings directory
11081108
auto summary = QStringList();
11091109
try {
@@ -1139,23 +1139,31 @@ bool App::importSettings(const QVariantMap &availableSettings, const QVariantMap
11391139
const auto availableDevices = availableSettings.value(QStringLiteral("devices")).toJsonArray();
11401140
const auto selectedFolders = selectedSettings.value(QStringLiteral("selectedFolders")).value<QVariantList>();
11411141
const auto selectedDevices = selectedSettings.value(QStringLiteral("selectedDevices")).value<QVariantList>();
1142-
const auto defaults = m_connection.rawConfig().value(QStringLiteral("defaults")).toObject();
1142+
const auto defaults = rawConfig.value(QStringLiteral("defaults")).toObject();
11431143
const auto folderTemplate = defaults.value(QStringLiteral("folder")).toObject();
11441144
const auto deviceTemplate = defaults.value(QStringLiteral("device")).toObject();
1145-
auto folders = m_connection.rawConfig().value(QStringLiteral("folders")).toArray();
1146-
auto devices = m_connection.rawConfig().value(QStringLiteral("devices")).toArray();
1145+
const auto foldersVal = rawConfig.value(QStringLiteral("folders"));
1146+
const auto devicesVal = rawConfig.value(QStringLiteral("devices"));
1147+
if (!foldersVal.isArray() || !devicesVal.isArray()) {
1148+
return std::make_pair(tr("Unable to find folders/devices in current Syncthing config."), true);
1149+
}
1150+
auto folders = foldersVal.toArray();
1151+
auto devices = devicesVal.toArray();
11471152
const auto importedFolders = importObjects(folderTemplate, availableFolders, selectedFolders, folders);
11481153
const auto importedDevices = importObjects(deviceTemplate, availableDevices, selectedDevices, devices);
11491154
if (importedFolders || importedDevices) {
1150-
auto newConfig = m_connection.rawConfig();
11511155
if (importedFolders) {
1152-
newConfig.insert(QStringLiteral("folders"), folders);
1156+
rawConfig.insert(QStringLiteral("folders"), folders);
11531157
}
11541158
if (importedDevices) {
1155-
newConfig.insert(QStringLiteral("devices"), devices);
1159+
rawConfig.insert(QStringLiteral("devices"), devices);
1160+
}
1161+
auto newConfigJson = QJsonDocument(rawConfig).toJson(QJsonDocument::Compact);
1162+
if (QMetaObject::invokeMethod(&m_connection, &SyncthingConnection::postRawConfig, Qt::QueuedConnection, std::move(newConfigJson))) {
1163+
summary.append(tr("Merging %1 folders and %2 devices").arg(importedFolders).arg(importedDevices));
1164+
} else {
1165+
return std::make_pair(tr("Unable to import folders/devices."), true);
11561166
}
1157-
m_connection.postConfigFromJsonObject(newConfig);
1158-
summary.append(tr("Imported %1 folders and %2 devices.").arg(importedFolders).arg(importedDevices));
11591167
}
11601168
}
11611169

0 commit comments

Comments
 (0)