comparison src/pages/anime_list.cpp @ 7:07a9095eaeed

Update Refactored some code, moved some around
author Paper <mrpapersonic@gmail.com>
date Thu, 24 Aug 2023 23:11:38 -0400
parents src/anime.cpp@1d82f6e04d7d
children
comparison
equal deleted inserted replaced
6:1d82f6e04d7d 7:07a9095eaeed
1 /**
2 * anime_list.cpp: defines the anime list page
3 * and widgets.
4 *
5 * much of this file is based around
6 * Qt's original QTabWidget implementation, because
7 * I needed a somewhat native way to create a tabbed
8 * widget with only one subwidget that worked exactly
9 * like a native tabbed widget.
10 **/
11 #include <cmath>
12 #include <QStyledItemDelegate>
13 #include <QProgressBar>
14 #include <QShortcut>
15 #include <QHBoxLayout>
16 #include <QStylePainter>
17 #include <QMenu>
18 #include <QHeaderView>
19 #include "anilist.h"
20 #include "anime.h"
21 #include "anime_list.h"
22 #include "information.h"
23 #include "session.h"
24 #include "time_utils.h"
25
26 AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent)
27 : QStyledItemDelegate (parent) {
28 }
29
30 QWidget* AnimeListWidgetDelegate::createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const {
31 // no edit 4 u
32 return nullptr;
33 }
34
35 void AnimeListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
36 {
37 switch (index.column()) {
38 case AnimeListWidgetModel::AL_PROGRESS: {
39 const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
40 const int episodes = static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
41
42 QStyleOptionViewItem customOption (option);
43 customOption.state.setFlag(QStyle::State_Enabled, true);
44
45 progress_bar.paint(painter, customOption, index.data().toString(), progress, episodes);
46 break;
47 }
48 default:
49 QStyledItemDelegate::paint(painter, option, index);
50 break;
51 }
52 }
53
54 AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject *parent)
55 : QSortFilterProxyModel(parent) {
56 }
57
58 bool AnimeListWidgetSortFilter::lessThan(const QModelIndex &l,
59 const QModelIndex &r) const {
60 QVariant left = sourceModel()->data(l, sortRole());
61 QVariant right = sourceModel()->data(r, sortRole());
62
63 switch (left.userType()) {
64 case QMetaType::Int:
65 case QMetaType::UInt:
66 case QMetaType::LongLong:
67 case QMetaType::ULongLong:
68 return left.toInt() < right.toInt();
69 case QMetaType::QDate:
70 return left.toDate() < right.toDate();
71 case QMetaType::QString:
72 default:
73 return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
74 }
75 }
76
77 AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
78 : QAbstractListModel(parent)
79 , list(*alist) {
80 return;
81 }
82
83 int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
84 return list.Size();
85 (void)(parent);
86 }
87
88 int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
89 return NB_COLUMNS;
90 (void)(parent);
91 }
92
93 QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
94 if (role == Qt::DisplayRole) {
95 switch (section) {
96 case AL_TITLE:
97 return tr("Anime title");
98 case AL_PROGRESS:
99 return tr("Progress");
100 case AL_EPISODES:
101 return tr("Episodes");
102 case AL_TYPE:
103 return tr("Type");
104 case AL_SCORE:
105 return tr("Score");
106 case AL_SEASON:
107 return tr("Season");
108 case AL_STARTED:
109 return tr("Date started");
110 case AL_COMPLETED:
111 return tr("Date completed");
112 case AL_NOTES:
113 return tr("Notes");
114 case AL_AVG_SCORE:
115 return tr("Average score");
116 case AL_UPDATED:
117 return tr("Last updated");
118 default:
119 return {};
120 }
121 } else if (role == Qt::TextAlignmentRole) {
122 switch (section) {
123 case AL_TITLE:
124 case AL_NOTES:
125 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
126 case AL_PROGRESS:
127 case AL_EPISODES:
128 case AL_TYPE:
129 case AL_SCORE:
130 case AL_AVG_SCORE:
131 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
132 case AL_SEASON:
133 case AL_STARTED:
134 case AL_COMPLETED:
135 case AL_UPDATED:
136 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
137 default:
138 return QAbstractListModel::headerData(section, orientation, role);
139 }
140 }
141 return QAbstractListModel::headerData(section, orientation, role);
142 }
143
144 Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
145 return (index.isValid()) ? &(list[index.row()]) : nullptr;
146 }
147
148 QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
149 if (!index.isValid())
150 return QVariant();
151 switch (role) {
152 case Qt::DisplayRole:
153 switch (index.column()) {
154 case AL_TITLE:
155 return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
156 case AL_PROGRESS:
157 return QString::number(list[index.row()].progress) + "/" + QString::number(list[index.row()].episodes);
158 case AL_EPISODES:
159 return list[index.row()].episodes;
160 case AL_SCORE:
161 return list[index.row()].score;
162 case AL_TYPE:
163 return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
164 case AL_SEASON:
165 return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number(list[index.row()].air_date.GetYear());
166 case AL_AVG_SCORE:
167 return QString::number(list[index.row()].audience_score) + "%";
168 case AL_STARTED:
169 return list[index.row()].started.GetAsQDate();
170 case AL_COMPLETED:
171 return list[index.row()].completed.GetAsQDate();
172 case AL_UPDATED: {
173 if (list[index.row()].updated == 0)
174 return QString("-");
175 Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
176 return QString::fromUtf8(duration.AsRelativeString().c_str());
177 }
178 case AL_NOTES:
179 return QString::fromUtf8(list[index.row()].notes.c_str());
180 default:
181 return "";
182 }
183 break;
184 case Qt::UserRole:
185 switch (index.column()) {
186 case AL_PROGRESS:
187 return list[index.row()].progress;
188 case AL_TYPE:
189 return list[index.row()].type;
190 case AL_SEASON:
191 return list[index.row()].air_date.GetAsQDate();
192 case AL_AVG_SCORE:
193 return list[index.row()].audience_score;
194 case AL_UPDATED:
195 return list[index.row()].updated;
196 default:
197 return data(index, Qt::DisplayRole);
198 }
199 break;
200 case Qt::TextAlignmentRole:
201 switch (index.column()) {
202 case AL_TITLE:
203 case AL_NOTES:
204 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
205 case AL_PROGRESS:
206 case AL_EPISODES:
207 case AL_TYPE:
208 case AL_SCORE:
209 case AL_AVG_SCORE:
210 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
211 case AL_SEASON:
212 case AL_STARTED:
213 case AL_COMPLETED:
214 case AL_UPDATED:
215 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
216 default:
217 break;
218 }
219 break;
220 }
221 return QVariant();
222 }
223
224 void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
225 int i = list.GetAnimeIndex(anime);
226 emit dataChanged(index(i), index(i));
227 }
228
229 void AnimeListWidgetModel::Update(AnimeList const& new_list) {
230 list = AnimeList(new_list);
231 emit dataChanged(index(0), index(rowCount()));
232 }
233
234 int AnimeListWidget::VisibleColumnsCount() const {
235 int count = 0;
236
237 for (int i = 0, end = tree_view->header()->count(); i < end; i++)
238 {
239 if (!tree_view->isColumnHidden(i))
240 count++;
241 }
242
243 return count;
244 }
245
246 void AnimeListWidget::SetColumnDefaults() {
247 tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
248 tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
249 tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
250 tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
251 tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
252 tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
253 tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
254 tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
255 tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
256 tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
257 tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
258 tree_view->setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
259 }
260
261 void AnimeListWidget::DisplayColumnHeaderMenu() {
262 QMenu *menu = new QMenu(this);
263 menu->setAttribute(Qt::WA_DeleteOnClose);
264 menu->setTitle(tr("Column visibility"));
265 menu->setToolTipsVisible(true);
266
267 for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
268 if (i == AnimeListWidgetModel::AL_TITLE)
269 continue;
270 const auto column_name = sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
271 QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
272 if (!checked && (VisibleColumnsCount() <= 1))
273 return;
274
275 tree_view->setColumnHidden(i, !checked);
276
277 if (checked && (tree_view->columnWidth(i) <= 5))
278 tree_view->resizeColumnToContents(i);
279
280 // SaveSettings();
281 });
282 action->setCheckable(true);
283 action->setChecked(!tree_view->isColumnHidden(i));
284 }
285
286 menu->addSeparator();
287 QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
288 for (int i = 0, count = tree_view->header()->count(); i < count; ++i)
289 {
290 SetColumnDefaults();
291 }
292 // SaveSettings();
293 });
294 menu->popup(QCursor::pos());
295 (void)(resetAction);
296 }
297
298 void AnimeListWidget::DisplayListMenu() {
299 QMenu *menu = new QMenu(this);
300 menu->setAttribute(Qt::WA_DeleteOnClose);
301 menu->setTitle(tr("Column visibility"));
302 menu->setToolTipsVisible(true);
303
304 const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
305 if (!selection.indexes().first().isValid()) {
306 return;
307 }
308
309 QAction* action = menu->addAction("Information", [this, selection]{
310 const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
311 Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
312 if (!anime) {
313 return;
314 }
315
316 InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);
317
318 dialog->show();
319 dialog->raise();
320 dialog->activateWindow();
321 });
322 menu->popup(QCursor::pos());
323 }
324
325 void AnimeListWidget::ItemDoubleClicked() {
326 /* throw out any other garbage */
327 const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
328 if (!selection.indexes().first().isValid()) {
329 return;
330 }
331
332 const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
333 Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
334 if (!anime) {
335 return;
336 }
337
338 InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);
339
340 dialog->show();
341 dialog->raise();
342 dialog->activateWindow();
343 }
344
345 void AnimeListWidget::paintEvent(QPaintEvent*) {
346 QStylePainter p(this);
347
348 QStyleOptionTabWidgetFrame opt;
349 InitStyle(&opt);
350 opt.rect = panelRect;
351 p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
352 }
353
354 void AnimeListWidget::resizeEvent(QResizeEvent* e) {
355 QWidget::resizeEvent(e);
356 SetupLayout();
357 }
358
359 void AnimeListWidget::showEvent(QShowEvent*) {
360 SetupLayout();
361 }
362
363 void AnimeListWidget::InitBasicStyle(QStyleOptionTabWidgetFrame *option) const
364 {
365 if (!option)
366 return;
367
368 option->initFrom(this);
369 option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
370 option->shape = QTabBar::RoundedNorth;
371 option->tabBarRect = tab_bar->geometry();
372 }
373
374 void AnimeListWidget::InitStyle(QStyleOptionTabWidgetFrame *option) const
375 {
376 if (!option)
377 return;
378
379 InitBasicStyle(option);
380
381 //int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
382 QSize t(0, tree_view->frameWidth());
383 if (tab_bar->isVisibleTo(this)) {
384 t = tab_bar->sizeHint();
385 t.setWidth(width());
386 }
387 option->tabBarSize = t;
388
389 QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
390 selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
391 option->selectedTabRect = selected_tab_rect;
392
393 option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
394 }
395
396 void AnimeListWidget::SetupLayout() {
397 QStyleOptionTabWidgetFrame option;
398 InitStyle(&option);
399
400 QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
401 tabRect.setLeft(tabRect.left()+1);
402 panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
403 QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);
404
405 tab_bar->setGeometry(tabRect);
406 tree_view->parentWidget()->setGeometry(contentsRect);
407 }
408
409 AnimeListWidget::AnimeListWidget(QWidget* parent)
410 : QWidget(parent) {
411 /* Tab bar */
412 tab_bar = new QTabBar(this);
413 tab_bar->setExpanding(false);
414 tab_bar->setDrawBase(false);
415
416 /* Tree view... */
417 QWidget* tree_widget = new QWidget(this);
418 tree_view = new QTreeView(tree_widget);
419 tree_view->setItemDelegate(new AnimeListWidgetDelegate(tree_view));
420 tree_view->setUniformRowHeights(true);
421 tree_view->setAllColumnsShowFocus(false);
422 tree_view->setSortingEnabled(true);
423 tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
424 tree_view->setItemsExpandable(false);
425 tree_view->setRootIsDecorated(false);
426 tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
427 tree_view->setFrameShape(QFrame::NoFrame);
428 QHBoxLayout* layout = new QHBoxLayout;
429 layout->addWidget(tree_view);
430 layout->setMargin(0);
431 tree_widget->setLayout(layout);
432 connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
433 connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
434
435 /* Enter & return keys */
436 connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
437 &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
438
439 connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
440 &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
441
442 tree_view->header()->setStretchLastSection(false);
443 tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
444 connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayColumnHeaderMenu);
445
446 connect(tab_bar, &QTabBar::currentChanged, this, [this](int index){
447 if (index < sort_models.size())
448 tree_view->setModel(sort_models[index]);
449 });
450
451 setFocusPolicy(Qt::TabFocus);
452 setFocusProxy(tab_bar);
453 }
454
455 void AnimeListWidget::SyncAnimeList() {
456 switch (session.config.service) {
457 case ANILIST: {
458 session.config.anilist.user_id = AniList::GetUserId(session.config.anilist.username);
459 FreeAnimeList();
460 AniList::UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
461 break;
462 }
463 default:
464 break;
465 }
466 for (unsigned int i = 0; i < anime_lists.size(); i++) {
467 tab_bar->addTab(QString::fromStdString(anime_lists[i].name));
468 AnimeListWidgetSortFilter* sort_model = new AnimeListWidgetSortFilter(tree_view);
469 sort_model->setSourceModel(new AnimeListWidgetModel(this, &anime_lists[i]));
470 sort_model->setSortRole(Qt::UserRole);
471 sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
472 sort_models.push_back(sort_model);
473 }
474 if (anime_lists.size() > 0)
475 tree_view->setModel(sort_models.at(0));
476 SetColumnDefaults();
477 SetupLayout();
478 }
479
480 void AnimeListWidget::FreeAnimeList() {
481 while (tab_bar->count())
482 tab_bar->removeTab(0);
483 while (sort_models.size()) {
484 delete sort_models[sort_models.size()-1];
485 sort_models.pop_back();
486 }
487 for (auto& list : anime_lists) {
488 list.Clear();
489 }
490 anime_lists.clear();
491 }
492
493 int AnimeListWidget::GetTotalAnimeAmount() {
494 int total = 0;
495 for (auto& list : anime_lists) {
496 total += list.Size();
497 }
498 return total;
499 }
500
501 int AnimeListWidget::GetTotalEpisodeAmount() {
502 /* FIXME: this also needs to take into account rewatches... */
503 int total = 0;
504 for (auto& list : anime_lists) {
505 for (auto& anime : list) {
506 total += anime.progress;
507 }
508 }
509 return total;
510 }
511
512 /* Returns the total watched amount in minutes. */
513 int AnimeListWidget::GetTotalWatchedAmount() {
514 int total = 0;
515 for (auto& list : anime_lists) {
516 for (auto& anime : list) {
517 total += anime.duration*anime.progress;
518 }
519 }
520 return total;
521 }
522
523 /* Returns the total planned amount in minutes.
524 Note that we should probably limit progress to the
525 amount of episodes, as AniList will let you
526 set episode counts up to 32768. But that should
527 rather be handled elsewhere. */
528 int AnimeListWidget::GetTotalPlannedAmount() {
529 int total = 0;
530 for (auto& list : anime_lists) {
531 for (auto& anime : list) {
532 total += anime.duration*(anime.episodes-anime.progress);
533 }
534 }
535 return total;
536 }
537
538 double AnimeListWidget::GetAverageScore() {
539 double avg = 0;
540 int amt = 0;
541 for (auto& list : anime_lists) {
542 for (auto& anime : list) {
543 avg += anime.score;
544 if (anime.score != 0)
545 amt++;
546 }
547 }
548 return avg/amt;
549 }
550
551 double AnimeListWidget::GetScoreDeviation() {
552 double squares_sum = 0, avg = GetAverageScore();
553 int amt = 0;
554 for (auto& list : anime_lists) {
555 for (auto& anime : list) {
556 if (anime.score != 0) {
557 squares_sum += std::pow((double)anime.score - avg, 2);
558 amt++;
559 }
560 }
561 }
562 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
563 }
564
565 #include "moc_anime_list.cpp"