comparison src/anime.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 1d82f6e04d7d
children
comparison
equal deleted inserted replaced
6:1d82f6e04d7d 7:07a9095eaeed
1 /*
2 * anime.cpp: defining of custom anime-related
3 * datatypes & variables
4 */
1 #include <chrono> 5 #include <chrono>
2 #include <string> 6 #include <string>
3 #include <vector> 7 #include <vector>
4 #include <cmath> 8 #include <cmath>
5 #include "window.h" 9 #include <algorithm>
6 #include "anilist.h" 10 #include "anilist.h"
7 #include "config.h"
8 #include "anime.h" 11 #include "anime.h"
9 #include "date.h" 12 #include "date.h"
10 #include "time_utils.h" 13 #include "session.h"
11 #include "information.h"
12 #include "ui_utils.h"
13 #include <iostream>
14 14
15 std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = { 15 std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
16 {CURRENT, "Watching"}, 16 {CURRENT, "Watching"},
17 {PLANNING, "Planning"}, 17 {PLANNING, "Planning"},
18 {COMPLETED, "Completed"}, 18 {COMPLETED, "Completed"},
74 synopsis = a.synopsis; 74 synopsis = a.synopsis;
75 duration = a.duration; 75 duration = a.duration;
76 } 76 }
77 77
78 std::string Anime::GetUserPreferredTitle() { 78 std::string Anime::GetUserPreferredTitle() {
79 if (title.english.empty()) 79 switch (session.config.anime_list.language) {
80 return title.romaji; 80 case NATIVE:
81 return title.english; 81 return (title.native.empty()) ? title.romaji : title.native;
82 case ENGLISH:
83 return (title.english.empty()) ? title.romaji : title.english;
84 default:
85 return title.romaji;
86 }
87 }
88
89 std::vector<std::string> Anime::GetTitleSynonyms() {
90 std::vector<std::string> result;
91 #define IN_VECTOR(v, k) \
92 (std::count(v.begin(), v.end(), k))
93 #define ADD_TO_SYNONYMS(v, k) \
94 if (!k.empty() && !IN_VECTOR(v, k) && k != GetUserPreferredTitle()) v.push_back(k)
95 ADD_TO_SYNONYMS(result, title.english);
96 ADD_TO_SYNONYMS(result, title.romaji);
97 ADD_TO_SYNONYMS(result, title.native);
98 for (auto& synonym : synonyms) {
99 ADD_TO_SYNONYMS(result, synonym);
100 }
101 #undef ADD_TO_SYNONYMS
102 #undef IN_VECTOR
103 return result;
82 } 104 }
83 105
84 void AnimeList::Add(Anime& anime) { 106 void AnimeList::Add(Anime& anime) {
85 if (anime_id_to_anime.contains(anime.id)) 107 if (anime_id_to_anime.contains(anime.id))
86 return; 108 return;
129 anime_list.push_back(Anime(l[i])); 151 anime_list.push_back(Anime(l[i]));
130 } 152 }
131 name = l.name; 153 name = l.name;
132 } 154 }
133 155
156 AnimeList& AnimeList::operator=(const AnimeList& l) {
157 if (this != &l) {
158 for (unsigned long long i = 0; i < l.Size(); i++) {
159 this->anime_list.push_back(Anime(l[i]));
160 }
161 this->name = l.name;
162 }
163 return *this;
164 }
165
134 AnimeList::~AnimeList() { 166 AnimeList::~AnimeList() {
135 anime_list.clear(); 167 anime_list.clear();
136 anime_list.shrink_to_fit(); 168 anime_list.shrink_to_fit();
137 } 169 }
138 170
158 } 190 }
159 191
160 const Anime& AnimeList::operator[](std::size_t index) const { 192 const Anime& AnimeList::operator[](std::size_t index) const {
161 return anime_list.at(index); 193 return anime_list.at(index);
162 } 194 }
163
164 /* ------------------------------------------------------------------------- */
165
166 AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent)
167 : QStyledItemDelegate (parent) {
168 }
169
170 QWidget *AnimeListWidgetDelegate::createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const
171 {
172 // LOL
173 return nullptr;
174 }
175
176 void AnimeListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
177 {
178 switch (index.column()) {
179 case AnimeListWidgetModel::AL_PROGRESS: {
180 const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
181 const int episodes = static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
182
183 QStyleOptionViewItem customOption (option);
184 customOption.state.setFlag(QStyle::State_Enabled, true);
185
186 progress_bar.paint(painter, customOption, index.data().toString(), progress, episodes);
187 break;
188 }
189 default:
190 QStyledItemDelegate::paint(painter, option, index);
191 break;
192 }
193 }
194
195 AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject *parent)
196 : QSortFilterProxyModel(parent) {
197 }
198
199 bool AnimeListWidgetSortFilter::lessThan(const QModelIndex &l,
200 const QModelIndex &r) const {
201 QVariant left = sourceModel()->data(l, sortRole());
202 QVariant right = sourceModel()->data(r, sortRole());
203
204 switch (left.userType()) {
205 case QMetaType::Int:
206 case QMetaType::UInt:
207 case QMetaType::LongLong:
208 case QMetaType::ULongLong:
209 return left.toInt() < right.toInt();
210 case QMetaType::QDate:
211 return left.toDate() < right.toDate();
212 case QMetaType::QString:
213 default:
214 return left.toString() < right.toString();
215 }
216 }
217
218 /* Thank you qBittorrent for having a great example of a
219 widget model. */
220 AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
221 : QAbstractListModel(parent)
222 , list(*alist) {
223 return;
224 }
225
226 int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
227 return list.Size();
228 (void)(parent);
229 }
230
231 int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
232 return NB_COLUMNS;
233 (void)(parent);
234 }
235
236 QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
237 if (role == Qt::DisplayRole) {
238 switch (section) {
239 case AL_TITLE:
240 return tr("Anime title");
241 case AL_PROGRESS:
242 return tr("Progress");
243 case AL_EPISODES:
244 return tr("Episodes");
245 case AL_TYPE:
246 return tr("Type");
247 case AL_SCORE:
248 return tr("Score");
249 case AL_SEASON:
250 return tr("Season");
251 case AL_STARTED:
252 return tr("Date started");
253 case AL_COMPLETED:
254 return tr("Date completed");
255 case AL_NOTES:
256 return tr("Notes");
257 case AL_AVG_SCORE:
258 return tr("Average score");
259 case AL_UPDATED:
260 return tr("Last updated");
261 default:
262 return {};
263 }
264 } else if (role == Qt::TextAlignmentRole) {
265 switch (section) {
266 case AL_TITLE:
267 case AL_NOTES:
268 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
269 case AL_PROGRESS:
270 case AL_EPISODES:
271 case AL_TYPE:
272 case AL_SCORE:
273 case AL_AVG_SCORE:
274 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
275 case AL_SEASON:
276 case AL_STARTED:
277 case AL_COMPLETED:
278 case AL_UPDATED:
279 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
280 default:
281 return QAbstractListModel::headerData(section, orientation, role);
282 }
283 }
284 return QAbstractListModel::headerData(section, orientation, role);
285 }
286
287 Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
288 return (index.isValid()) ? &(list[index.row()]) : nullptr;
289 }
290
291 QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
292 if (!index.isValid())
293 return QVariant();
294 switch (role) {
295 case Qt::DisplayRole:
296 switch (index.column()) {
297 case AL_TITLE:
298 return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
299 case AL_PROGRESS:
300 return QString::number(list[index.row()].progress) + "/" + QString::number(list[index.row()].episodes);
301 case AL_EPISODES:
302 return list[index.row()].episodes;
303 case AL_SCORE:
304 return list[index.row()].score;
305 case AL_TYPE:
306 return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
307 case AL_SEASON:
308 return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number(list[index.row()].air_date.GetYear());
309 case AL_AVG_SCORE:
310 return QString::number(list[index.row()].audience_score) + "%";
311 case AL_STARTED:
312 return list[index.row()].started.GetAsQDate();
313 case AL_COMPLETED:
314 return list[index.row()].completed.GetAsQDate();
315 case AL_UPDATED: {
316 if (list[index.row()].updated == 0)
317 return QString("-");
318 Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
319 return QString::fromUtf8(duration.AsRelativeString().c_str());
320 }
321 case AL_NOTES:
322 return QString::fromUtf8(list[index.row()].notes.c_str());
323 default:
324 return "";
325 }
326 break;
327 case Qt::UserRole:
328 switch (index.column()) {
329 case AL_PROGRESS:
330 return list[index.row()].progress;
331 case AL_TYPE:
332 return list[index.row()].type;
333 case AL_SEASON:
334 return list[index.row()].air_date.GetAsQDate();
335 case AL_AVG_SCORE:
336 return list[index.row()].audience_score;
337 case AL_UPDATED:
338 return list[index.row()].updated;
339 default:
340 return data(index, Qt::DisplayRole);
341 }
342 break;
343 case Qt::TextAlignmentRole:
344 switch (index.column()) {
345 case AL_TITLE:
346 case AL_NOTES:
347 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
348 case AL_PROGRESS:
349 case AL_EPISODES:
350 case AL_TYPE:
351 case AL_SCORE:
352 case AL_AVG_SCORE:
353 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
354 case AL_SEASON:
355 case AL_STARTED:
356 case AL_COMPLETED:
357 case AL_UPDATED:
358 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
359 default:
360 break;
361 }
362 break;
363 }
364 return QVariant();
365 }
366
367 void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
368 int i = list.GetAnimeIndex(anime);
369 emit dataChanged(index(i), index(i));
370 }
371
372 int AnimeListWidget::VisibleColumnsCount() const {
373 int count = 0;
374
375 for (int i = 0, end = header()->count(); i < end; i++)
376 {
377 if (!isColumnHidden(i))
378 count++;
379 }
380
381 return count;
382 }
383
384 void AnimeListWidget::SetColumnDefaults() {
385 setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
386 setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
387 setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
388 setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
389 setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
390 setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
391 setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
392 setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
393 setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
394 setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
395 setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
396 setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
397 }
398
399 void AnimeListWidget::DisplayColumnHeaderMenu() {
400 QMenu *menu = new QMenu(this);
401 menu->setAttribute(Qt::WA_DeleteOnClose);
402 menu->setTitle(tr("Column visibility"));
403 menu->setToolTipsVisible(true);
404
405 for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
406 if (i == AnimeListWidgetModel::AL_TITLE)
407 continue;
408 const auto column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
409 QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
410 if (!checked && (VisibleColumnsCount() <= 1))
411 return;
412
413 setColumnHidden(i, !checked);
414
415 if (checked && (columnWidth(i) <= 5))
416 resizeColumnToContents(i);
417
418 // SaveSettings();
419 });
420 action->setCheckable(true);
421 action->setChecked(!isColumnHidden(i));
422 }
423
424 menu->addSeparator();
425 QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
426 for (int i = 0, count = header()->count(); i < count; ++i)
427 {
428 SetColumnDefaults();
429 }
430 // SaveSettings();
431 });
432 menu->popup(QCursor::pos());
433 (void)(resetAction);
434 }
435
436 void AnimeListWidget::DisplayListMenu() {
437 QMenu *menu = new QMenu(this);
438 menu->setAttribute(Qt::WA_DeleteOnClose);
439 menu->setTitle(tr("Column visibility"));
440 menu->setToolTipsVisible(true);
441
442 const QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
443 if (!selection.indexes().first().isValid()) {
444 return;
445 }
446
447 QAction* action = menu->addAction("Information", [this, selection]{
448 const QModelIndex index = model->index(selection.indexes().first().row());
449 Anime* anime = model->GetAnimeFromIndex(index);
450 if (!anime) {
451 return;
452 }
453
454 InformationDialog* dialog = new InformationDialog(*anime, model, this);
455
456 dialog->show();
457 dialog->raise();
458 dialog->activateWindow();
459 });
460 menu->popup(QCursor::pos());
461 }
462
463 void AnimeListWidget::ItemDoubleClicked() {
464 /* throw out any other garbage */
465 const QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
466 if (!selection.indexes().first().isValid()) {
467 return;
468 }
469
470 const QModelIndex index = model->index(selection.indexes().first().row());
471 const QString title = index.siblingAtColumn(AnimeListWidgetModel::AL_TITLE).data(Qt::UserRole).toString();
472 QMessageBox box;
473 box.setText(QString::number(title.size()));
474 box.exec();
475 Anime* anime = model->GetAnimeFromIndex(index);
476 if (!anime) {
477 return;
478 }
479
480 InformationDialog* dialog = new InformationDialog(*anime, model, this);
481
482 dialog->show();
483 dialog->raise();
484 dialog->activateWindow();
485 }
486
487 AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
488 : QTreeView(parent) {
489 setItemDelegate(new AnimeListWidgetDelegate(this));
490 model = new AnimeListWidgetModel(parent, alist);
491 sort_model = new AnimeListWidgetSortFilter(this);
492 sort_model->setSourceModel(model);
493 sort_model->setSortRole(Qt::UserRole);
494 setModel(sort_model);
495 setObjectName("listwidget");
496 setStyleSheet("QTreeView#listwidget{border:0px;}");
497 setUniformRowHeights(true);
498 setAllColumnsShowFocus(false);
499 setSortingEnabled(true);
500 setSelectionMode(QAbstractItemView::ExtendedSelection);
501 setItemsExpandable(false);
502 setRootIsDecorated(false);
503 setContextMenuPolicy(Qt::CustomContextMenu);
504 connect(this, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
505 connect(this, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
506
507 /* Enter & return keys */
508 connect(new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut),
509 &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
510
511 connect(new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut),
512 &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
513
514 header()->setStretchLastSection(false);
515 header()->setContextMenuPolicy(Qt::CustomContextMenu);
516 connect(header(), &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayColumnHeaderMenu);
517 // if(!session.config.anime_list.columns) {
518 SetColumnDefaults();
519 // }
520 }
521
522 AnimeListPage::AnimeListPage(QWidget* parent) : QTabWidget (parent) {
523 setDocumentMode(false);
524 setObjectName("animepage");
525 //setStyleSheet("QTabWidget#animepage{border-bottom:0px;border-left:0px;border-right:0px;}");
526 SyncAnimeList();
527 for (AnimeList& list : anime_lists) {
528 addTab(new AnimeListWidget(this, &list), QString::fromUtf8(list.name.c_str()));
529 }
530 }
531
532 void AnimeListPage::SyncAnimeList() {
533 switch (session.config.service) {
534 case ANILIST: {
535 AniList anilist = AniList();
536 session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username);
537 FreeAnimeList();
538 anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
539 break;
540 }
541 default:
542 break;
543 }
544 }
545
546 void AnimeListPage::FreeAnimeList() {
547 for (auto& list : anime_lists) {
548 list.Clear();
549 }
550 anime_lists.clear();
551 }
552
553 int AnimeListPage::GetTotalAnimeAmount() {
554 int total = 0;
555 for (auto& list : anime_lists) {
556 total += list.Size();
557 }
558 return total;
559 }
560
561 int AnimeListPage::GetTotalEpisodeAmount() {
562 /* FIXME: this also needs to take into account rewatches... */
563 int total = 0;
564 for (auto& list : anime_lists) {
565 for (auto& anime : list) {
566 total += anime.progress;
567 }
568 }
569 return total;
570 }
571
572 /* Returns the total watched amount in minutes. */
573 int AnimeListPage::GetTotalWatchedAmount() {
574 int total = 0;
575 for (auto& list : anime_lists) {
576 for (auto& anime : list) {
577 total += anime.duration*anime.progress;
578 }
579 }
580 return total;
581 }
582
583 /* Returns the total planned amount in minutes.
584 Note that we should probably limit progress to the
585 amount of episodes, as AniList will let you
586 set episode counts up to 32768. But that should
587 rather be handled elsewhere. */
588 int AnimeListPage::GetTotalPlannedAmount() {
589 int total = 0;
590 for (auto& list : anime_lists) {
591 for (auto& anime : list) {
592 total += anime.duration*(anime.episodes-anime.progress);
593 }
594 }
595 return total;
596 }
597
598 double AnimeListPage::GetAverageScore() {
599 double avg = 0;
600 int amt = 0;
601 for (auto& list : anime_lists) {
602 for (auto& anime : list) {
603 avg += anime.score;
604 if (anime.score != 0)
605 amt++;
606 }
607 }
608 return avg/amt;
609 }
610
611 double AnimeListPage::GetScoreDeviation() {
612 double squares_sum = 0, avg = GetAverageScore();
613 int amt = 0;
614 for (auto& list : anime_lists) {
615 for (auto& anime : list) {
616 if (anime.score != 0) {
617 squares_sum += std::pow((double)anime.score - avg, 2);
618 amt++;
619 }
620 }
621 }
622 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
623 }
624
625 #include "moc_anime.cpp"