Mercurial > minori
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" |