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