1 #include "./syncthingdirectorymodel.h"
2 #include "./colors.h"
3 #include "./syncthingicons.h"
4
5 #include <syncthingconnector/syncthingconnection.h>
6 #include <syncthingconnector/utils.h>
7
8 #include <c++utilities/conversion/stringconversion.h>
9
10 #include <QStringBuilder>
11
12 using namespace std;
13 using namespace CppUtilities;
14
15 namespace Data {
16
computeDirectoryRowCount(const SyncthingDir & dir)17 static int computeDirectoryRowCount(const SyncthingDir &dir)
18 {
19 return dir.paused ? 8 : 10;
20 }
21
SyncthingDirectoryModel(SyncthingConnection & connection,QObject * parent)22 SyncthingDirectoryModel::SyncthingDirectoryModel(SyncthingConnection &connection, QObject *parent)
23 : SyncthingModel(connection, parent)
24 , m_dirs(connection.dirInfo())
25 {
26 updateRowCount();
27 connect(&m_connection, &SyncthingConnection::dirStatusChanged, this, &SyncthingDirectoryModel::dirStatusChanged);
28 }
29
roleNames() const30 QHash<int, QByteArray> SyncthingDirectoryModel::roleNames() const
31 {
32 const static QHash<int, QByteArray> roles{
33 { Qt::DisplayRole, "name" },
34 { DirectoryStatus, "status" },
35 { Qt::DecorationRole, "statusIcon" },
36 { DirectoryStatusString, "statusString" },
37 { DirectoryStatusColor, "statusColor" },
38 { DirectoryPaused, "paused" },
39 { DirectoryId, "dirId" },
40 { DirectoryPath, "path" },
41 { DirectoryPullErrorCount, "pullErrorCount" },
42 { DirectoryDetail, "detail" },
43 { DirectoryDetailIcon, "detailIcon" },
44 };
45 return roles;
46 }
47
colorRoles() const48 const QVector<int> &SyncthingDirectoryModel::colorRoles() const
49 {
50 static const QVector<int> colorRoles({ Qt::DecorationRole, Qt::ForegroundRole, DirectoryStatusColor, DirectoryDetailIcon });
51 return colorRoles;
52 }
53
54 /*!
55 * \brief Returns the directory info for the spcified \a index. The returned object is not persistent.
56 */
dirInfo(const QModelIndex & index) const57 const SyncthingDir *SyncthingDirectoryModel::dirInfo(const QModelIndex &index) const
58 {
59 return (index.parent().isValid() ? dirInfo(index.parent())
60 : (static_cast<size_t>(index.row()) < m_dirs.size() ? &m_dirs[static_cast<size_t>(index.row())] : nullptr));
61 }
62
index(int row,int column,const QModelIndex & parent) const63 QModelIndex SyncthingDirectoryModel::index(int row, int column, const QModelIndex &parent) const
64 {
65 if (!parent.isValid()) {
66 // top-level: all dir labels/IDs
67 if (row < rowCount(parent)) {
68 return createIndex(row, column, static_cast<quintptr>(-1));
69 }
70 } else if (!parent.parent().isValid()) {
71 // dir-level: dir attributes
72 if (row < rowCount(parent)) {
73 return createIndex(row, column, static_cast<quintptr>(parent.row()));
74 }
75 }
76 return QModelIndex();
77 }
78
parent(const QModelIndex & child) const79 QModelIndex SyncthingDirectoryModel::parent(const QModelIndex &child) const
80 {
81 return child.internalId() != static_cast<quintptr>(-1) ? index(static_cast<int>(child.internalId()), 0, QModelIndex()) : QModelIndex();
82 }
83
headerData(int section,Qt::Orientation orientation,int role) const84 QVariant SyncthingDirectoryModel::headerData(int section, Qt::Orientation orientation, int role) const
85 {
86 switch (orientation) {
87 case Qt::Horizontal:
88 switch (role) {
89 case Qt::DisplayRole:
90 switch (section) {
91 case 0:
92 return tr("ID");
93 case 1:
94 return tr("Status");
95 }
96 break;
97 default:;
98 }
99 break;
100 default:;
101 }
102 return QVariant();
103 }
104
data(const QModelIndex & index,int role) const105 QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
106 {
107 if (!index.isValid()) {
108 return QVariant();
109 }
110 if (index.parent().isValid()) {
111 // dir attributes
112 if (static_cast<size_t>(index.parent().row()) >= m_dirs.size()) {
113 return QVariant();
114 }
115 const SyncthingDir &dir = m_dirs[static_cast<size_t>(index.parent().row())];
116 const auto row = dir.paused && index.row() > 1 ? index.row() + 2 : index.row();
117 switch (role) {
118 case Qt::DisplayRole:
119 case Qt::EditRole:
120 if (index.column() == 0) {
121 // attribute names
122 switch (row) {
123 case 0:
124 return tr("ID");
125 case 1:
126 return tr("Path");
127 case 2:
128 return tr("Global status");
129 case 3:
130 return tr("Local status");
131 case 4:
132 return tr("Shared with");
133 case 5:
134 return tr("Type");
135 case 6:
136 return tr("Rescan interval");
137 case 7:
138 return tr("Last scan");
139 case 8:
140 return tr("Last file");
141 case 9:
142 return tr("Errors");
143 }
144 break;
145 }
146 [[fallthrough]];
147 case DirectoryDetail:
148 if (index.column() == 1 || role == DirectoryDetail) {
149 // attribute values
150 switch (row) {
151 case 0:
152 return dir.id;
153 case 1:
154 return dir.path;
155 case 2:
156 return directoryStatusString(dir.globalStats);
157 case 3:
158 return directoryStatusString(dir.localStats);
159 case 4:
160 if (!dir.deviceNames.isEmpty()) {
161 return dir.deviceNames.join(QStringLiteral(", "));
162 } else if (!dir.deviceIds.isEmpty()) {
163 return dir.deviceIds.join(QStringLiteral(", "));
164 } else {
165 return tr("not shared");
166 }
167 case 5:
168 return dir.dirTypeString();
169 case 6:
170 return rescanIntervalString(dir.rescanInterval, dir.fileSystemWatcherEnabled);
171 case 7:
172 return dir.lastScanTime.isNull() ? tr("unknown")
173 : QString::fromLatin1(dir.lastScanTime.toString(DateTimeOutputFormat::DateAndTime, true).data());
174 case 8:
175 return dir.lastFileName.isEmpty() ? tr("unknown") : dir.lastFileName;
176 case 9:
177 if (dir.globalError.isEmpty() && !dir.pullErrorCount) {
178 return tr("none");
179 }
180 if (!dir.pullErrorCount) {
181 return dir.globalError;
182 }
183 if (dir.globalError.isEmpty()) {
184 return tr("%1 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount);
185 }
186 return tr("%1 and %2 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.globalError).arg(dir.pullErrorCount);
187 }
188 }
189 break;
190 case Qt::DecorationRole:
191 case DirectoryDetailIcon:
192 if (index.column() == 0) {
193 // attribute icons
194 const auto &icons = m_brightColors ? fontAwesomeIconsForDarkTheme() : fontAwesomeIconsForLightTheme();
195 switch (row) {
196 case 0:
197 return icons.hashtag;
198 case 1:
199 return icons.folderOpen;
200 case 2:
201 return icons.globe;
202 case 3:
203 return icons.home;
204 case 4:
205 return icons.shareAlt;
206 case 5:
207 return icons.cogs;
208 case 6:
209 return icons.refresh;
210 case 7:
211 return icons.clock;
212 case 8:
213 return icons.exchangeAlt;
214 case 9:
215 return icons.exclamationTriangle;
216 }
217 }
218 break;
219 case Qt::ForegroundRole:
220 switch (index.column()) {
221 case 1:
222 switch (row) {
223 case 4:
224 if (dir.deviceIds.isEmpty()) {
225 return Colors::gray(m_brightColors);
226 }
227 break;
228 case 7:
229 if (dir.lastScanTime.isNull()) {
230 return Colors::gray(m_brightColors);
231 }
232 break;
233 case 8:
234 return dir.lastFileName.isEmpty() ? Colors::gray(m_brightColors)
235 : (dir.lastFileDeleted ? Colors::red(m_brightColors) : QVariant());
236 case 9:
237 return dir.globalError.isEmpty() && !dir.pullErrorCount ? Colors::gray(m_brightColors) : Colors::red(m_brightColors);
238 }
239 }
240 break;
241 case Qt::ToolTipRole:
242 switch (index.column()) {
243 case 1:
244 switch (row) {
245 case 3:
246 if (dir.deviceNames.isEmpty()) {
247 return dir.deviceIds.join(QChar('\n'));
248 } else {
249 return QString(
250 dir.deviceNames.join(QStringLiteral(", ")) % QChar('\n') % QChar('(') % dir.deviceIds.join(QChar('\n')) % QChar(')'));
251 }
252 case 7:
253 if (!dir.lastScanTime.isNull()) {
254 return agoString(dir.lastScanTime);
255 }
256 break;
257 case 8:
258 if (!dir.lastFileTime.isNull()) {
259 if (dir.lastFileDeleted) {
260 return tr("Deleted at %1")
261 .arg(QString::fromStdString(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true)));
262 } else {
263 return tr("Updated at %1")
264 .arg(QString::fromStdString(dir.lastFileTime.toString(DateTimeOutputFormat::DateAndTime, true)));
265 }
266 }
267 break;
268 case 9:
269 if (!dir.itemErrors.empty()) {
270 QStringList errors;
271 errors.reserve(static_cast<int>(dir.itemErrors.size()));
272 for (const auto &error : dir.itemErrors) {
273 errors << error.path;
274 }
275 return QVariant(QStringLiteral("<b>") % tr("Failed items") % QStringLiteral("</b><ul><li>")
276 % errors.join(QStringLiteral("</li><li>")) % QStringLiteral("</li></ul>") % tr("Click for details"));
277 }
278 }
279 }
280 break;
281 default:;
282 }
283 return QVariant();
284 }
285
286 if (static_cast<size_t>(index.row()) >= m_dirs.size()) {
287 return QVariant();
288 }
289
290 // dir IDs and status
291 const SyncthingDir &dir = m_dirs[static_cast<size_t>(index.row())];
292 switch (role) {
293 case Qt::DisplayRole:
294 case Qt::EditRole:
295 switch (index.column()) {
296 case 0:
297 return dir.label.isEmpty() ? dir.id : dir.label;
298 case 1:
299 return dirStatusString(dir);
300 }
301 break;
302 case Qt::DecorationRole:
303 switch (index.column()) {
304 case 0:
305 if (dir.paused && dir.status != SyncthingDirStatus::OutOfSync) {
306 return statusIcons().pause;
307 } else if (dir.deviceIds.empty()) {
308 return statusIcons().disconnected; // "unshared" status
309 } else {
310 switch (dir.status) {
311 case SyncthingDirStatus::Unknown:
312 return statusIcons().disconnected;
313 case SyncthingDirStatus::Idle:
314 case SyncthingDirStatus::Cleaning:
315 case SyncthingDirStatus::WaitingToClean:
316 return statusIcons().idling;
317 case SyncthingDirStatus::WaitingToScan:
318 case SyncthingDirStatus::Scanning:
319 return statusIcons().scanninig;
320 case SyncthingDirStatus::WaitingToSync:
321 case SyncthingDirStatus::PreparingToSync:
322 case SyncthingDirStatus::Synchronizing:
323 return statusIcons().sync;
324 case SyncthingDirStatus::OutOfSync:
325 return statusIcons().error;
326 }
327 }
328 break;
329 }
330 break;
331 case Qt::TextAlignmentRole:
332 switch (index.column()) {
333 case 0:
334 break;
335 case 1:
336 return static_cast<int>(Qt::AlignRight | Qt::AlignVCenter);
337 }
338 break;
339 case Qt::ForegroundRole:
340 switch (index.column()) {
341 case 0:
342 break;
343 case 1:
344 return dirStatusColor(dir);
345 }
346 break;
347 case DirectoryStatus:
348 return static_cast<int>(dir.status);
349 case DirectoryPaused:
350 return dir.paused;
351 case DirectoryStatusString:
352 return dirStatusString(dir);
353 case DirectoryStatusColor:
354 return dirStatusColor(dir);
355 case DirectoryId:
356 return dir.id;
357 case DirectoryPath:
358 return dir.path;
359 case DirectoryPullErrorCount:
360 return dir.pullErrorCount;
361 default:;
362 }
363
364 return QVariant();
365 }
366
setData(const QModelIndex & index,const QVariant & value,int role)367 bool SyncthingDirectoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
368 {
369 Q_UNUSED(index)
370 Q_UNUSED(value)
371 Q_UNUSED(role)
372 return false;
373 }
374
rowCount(const QModelIndex & parent) const375 int SyncthingDirectoryModel::rowCount(const QModelIndex &parent) const
376 {
377 if (!parent.isValid()) {
378 return static_cast<int>(m_dirs.size());
379 } else if (!parent.parent().isValid() && static_cast<size_t>(parent.row()) < m_rowCount.size()) {
380 return m_rowCount[static_cast<size_t>(parent.row())];
381 } else {
382 return 0;
383 }
384 }
385
columnCount(const QModelIndex & parent) const386 int SyncthingDirectoryModel::columnCount(const QModelIndex &parent) const
387 {
388 if (!parent.isValid()) {
389 return 2; // label/ID, status/buttons
390 } else if (!parent.parent().isValid()) {
391 return 2; // field name and value
392 } else {
393 return 0;
394 }
395 }
396
dirStatusChanged(const SyncthingDir & dir,int index)397 void SyncthingDirectoryModel::dirStatusChanged(const SyncthingDir &dir, int index)
398 {
399 if (index < 0 || static_cast<size_t>(index) >= m_rowCount.size()) {
400 return;
401 }
402
403 // update top-level indizes
404 const QModelIndex modelIndex1(this->index(index, 0, QModelIndex()));
405 static const QVector<int> modelRoles1({ Qt::DisplayRole, Qt::EditRole, Qt::DecorationRole, DirectoryPaused, DirectoryStatus,
406 DirectoryStatusString, DirectoryStatusColor, DirectoryId, DirectoryPath, DirectoryPullErrorCount });
407 emit dataChanged(modelIndex1, modelIndex1, modelRoles1);
408 const QModelIndex modelIndex2(this->index(index, 1, QModelIndex()));
409 static const QVector<int> modelRoles2({ Qt::DisplayRole, Qt::EditRole, Qt::ForegroundRole });
410 emit dataChanged(modelIndex2, modelIndex2, modelRoles2);
411
412 // remove/insert detail rows
413 const auto oldRowCount = m_rowCount[static_cast<size_t>(index)];
414 const auto newRowCount = computeDirectoryRowCount(dir);
415 const auto newLastRow = newRowCount - 1;
416 if (oldRowCount > newRowCount) {
417 // begin removing rows for statistics
418 beginRemoveRows(modelIndex1, 2, 3);
419 m_rowCount[static_cast<size_t>(index)] = newRowCount;
420 endRemoveRows();
421 } else if (newRowCount > oldRowCount) {
422 // begin inserting rows for statistics
423 beginInsertRows(modelIndex1, 2, 3);
424 m_rowCount[static_cast<size_t>(index)] = newRowCount;
425 endInsertRows();
426 }
427
428 // update detail rows
429 static const QVector<int> modelRoles3({ Qt::DisplayRole, Qt::EditRole, Qt::ToolTipRole });
430 emit dataChanged(this->index(0, 1, modelIndex1), this->index(newLastRow, 1, modelIndex1), modelRoles3);
431 static const QVector<int> modelRoles4({ Qt::DisplayRole, Qt::EditRole, DirectoryDetail });
432 emit dataChanged(this->index(0, 0, modelIndex1), this->index(newLastRow, 0, modelIndex1), modelRoles4);
433 }
434
handleConfigInvalidated()435 void SyncthingDirectoryModel::handleConfigInvalidated()
436 {
437 beginResetModel();
438 }
439
handleNewConfigAvailable()440 void SyncthingDirectoryModel::handleNewConfigAvailable()
441 {
442 updateRowCount();
443 endResetModel();
444 }
445
handleStatusIconsChanged()446 void SyncthingDirectoryModel::handleStatusIconsChanged()
447 {
448 emit dataChanged(index(0, 0), index(static_cast<int>(m_dirs.size()) - 1, 0), QVector<int>({ Qt::DecorationRole }));
449 }
450
dirStatusString(const SyncthingDir & dir)451 QString SyncthingDirectoryModel::dirStatusString(const SyncthingDir &dir)
452 {
453 if (dir.paused && dir.status != SyncthingDirStatus::OutOfSync) {
454 return tr("Paused");
455 }
456 if (dir.isUnshared()) {
457 return tr("Unshared");
458 }
459 switch (dir.status) {
460 case SyncthingDirStatus::Unknown:
461 return dir.rawStatus.isEmpty() ? tr("Unknown status") : QString(dir.rawStatus);
462 case SyncthingDirStatus::Idle:
463 return tr("Idle");
464 case SyncthingDirStatus::WaitingToScan:
465 return tr("Waiting to scan");
466 case SyncthingDirStatus::Scanning:
467 if (dir.scanningPercentage > 0) {
468 if (dir.scanningRate != 0.0) {
469 return tr("Scanning (%1 %, %2)").arg(dir.scanningPercentage).arg(bitrateToString(dir.scanningRate * 0.008, true).data());
470 }
471 return tr("Scanning (%1 %)").arg(dir.scanningPercentage);
472 }
473 return tr("Scanning");
474 case SyncthingDirStatus::WaitingToSync:
475 return tr("Waiting to sync");
476 case SyncthingDirStatus::PreparingToSync:
477 return tr("Preparing to sync");
478 case SyncthingDirStatus::Synchronizing:
479 return dir.completionPercentage > 0 ? tr("Synchronizing (%1 %)").arg(dir.completionPercentage) : tr("Synchronizing");
480 case SyncthingDirStatus::Cleaning:
481 return tr("Cleaning");
482 case SyncthingDirStatus::WaitingToClean:
483 return tr("Waiting to clean");
484 case SyncthingDirStatus::OutOfSync:
485 return tr("Out of sync");
486 }
487 return QString();
488 }
489
dirStatusColor(const SyncthingDir & dir) const490 QVariant SyncthingDirectoryModel::dirStatusColor(const SyncthingDir &dir) const
491 {
492 if (dir.paused && dir.status != SyncthingDirStatus::OutOfSync) {
493 return QVariant();
494 }
495 if (dir.isUnshared()) {
496 return Colors::orange(m_brightColors);
497 }
498 switch (dir.status) {
499 case SyncthingDirStatus::Unknown:
500 break;
501 case SyncthingDirStatus::Idle:
502 return Colors::green(m_brightColors);
503 case SyncthingDirStatus::WaitingToScan:
504 case SyncthingDirStatus::WaitingToSync:
505 case SyncthingDirStatus::WaitingToClean:
506 return Colors::orange(m_brightColors);
507 case SyncthingDirStatus::Scanning:
508 case SyncthingDirStatus::PreparingToSync:
509 case SyncthingDirStatus::Synchronizing:
510 case SyncthingDirStatus::Cleaning:
511 return Colors::blue(m_brightColors);
512 case SyncthingDirStatus::OutOfSync:
513 return Colors::red(m_brightColors);
514 }
515 return QVariant();
516 }
517
updateRowCount()518 void SyncthingDirectoryModel::updateRowCount()
519 {
520 m_rowCount.clear();
521 m_rowCount.reserve(m_dirs.size());
522 for (const auto &dir : m_dirs) {
523 m_rowCount.emplace_back(computeDirectoryRowCount(dir));
524 }
525 }
526
527 } // namespace Data
528