1 #include <QTimer>
2 #include <QHash>
3 #include <QDebug>
4 #include <algorithm> // std::sort
5
6 #include "api/server-repo.h"
7
8 #include "utils/utils.h"
9 #include "seafile-applet.h"
10 #include "account-mgr.h"
11 #include "main-window.h"
12 #include "rpc/rpc-client.h"
13 #include "rpc/clone-task.h"
14 #include "repo-service.h"
15
16 #include "repo-item.h"
17 #include "repo-tree-view.h"
18 #include "repo-tree-model.h"
19
20 namespace {
21
22 const int kRefreshLocalReposInterval = 1000;
23 const int kMaxRecentUpdatedRepos = 10;
24 const int kIndexOfVirtualReposCategory = 2;
25 const char *kReadonlyPropertyName = "is-readonly";
26
compareRepoByTimestamp(const ServerRepo & a,const ServerRepo & b)27 bool compareRepoByTimestamp(const ServerRepo& a, const ServerRepo& b)
28 {
29 return a.mtime > b.mtime;
30 }
31
makeFilterRegExp(const QString & text)32 QRegExp makeFilterRegExp(const QString& text)
33 {
34 return QRegExp(text.split(" ", QString::SkipEmptyParts).join(".*"),
35 Qt::CaseInsensitive);
36 }
37
38
39 } // namespace
40
41
RepoTreeModel(QObject * parent)42 RepoTreeModel::RepoTreeModel(QObject *parent)
43 : QStandardItemModel(parent),
44 tree_view_(NULL)
45 {
46 initialize();
47
48 refresh_local_timer_ = new QTimer(this);
49 connect(refresh_local_timer_, SIGNAL(timeout()),
50 this, SLOT(refreshLocalRepos()));
51
52 refresh_local_timer_->start(kRefreshLocalReposInterval);
53 }
54
~RepoTreeModel()55 RepoTreeModel::~RepoTreeModel()
56 {
57 if (item(kIndexOfVirtualReposCategory) != virtual_repos_category_)
58 delete virtual_repos_category_;
59 }
60
initialize()61 void RepoTreeModel::initialize()
62 {
63 recent_updated_category_ = new RepoCategoryItem(CAT_INDEX_RECENT_UPDATED, tr("Recently Updated"));
64 my_repos_category_ = new RepoCategoryItem(CAT_INDEX_MY_REPOS, tr("My Libraries"));
65 virtual_repos_category_ = new RepoCategoryItem(CAT_INDEX_VIRTUAL_REPOS, tr("Sub Libraries"));
66 shared_repos_category_ = new RepoCategoryItem(CAT_INDEX_SHARED_REPOS, tr("Shared with me"));
67 org_repos_category_ = new RepoCategoryItem(CAT_INDEX_SHARED_REPOS, tr("Shared with all"));
68 groups_root_category_ = new RepoCategoryItem(CAT_INDEX_GROUP_REPOS, tr("Shared with groups"), 0);
69 synced_repos_category_ = new RepoCategoryItem(CAT_INDEX_SYNCED_REPOS, tr("Synced Libraries"));
70
71 appendRow(recent_updated_category_);
72 appendRow(my_repos_category_);
73 // appendRow(virtual_repos_category_);
74 appendRow(shared_repos_category_);
75 appendRow(groups_root_category_);
76 appendRow(synced_repos_category_);
77
78 if (tree_view_) {
79 tree_view_->restoreExpandedCategries();
80 }
81 }
82
proxiedIndexFromItem(const QStandardItem * item)83 QModelIndex RepoTreeModel::proxiedIndexFromItem(const QStandardItem* item)
84 {
85 QSortFilterProxyModel *proxy_model = (QSortFilterProxyModel *)tree_view_->model();
86 return proxy_model->mapFromSource(indexFromItem(item));
87 }
88
clear()89 void RepoTreeModel::clear()
90 {
91 beginResetModel();
92 QStandardItemModel::clear();
93 initialize();
94 endResetModel();
95 }
96
setRepos(const std::vector<ServerRepo> & repos)97 void RepoTreeModel::setRepos(const std::vector<ServerRepo>& repos)
98 {
99 size_t i, n = repos.size();
100 // removeReposDeletedOnServer(repos);
101
102 clear();
103
104 QHash<QString, ServerRepo> map;
105 const Account& account = seafApplet->accountManager()->currentAccount();
106
107 QHash<QString, QString> merged_repo_perms;
108 // If a repo is shared read-only to org and read-write to me (or vice
109 // versa), the final permission should be read-write.
110 for (i = 0; i < n; i++) {
111 ServerRepo repo = repos[i];
112 QString exist_perm, current_perm;
113
114 if (merged_repo_perms.contains(repo.id)) {
115 exist_perm = merged_repo_perms[repo.id];
116 }
117
118 current_perm = repo.permission;
119 if (repo.owner == account.username && repo.permission == "r") {
120 // When the user shares a repo to organization with read-only
121 // permission, the returned repo has permission set to "r", we need
122 // to fix it.
123 current_perm = "rw";
124 }
125
126 if (current_perm == "rw" || exist_perm == "rw") {
127 merged_repo_perms[repo.id] = "rw";
128 } else {
129 merged_repo_perms[repo.id] = "r";
130 }
131
132 // printf("repo: %s, exist_perm: %s, current_perm: %s, final_perm: %s\n",
133 // toCStr(repo.name),
134 // toCStr(exist_perm),
135 // toCStr(current_perm),
136 // toCStr(merged_repo_perms[repo.id]));
137 }
138
139 for (i = 0; i < n; i++) {
140 ServerRepo repo = repos[i];
141 repo.permission = merged_repo_perms[repo.id];
142 // TODO: repo.readonly totally depends on repo.permission, so it should
143 // be a function instead of a variable.
144 repo.readonly = repo.permission == "r";
145
146 if (repo.isPersonalRepo()) {
147 if (!repo.isSubfolder() && !repo.isVirtual()) {
148 checkPersonalRepo(repo);
149 }
150 } else if (repo.isSharedRepo()) {
151 checkSharedRepo(repo);
152 } else if (repo.isOrgRepo()) {
153 checkOrgRepo(repo);
154 } else {
155 checkGroupRepo(repo);
156 }
157
158 if (repo.isSubfolder() || seafApplet->rpcClient()->hasLocalRepo(repo.id))
159 checkSyncedRepo(repo);
160
161 // we have a conflicting case, don't use group version if we can
162 if (map.contains(repo.id) && repo.isGroupRepo())
163 continue;
164 map[repo.id] = repo;
165 }
166
167 QList<ServerRepo> list = map.values();
168 // sort all repos by timestamp
169 // use std::sort for qt containers will force additional copy.
170 // anyway, we can use qt's alternative qSort for it
171 std::stable_sort(list.begin(), list.end(), compareRepoByTimestamp);
172
173 n = qMin(list.size(), kMaxRecentUpdatedRepos);
174 for (i = 0; i < n; i++) {
175 RepoItem *item = new RepoItem(list[i]);
176 recent_updated_category_->appendRow(item);
177 }
178 updateLocalReposPerm(list);
179 }
180
updateLocalReposPerm(const QList<ServerRepo> & repos)181 void RepoTreeModel::updateLocalReposPerm(const QList<ServerRepo> &repos)
182 {
183 int n = repos.size();
184 for (int i = 0; i < n; i++) {
185 ServerRepo repo = repos[i];
186 if (seafApplet->rpcClient()->hasLocalRepo(repo.id)) {
187 QString readonly_prop;
188 if (seafApplet->rpcClient()->getRepoProperty(
189 repo.id, kReadonlyPropertyName, &readonly_prop) < 0) {
190 continue;
191 }
192 bool readonly = readonly_prop == "true";
193 if (repo.readonly != readonly) {
194 seafApplet->rpcClient()->setRepoProperty(
195 repo.id,
196 kReadonlyPropertyName,
197 repo.readonly ? "true" : "false");
198 qWarning("repo %s %s permission changed to %s",
199 toCStr(repo.id),
200 toCStr(repo.name.left(40)),
201 toCStr(repo.permission));
202 }
203 }
204 }
205 }
206
207 struct DeleteRepoData {
208 QHash<QString, const ServerRepo*> map;
209 QList<RepoItem*> itemsToDelete;
210 };
211
collectDeletedRepos(RepoItem * item,void * vdata)212 void RepoTreeModel::collectDeletedRepos(RepoItem *item, void *vdata)
213 {
214 DeleteRepoData *data = (DeleteRepoData *)vdata;
215 const ServerRepo* repo = data->map.value(item->repo().id);
216 if (!repo || repo->type != item->repo().type) {
217 data->itemsToDelete << item;
218 }
219 }
220
removeReposDeletedOnServer(const std::vector<ServerRepo> & repos)221 void RepoTreeModel::removeReposDeletedOnServer(const std::vector<ServerRepo>& repos)
222 {
223 int i, n;
224 DeleteRepoData data;
225 n = repos.size();
226 for (i = 0; i < n; i++) {
227 const ServerRepo& repo = repos[i];
228 data.map.insert(repo.id, &repo);
229 }
230
231 forEachRepoItem(&RepoTreeModel::collectDeletedRepos, (void *)&data);
232
233 QListIterator<RepoItem*> iter(data.itemsToDelete);
234 while(iter.hasNext()) {
235 RepoItem *item = iter.next();
236
237 const ServerRepo& repo = item->repo();
238
239 qDebug("remove repo %s(%s) from \"%s\"\n",
240 toCStr(repo.name), toCStr(repo.id),
241 toCStr(((RepoCategoryItem*)item->parent())->name()));
242
243 item->parent()->removeRow(item->row());
244 }
245 }
246
247
checkPersonalRepo(const ServerRepo & repo)248 void RepoTreeModel::checkPersonalRepo(const ServerRepo& repo)
249 {
250 int row, n = my_repos_category_->rowCount();
251 for (row = 0; row < n; row++) {
252 RepoItem *item = (RepoItem *)(my_repos_category_->child(row));
253 if (item->repo().id == repo.id) {
254 updateRepoItem(item, repo);
255 return;
256 }
257 }
258
259 // The repo is new
260 RepoItem *item = new RepoItem(repo);
261 my_repos_category_->appendRow(item);
262 }
263
checkVirtualRepo(const ServerRepo & repo)264 void RepoTreeModel::checkVirtualRepo(const ServerRepo& repo)
265 {
266 if (item(kIndexOfVirtualReposCategory) != virtual_repos_category_) {
267 insertRow(kIndexOfVirtualReposCategory, virtual_repos_category_);
268 }
269
270 int row, n = virtual_repos_category_->rowCount();
271 for (row = 0; row < n; row++) {
272 RepoItem *item = (RepoItem *)(virtual_repos_category_->child(row));
273 if (item->repo().id == repo.id) {
274 updateRepoItem(item, repo);
275 return;
276 }
277 }
278
279 // The repo is new
280 RepoItem *item = new RepoItem(repo);
281 virtual_repos_category_->appendRow(item);
282 }
283
checkSharedRepo(const ServerRepo & repo)284 void RepoTreeModel::checkSharedRepo(const ServerRepo& repo)
285 {
286 int row, n = shared_repos_category_->rowCount();
287 for (row = 0; row < n; row++) {
288 RepoItem *item = (RepoItem *)(shared_repos_category_->child(row));
289 if (item->repo().id == repo.id) {
290 updateRepoItem(item, repo);
291 return;
292 }
293 }
294
295 // the repo is a new one
296 RepoItem *item = new RepoItem(repo);
297 shared_repos_category_->appendRow(item);
298 }
299
checkOrgRepo(const ServerRepo & repo)300 void RepoTreeModel::checkOrgRepo(const ServerRepo& repo)
301 {
302 int row, n = org_repos_category_->rowCount();
303 if (invisibleRootItem()->child(3) != org_repos_category_) {
304 // Insert pub repos after "recent updated", "my libraries", "shared libraries"
305 insertRow(3, org_repos_category_);
306 }
307 for (row = 0; row < n; row++) {
308 RepoItem *item = (RepoItem *)(org_repos_category_->child(row));
309 if (item->repo().id == repo.id) {
310 updateRepoItem(item, repo);
311 return;
312 }
313 }
314
315 // the repo is a new one
316 RepoItem *item = new RepoItem(repo);
317 org_repos_category_->appendRow(item);
318 }
319
checkGroupRepo(const ServerRepo & repo)320 void RepoTreeModel::checkGroupRepo(const ServerRepo &repo)
321 {
322 RepoCategoryItem *group = NULL;
323 int row, n = groups_root_category_->rowCount();
324
325 for (row = 0; row < n; row++) {
326 RepoCategoryItem *item =
327 (RepoCategoryItem *)(groups_root_category_->child(row));
328 if (item->groupId() == repo.group_id) {
329 group = item;
330 break;
331 }
332 }
333
334 if (!group) {
335 group = new RepoCategoryItem(
336 CAT_INDEX_GROUP_REPOS, repo.group_name, repo.group_id);
337 group->setLevel(1);
338 groups_root_category_->appendRow(group);
339 }
340
341 // Find the repo in this group
342 n = group->rowCount();
343 for (row = 0; row < n; row++) {
344 RepoItem *item = (RepoItem *)(group->child(row));
345 if (item->repo().id == repo.id) {
346 item->setLevel(2);
347 updateRepoItem(item, repo);
348 return;
349 }
350 }
351
352 // Current repo not in this group yet
353 RepoItem *item = new RepoItem(repo);
354 item->setLevel(2);
355 group->appendRow(item);
356 }
357
checkSyncedRepo(const ServerRepo & repo)358 void RepoTreeModel::checkSyncedRepo(const ServerRepo& repo)
359 {
360 int row, n = synced_repos_category_->rowCount();
361 for (row = 0; row < n; row++) {
362 RepoItem *item = (RepoItem *)(synced_repos_category_->child(row));
363 if (item->repo().id == repo.id) {
364 updateRepoItem(item, repo);
365 return;
366 }
367 }
368
369 // The repo is new
370 RepoItem *item = new RepoItem(repo);
371 synced_repos_category_->appendRow(item);
372 }
373
updateRepoItem(RepoItem * item,const ServerRepo & repo)374 void RepoTreeModel::updateRepoItem(RepoItem *item, const ServerRepo& repo)
375 {
376 item->setRepo(repo);
377 }
378
forEachRepoItem(void (RepoTreeModel::* func)(RepoItem *,void *),void * data,QStandardItem * item)379 void RepoTreeModel::forEachRepoItem(void (RepoTreeModel::*func)(RepoItem *, void *),
380 void *data,
381 QStandardItem *item)
382 {
383 if (item == nullptr) {
384 item = invisibleRootItem();
385 }
386 if (item->type() == REPO_ITEM_TYPE) {
387 (this->*func)((RepoItem *)item, data);
388 }
389
390 int row, n = item->rowCount();
391 for (row = 0; row < n; row++) {
392 forEachRepoItem(func, data, item->child(row));
393 }
394 }
395
forEachCategoryItem(void (* func)(RepoCategoryItem *,void *),void * data,QStandardItem * item)396 void RepoTreeModel::forEachCategoryItem(void (*func)(RepoCategoryItem *, void *),
397 void *data,
398 QStandardItem *item)
399 {
400 if (item == nullptr) {
401 item = invisibleRootItem();
402 }
403 if (item->type() == REPO_CATEGORY_TYPE) {
404 func((RepoCategoryItem *)item, data);
405 }
406
407 int row, n = item->rowCount();
408 for (row = 0; row < n; row++) {
409 forEachCategoryItem(func, data, item->child(row));
410 }
411 }
412
refreshLocalRepos()413 void RepoTreeModel::refreshLocalRepos()
414 {
415 if (!seafApplet->mainWindow()->isVisible()) {
416 return;
417 }
418
419 std::vector<CloneTask> tasks;
420 seafApplet->rpcClient()->getCloneTasks(&tasks);
421
422 forEachRepoItem(&RepoTreeModel::refreshRepoItem, (void*) &tasks);
423 }
424
refreshRepoItem(RepoItem * item,void * data)425 void RepoTreeModel::refreshRepoItem(RepoItem *item, void *data)
426 {
427 if (!tree_view_->isExpanded(proxiedIndexFromItem(item->parent()))) {
428 return;
429 }
430
431 if (item->syncNowClicked()) {
432 // Skip refresh repo item on which the user has clicked "sync now"
433 item->setSyncNowClicked(false);
434 return;
435 }
436
437 LocalRepo local_repo;
438 seafApplet->rpcClient()->getLocalRepo(item->repo().id, &local_repo);
439 if (local_repo != item->localRepo()) {
440 // if (local_repo.isValid()) {
441 // printf("local repo of %s changed\n", local_repo.name.toUtf8().data());
442 // }
443 item->setLocalRepo(local_repo);
444 QModelIndex index = indexFromItem(item);
445 emit dataChanged(index,index);
446 emit repoStatusChanged(index);
447 // printf("repo %s is changed\n", toCStr(item->repo().name));
448 }
449
450 item->setCloneTask();
451
452 CloneTask clone_task;
453 std::vector<CloneTask>* tasks = (std::vector<CloneTask>*)data;
454 if (!local_repo.isValid()) {
455 for (size_t i=0; i < tasks->size(); ++i) {
456 clone_task = tasks->at(i);
457 if (clone_task.repo_id == item->repo().id) {
458 item->setCloneTask(clone_task);
459 QModelIndex index = indexFromItem(item);
460 emit dataChanged(index, index);
461 emit repoStatusChanged(index);
462 }
463 }
464 }
465 }
466
updateRepoItemAfterSyncNow(const QString & repo_id)467 void RepoTreeModel::updateRepoItemAfterSyncNow(const QString& repo_id)
468 {
469 QString id = repo_id;
470 forEachRepoItem(&RepoTreeModel::updateRepoItemAfterSyncNow, (void*) &id);
471 }
472
updateRepoItemAfterSyncNow(RepoItem * item,void * data)473 void RepoTreeModel::updateRepoItemAfterSyncNow(RepoItem *item, void *data)
474 {
475 QString repo_id = *(QString *)data;
476 LocalRepo r = item->localRepo();
477 if (r.isValid() && r.id == repo_id) {
478 // We manually set the sync state of the repo to "SYNC_STATE_ING" to give
479 // the user immediate feedback
480
481 r.setSyncInfo("initializing");
482 r.sync_state = LocalRepo::SYNC_STATE_ING;
483 r.sync_state_str = tr("sync initializing");
484 item->setLocalRepo(r);
485 item->setSyncNowClicked(true);
486 }
487 }
488
onFilterTextChanged(const QString & text)489 void RepoTreeModel::onFilterTextChanged(const QString& text)
490 {
491 // Recalculate the matched repos count for each category
492 QStandardItem *root = invisibleRootItem();
493 int row, n;
494 n = root->rowCount();
495 QRegExp re = makeFilterRegExp(text);
496 for (row = 0; row < n; row++) {
497 RepoCategoryItem *category = (RepoCategoryItem *)root->child(row);
498 if (category->isGroupsRoot()) {
499 continue;
500 }
501 int j, total, matched = 0;
502 total = category->rowCount();
503 for (j = 0; j < total; j++) {
504 RepoItem *item = (RepoItem *)category->child(j);
505 if (item->repo().name.contains(re)) {
506 matched++;
507 }
508 }
509 category->setMatchedReposCount(matched);
510 }
511 }
512
RepoFilterProxyModel(QObject * parent)513 RepoFilterProxyModel::RepoFilterProxyModel(QObject *parent)
514 : QSortFilterProxyModel(parent),
515 has_filter_(false)
516 {
517 }
518
setSourceModel(QAbstractItemModel * source_model)519 void RepoFilterProxyModel::setSourceModel(QAbstractItemModel *source_model)
520 {
521 QSortFilterProxyModel::setSourceModel(source_model);
522 RepoTreeModel *tree_model = (RepoTreeModel *)source_model;
523 connect(tree_model, SIGNAL(repoStatusChanged(const QModelIndex&)),
524 this, SLOT(onRepoStatusChanged(const QModelIndex&)));
525 }
526
onRepoStatusChanged(const QModelIndex & source_index)527 void RepoFilterProxyModel::onRepoStatusChanged(const QModelIndex& source_index)
528 {
529 QModelIndex index = mapFromSource(source_index);
530 emit dataChanged(index, index);
531 }
532
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const533 bool RepoFilterProxyModel::filterAcceptsRow(int source_row,
534 const QModelIndex & source_parent) const
535 {
536 RepoTreeModel *tree_model = (RepoTreeModel *)(sourceModel());
537 QModelIndex index = tree_model->index(source_row, 0, source_parent);
538 QStandardItem *item = tree_model->itemFromIndex(index);
539 if (item->type() == REPO_CATEGORY_TYPE) {
540 // RepoCategoryItem *category = (RepoCategoryItem *)item;
541 // We don't filter repo categories, only filter repos by name.
542 return true;
543 } else if (item->type() == REPO_ITEM_TYPE) {
544 // Use default filtering (filter by item DisplayRole, i.e. repo name)
545 bool match = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
546 // if (match) {
547 // RepoCategoryItem *category = (RepoCategoryItem *)(item->parent());
548 // }
549 return match;
550 }
551
552 return false;
553 }
554
setFilterText(const QString & text)555 void RepoFilterProxyModel::setFilterText(const QString& text)
556 {
557 has_filter_ = !text.isEmpty();
558 invalidate();
559 setFilterRegExp(makeFilterRegExp(text));
560 }
561
562 // void RepoFilterProxyModel::sort()
563 // {
564 // }
565
lessThan(const QModelIndex & left,const QModelIndex & right) const566 bool RepoFilterProxyModel::lessThan(const QModelIndex &left,
567 const QModelIndex &right) const
568 {
569 RepoTreeModel *tree_model = (RepoTreeModel *)(sourceModel());
570 QStandardItem *item_l = tree_model->itemFromIndex(left);
571 QStandardItem *item_r = tree_model->itemFromIndex(right);
572
573 /**
574 * When we have filter: sort category by matched repos count
575 * When we have no filter: sort category by category index order
576 *
577 */
578 if (item_l->type() == REPO_CATEGORY_TYPE) {
579 // repo categories
580 RepoCategoryItem *cl = (RepoCategoryItem *)item_l;
581 RepoCategoryItem *cr = (RepoCategoryItem *)item_r;
582 if (has_filter_) {
583 // printf ("%s matched: %d, %s matched: %d\n",
584 // cl->name().toUtf8().data(), cl->matchedReposCount(),
585 // cr->name().toUtf8().data(), cr->matchedReposCount());
586 return cl->matchedReposCount() > cr->matchedReposCount();
587 } else {
588 int cat_l = cl->categoryIndex();
589 int cat_r = cr->categoryIndex();
590 if (cat_l == cat_r) {
591 return cl->name() < cr->name();
592 } else {
593 return cat_l < cat_r;
594 }
595 }
596 } else {
597 // repos
598 RepoItem *cl = (RepoItem *)item_l;
599 RepoItem *cr = (RepoItem *)item_r;
600 if (cl->repo().mtime != cr->repo().mtime) {
601 return cl->repo().mtime > cr->repo().mtime;
602 } else {
603 return cl->repo().name > cr->repo().name;
604 }
605 }
606
607 return false;
608 }
609
flags(const QModelIndex & index) const610 Qt::ItemFlags RepoFilterProxyModel::flags(const QModelIndex& index) const
611 {
612 Qt::ItemFlags flgs = QSortFilterProxyModel::flags(index);
613 return flgs & ~Qt::ItemIsEditable;
614 }
615