1 /*
2 * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15 #include "syncstatussummary.h"
16 #include "accountfwd.h"
17 #include "folderman.h"
18 #include "navigationpanehelper.h"
19 #include "networkjobs.h"
20 #include "syncresult.h"
21 #include "tray/usermodel.h"
22
23 #include <theme.h>
24
25 namespace {
26
determineSyncStatus(const OCC::SyncResult & syncResult)27 OCC::SyncResult::Status determineSyncStatus(const OCC::SyncResult &syncResult)
28 {
29 const auto status = syncResult.status();
30
31 if (status == OCC::SyncResult::Success || status == OCC::SyncResult::Problem) {
32 if (syncResult.hasUnresolvedConflicts()) {
33 return OCC::SyncResult::Problem;
34 }
35 return OCC::SyncResult::Success;
36 } else if (status == OCC::SyncResult::SyncPrepare || status == OCC::SyncResult::Undefined) {
37 return OCC::SyncResult::SyncRunning;
38 }
39 return status;
40 }
41 }
42
43 namespace OCC {
44
45 Q_LOGGING_CATEGORY(lcSyncStatusModel, "nextcloud.gui.syncstatusmodel", QtInfoMsg)
46
SyncStatusSummary(QObject * parent)47 SyncStatusSummary::SyncStatusSummary(QObject *parent)
48 : QObject(parent)
49 {
50 const auto folderMan = FolderMan::instance();
51 connect(folderMan, &FolderMan::folderListChanged, this, &SyncStatusSummary::onFolderListChanged);
52 connect(folderMan, &FolderMan::folderSyncStateChange, this, &SyncStatusSummary::onFolderSyncStateChanged);
53 }
54
reloadNeeded(AccountState * accountState) const55 bool SyncStatusSummary::reloadNeeded(AccountState *accountState) const
56 {
57 if (_accountState.data() == accountState) {
58 return false;
59 }
60 return true;
61 }
62
load()63 void SyncStatusSummary::load()
64 {
65 const auto currentUser = UserModel::instance()->currentUser();
66 if (!currentUser) {
67 return;
68 }
69 setAccountState(currentUser->accountState());
70 clearFolderErrors();
71 connectToFoldersProgress(FolderMan::instance()->map());
72 initSyncState();
73 }
74
syncProgress() const75 double SyncStatusSummary::syncProgress() const
76 {
77 return _progress;
78 }
79
syncIcon() const80 QUrl SyncStatusSummary::syncIcon() const
81 {
82 return _syncIcon;
83 }
84
syncing() const85 bool SyncStatusSummary::syncing() const
86 {
87 return _isSyncing;
88 }
89
onFolderListChanged(const OCC::Folder::Map & folderMap)90 void SyncStatusSummary::onFolderListChanged(const OCC::Folder::Map &folderMap)
91 {
92 connectToFoldersProgress(folderMap);
93 }
94
markFolderAsError(const Folder * folder)95 void SyncStatusSummary::markFolderAsError(const Folder *folder)
96 {
97 _foldersWithErrors.insert(folder->alias());
98 }
99
markFolderAsSuccess(const Folder * folder)100 void SyncStatusSummary::markFolderAsSuccess(const Folder *folder)
101 {
102 _foldersWithErrors.erase(folder->alias());
103 }
104
folderErrors() const105 bool SyncStatusSummary::folderErrors() const
106 {
107 return _foldersWithErrors.size() != 0;
108 }
109
folderError(const Folder * folder) const110 bool SyncStatusSummary::folderError(const Folder *folder) const
111 {
112 return _foldersWithErrors.find(folder->alias()) != _foldersWithErrors.end();
113 }
114
clearFolderErrors()115 void SyncStatusSummary::clearFolderErrors()
116 {
117 _foldersWithErrors.clear();
118 }
119
setSyncStateForFolder(const Folder * folder)120 void SyncStatusSummary::setSyncStateForFolder(const Folder *folder)
121 {
122 if (_accountState && !_accountState->isConnected()) {
123 setSyncing(false);
124 setSyncStatusString(tr("Offline"));
125 setSyncStatusDetailString("");
126 setSyncIcon(Theme::instance()->folderOffline());
127 return;
128 }
129
130 const auto state = determineSyncStatus(folder->syncResult());
131
132 switch (state) {
133 case SyncResult::Success:
134 case SyncResult::SyncPrepare:
135 // Success should only be shown if all folders were fine
136 if (!folderErrors() || folderError(folder)) {
137 setSyncing(false);
138 setSyncStatusString(tr("All synced!"));
139 setSyncStatusDetailString("");
140 setSyncIcon(Theme::instance()->syncStatusOk());
141 markFolderAsSuccess(folder);
142 }
143 break;
144 case SyncResult::Error:
145 case SyncResult::SetupError:
146 setSyncing(false);
147 setSyncStatusString(tr("Some files couldn't be synced!"));
148 setSyncStatusDetailString(tr("See below for errors"));
149 setSyncIcon(Theme::instance()->syncStatusError());
150 markFolderAsError(folder);
151 break;
152 case SyncResult::SyncRunning:
153 case SyncResult::NotYetStarted:
154 setSyncing(true);
155 setSyncStatusString(tr("Syncing"));
156 setSyncStatusDetailString("");
157 setSyncIcon(Theme::instance()->syncStatusRunning());
158 break;
159 case SyncResult::Paused:
160 case SyncResult::SyncAbortRequested:
161 setSyncing(false);
162 setSyncStatusString(tr("Sync paused"));
163 setSyncStatusDetailString("");
164 setSyncIcon(Theme::instance()->syncStatusPause());
165 break;
166 case SyncResult::Problem:
167 case SyncResult::Undefined:
168 setSyncing(false);
169 setSyncStatusString(tr("Some files could not be synced!"));
170 setSyncStatusDetailString(tr("See below for warnings"));
171 setSyncIcon(Theme::instance()->syncStatusWarning());
172 markFolderAsError(folder);
173 break;
174 }
175 }
176
onFolderSyncStateChanged(const Folder * folder)177 void SyncStatusSummary::onFolderSyncStateChanged(const Folder *folder)
178 {
179 if (!folder) {
180 return;
181 }
182
183 if (!_accountState || folder->accountState() != _accountState.data()) {
184 return;
185 }
186
187 setSyncStateForFolder(folder);
188 }
189
calculateOverallPercent(qint64 totalFileCount,qint64 completedFile,qint64 totalSize,qint64 completedSize)190 constexpr double calculateOverallPercent(
191 qint64 totalFileCount, qint64 completedFile, qint64 totalSize, qint64 completedSize)
192 {
193 int overallPercent = 0;
194 if (totalFileCount > 0) {
195 // Add one 'byte' for each file so the percentage is moving when deleting or renaming files
196 overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
197 }
198 overallPercent = qBound(0, overallPercent, 100);
199 return overallPercent / 100.0;
200 }
201
onFolderProgressInfo(const ProgressInfo & progress)202 void SyncStatusSummary::onFolderProgressInfo(const ProgressInfo &progress)
203 {
204 const qint64 completedSize = progress.completedSize();
205 const qint64 currentFile = progress.currentFile();
206 const qint64 completedFile = progress.completedFiles();
207 const qint64 totalSize = qMax(completedSize, progress.totalSize());
208 const qint64 totalFileCount = qMax(currentFile, progress.totalFiles());
209
210 setSyncProgress(calculateOverallPercent(totalFileCount, completedFile, totalSize, completedSize));
211
212 if (totalSize > 0) {
213 const auto completedSizeString = Utility::octetsToString(completedSize);
214 const auto totalSizeString = Utility::octetsToString(totalSize);
215
216 if (progress.trustEta()) {
217 setSyncStatusDetailString(
218 tr("%1 of %2 · %3 left")
219 .arg(completedSizeString, totalSizeString)
220 .arg(Utility::durationToDescriptiveString1(progress.totalProgress().estimatedEta)));
221 } else {
222 setSyncStatusDetailString(tr("%1 of %2").arg(completedSizeString, totalSizeString));
223 }
224 }
225
226 if (totalFileCount > 0) {
227 setSyncStatusString(tr("Syncing file %1 of %2").arg(currentFile).arg(totalFileCount));
228 }
229 }
230
setSyncing(bool value)231 void SyncStatusSummary::setSyncing(bool value)
232 {
233 if (value == _isSyncing) {
234 return;
235 }
236
237 _isSyncing = value;
238 emit syncingChanged();
239 }
240
setSyncProgress(double value)241 void SyncStatusSummary::setSyncProgress(double value)
242 {
243 if (_progress == value) {
244 return;
245 }
246
247 _progress = value;
248 emit syncProgressChanged();
249 }
250
setSyncStatusString(const QString & value)251 void SyncStatusSummary::setSyncStatusString(const QString &value)
252 {
253 if (_syncStatusString == value) {
254 return;
255 }
256
257 _syncStatusString = value;
258 emit syncStatusStringChanged();
259 }
260
syncStatusString() const261 QString SyncStatusSummary::syncStatusString() const
262 {
263 return _syncStatusString;
264 }
265
syncStatusDetailString() const266 QString SyncStatusSummary::syncStatusDetailString() const
267 {
268 return _syncStatusDetailString;
269 }
270
setSyncIcon(const QUrl & value)271 void SyncStatusSummary::setSyncIcon(const QUrl &value)
272 {
273 if (_syncIcon == value) {
274 return;
275 }
276
277 _syncIcon = value;
278 emit syncIconChanged();
279 }
280
setSyncStatusDetailString(const QString & value)281 void SyncStatusSummary::setSyncStatusDetailString(const QString &value)
282 {
283 if (_syncStatusDetailString == value) {
284 return;
285 }
286
287 _syncStatusDetailString = value;
288 emit syncStatusDetailStringChanged();
289 }
290
connectToFoldersProgress(const Folder::Map & folderMap)291 void SyncStatusSummary::connectToFoldersProgress(const Folder::Map &folderMap)
292 {
293 for (const auto &folder : folderMap) {
294 if (folder->accountState() == _accountState.data()) {
295 connect(
296 folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo, Qt::UniqueConnection);
297 } else {
298 disconnect(folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo);
299 }
300 }
301 }
302
onIsConnectedChanged()303 void SyncStatusSummary::onIsConnectedChanged()
304 {
305 setSyncStateToConnectedState();
306 }
307
setSyncStateToConnectedState()308 void SyncStatusSummary::setSyncStateToConnectedState()
309 {
310 setSyncing(false);
311 setSyncStatusDetailString("");
312 if (_accountState && !_accountState->isConnected()) {
313 setSyncStatusString(tr("Offline"));
314 setSyncIcon(Theme::instance()->folderOffline());
315 } else {
316 setSyncStatusString(tr("All synced!"));
317 setSyncIcon(Theme::instance()->syncStatusOk());
318 }
319 }
320
setAccountState(AccountStatePtr accountState)321 void SyncStatusSummary::setAccountState(AccountStatePtr accountState)
322 {
323 if (!reloadNeeded(accountState.data())) {
324 return;
325 }
326 if (_accountState) {
327 disconnect(
328 _accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
329 }
330 _accountState = accountState;
331 connect(_accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
332 }
333
initSyncState()334 void SyncStatusSummary::initSyncState()
335 {
336 auto syncStateFallbackNeeded = true;
337 for (const auto &folder : FolderMan::instance()->map()) {
338 onFolderSyncStateChanged(folder);
339 syncStateFallbackNeeded = false;
340 }
341
342 if (syncStateFallbackNeeded) {
343 setSyncStateToConnectedState();
344 }
345 }
346 }
347