comparison src/anime.cpp @ 1:1ae666fdf9e2

*: initial commit
author Paper <mrpapersonic@gmail.com>
date Tue, 08 Aug 2023 19:49:15 -0400
parents
children 23d0d9319a00
comparison
equal deleted inserted replaced
0:5a76e1b94163 1:1ae666fdf9e2
1 #include <chrono>
2 #include <string>
3 #include <vector>
4 #include <cmath>
5 #include "window.h"
6 #include "anilist.h"
7 #include "config.h"
8 #include "anime.h"
9 //#include "information.h"
10
11 std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
12 {CURRENT, "Watching"},
13 {PLANNING, "Planning"},
14 {COMPLETED, "Completed"},
15 {DROPPED, "Dropped"},
16 {PAUSED, "On hold"},
17 {REPEATING, "Rewatching"}
18 };
19
20 std::map<enum AnimeAiringStatus, std::string> AnimeAiringToStringMap = {
21 {FINISHED, "Finished"},
22 {RELEASING, "Airing"},
23 {NOT_YET_RELEASED, "Not aired yet"},
24 {CANCELLED, "Cancelled"},
25 {HIATUS, "On hiatus"}
26 };
27
28 std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap = {
29 {WINTER, "Winter"},
30 {SPRING, "Spring"},
31 {SUMMER, "Summer"},
32 {FALL, "Fall"}
33 };
34
35 std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap = {
36 {TV, "TV"},
37 {TV_SHORT, "TV short"},
38 {MOVIE, "Movie"},
39 {SPECIAL, "Special"},
40 {OVA, "OVA"},
41 {ONA, "ONA"},
42 {MUSIC, "Music video"},
43 /* these should NEVER be in the list. naybe we should
44 remove them? */
45 {MANGA, "Manga"},
46 {NOVEL, "Novel"},
47 {ONE_SHOT, "One-shot"}
48 };
49
50 Anime::Anime() {}
51 Anime::Anime(const Anime& a) {
52 status = a.status;
53 progress = a.progress;
54 score = a.score;
55 started = a.started;
56 completed = a.completed;
57 notes = a.notes;
58 id = a.id;
59 title = a.title;
60 episodes = a.episodes;
61 airing = a.airing;
62 air_date = a.air_date;
63 genres = a.genres;
64 producers = a.producers;
65 type = a.type;
66 season = a.season;
67 audience_score = a.audience_score;
68 synopsis = a.synopsis;
69 duration = a.duration;
70 }
71
72 void AnimeList::Add(Anime& anime) {
73 if (anime_id_to_anime.contains(anime.id))
74 return;
75 anime_list.push_back(anime);
76 anime_id_to_anime.emplace(anime.id, &anime);
77 }
78
79 void AnimeList::Insert(size_t pos, Anime& anime) {
80 if (anime_id_to_anime.contains(anime.id))
81 return;
82 anime_list.insert(anime_list.begin()+pos, anime);
83 anime_id_to_anime.emplace(anime.id, &anime);
84 }
85
86 void AnimeList::Delete(size_t index) {
87 anime_list.erase(anime_list.begin()+index);
88 }
89
90 void AnimeList::Clear() {
91 anime_list.clear();
92 }
93
94 size_t AnimeList::Size() const {
95 return anime_list.size();
96 }
97
98 std::vector<Anime>::iterator AnimeList::begin() noexcept {
99 return anime_list.begin();
100 }
101
102 std::vector<Anime>::iterator AnimeList::end() noexcept {
103 return anime_list.end();
104 }
105
106 std::vector<Anime>::const_iterator AnimeList::cbegin() noexcept {
107 return anime_list.cbegin();
108 }
109
110 std::vector<Anime>::const_iterator AnimeList::cend() noexcept {
111 return anime_list.cend();
112 }
113
114 AnimeList::AnimeList() {}
115 AnimeList::AnimeList(const AnimeList& l) {
116 for (int i = 0; i < l.Size(); i++) {
117 anime_list.push_back(Anime(l[i]));
118 }
119 name = l.name;
120 }
121
122 AnimeList::~AnimeList() {
123 anime_list.clear();
124 anime_list.shrink_to_fit();
125 }
126
127 Anime* AnimeList::AnimeById(int id) {
128 return anime_id_to_anime.contains(id) ? anime_id_to_anime[id] : nullptr;
129 }
130
131 bool AnimeList::AnimeInList(int id) {
132 return anime_id_to_anime.contains(id);
133 }
134
135 Anime& AnimeList::operator[](std::size_t index) {
136 return anime_list.at(index);
137 }
138
139 const Anime& AnimeList::operator[](std::size_t index) const {
140 return anime_list.at(index);
141 }
142
143 /* ------------------------------------------------------------------------- */
144
145 /* Thank you qBittorrent for having a great example of a
146 widget model. */
147 AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
148 : QAbstractListModel(parent)
149 , list(*alist) {
150 return;
151 }
152
153 int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
154 return list.Size();
155 }
156
157 int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
158 return NB_COLUMNS;
159 }
160
161 QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
162 if (role == Qt::DisplayRole) {
163 switch (section) {
164 case AL_TITLE:
165 return tr("Anime title");
166 case AL_PROGRESS:
167 return tr("Progress");
168 case AL_TYPE:
169 return tr("Type");
170 case AL_SCORE:
171 return tr("Score");
172 case AL_SEASON:
173 return tr("Season");
174 case AL_STARTED:
175 return tr("Date started");
176 case AL_COMPLETED:
177 return tr("Date completed");
178 case AL_NOTES:
179 return tr("Notes");
180 case AL_AVG_SCORE:
181 return tr("Average score");
182 case AL_UPDATED:
183 return tr("Last updated");
184 default:
185 return {};
186 }
187 } else if (role == Qt::TextAlignmentRole) {
188 switch (section) {
189 case AL_TITLE:
190 case AL_NOTES:
191 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
192 case AL_PROGRESS:
193 case AL_TYPE:
194 case AL_SCORE:
195 case AL_AVG_SCORE:
196 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
197 case AL_SEASON:
198 case AL_STARTED:
199 case AL_COMPLETED:
200 case AL_UPDATED:
201 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
202 default:
203 return QAbstractListModel::headerData(section, orientation, role);
204 }
205 }
206 return QAbstractListModel::headerData(section, orientation, role);
207 }
208
209 Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
210 return (!index.isValid()) ? &(list[index.row()]) : nullptr;
211 }
212
213 QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
214 if (!index.isValid())
215 return QVariant();
216 if (role == Qt::DisplayRole) {
217 switch (index.column()) {
218 case AL_TITLE:
219 return QString::fromWCharArray(list[index.row()].title.c_str());
220 case AL_PROGRESS:
221 return list[index.row()].progress;
222 case AL_SCORE:
223 return list[index.row()].score;
224 case AL_TYPE:
225 return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
226 case AL_SEASON:
227 return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number((int)list[index.row()].air_date.year());
228 case AL_AVG_SCORE:
229 return list[index.row()].audience_score;
230 case AL_STARTED:
231 /* why c++20 chrono is stinky: the game */
232 return QDate(int(list[index.row()].started.year()), static_cast<int>((unsigned int)list[index.row()].started.month()), static_cast<int>((unsigned int)list[index.row()].started.day()));
233 case AL_COMPLETED:
234 return QDate(int(list[index.row()].completed.year()), static_cast<int>((unsigned int)list[index.row()].completed.month()), static_cast<int>((unsigned int)list[index.row()].completed.day()));
235 case AL_NOTES:
236 return QString::fromWCharArray(list[index.row()].notes.c_str());
237 default:
238 return "";
239 }
240 } else if (role == Qt::TextAlignmentRole) {
241 switch (index.column()) {
242 case AL_TITLE:
243 case AL_NOTES:
244 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
245 case AL_PROGRESS:
246 case AL_TYPE:
247 case AL_SCORE:
248 case AL_AVG_SCORE:
249 return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
250 case AL_SEASON:
251 case AL_STARTED:
252 case AL_COMPLETED:
253 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
254 default:
255 break;
256 }
257 }
258 return QVariant();
259 }
260
261 /* this should ALWAYS be called if the list is edited */
262 void AnimeListWidgetModel::Update() {
263
264 }
265
266 /* Most of this stuff is const and/or should be edited in the Information dialog
267
268 bool AnimeListWidgetModel::setData(const QModelIndex &index, const QVariant &value, int role) {
269 if (!index.isValid() || role != Qt::DisplayRole)
270 return false;
271
272 Anime* const anime = &list[index.row()];
273
274 switch (index.column()) {
275 case AL_TITLE:
276 break;
277 case AL_CATEGORY:
278 break;
279 default:
280 return false;
281 }
282
283 return true;
284 }
285 */
286
287 int AnimeListWidget::VisibleColumnsCount() const {
288 int count = 0;
289
290 for (int i = 0, end = header()->count(); i < end; i++)
291 {
292 if (!isColumnHidden(i))
293 count++;
294 }
295
296 return count;
297 }
298
299 void AnimeListWidget::SetColumnDefaults() {
300 setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
301 setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
302 setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
303 setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
304 setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
305 setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
306 setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
307 setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
308 setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
309 setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
310 setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
311 }
312
313 void AnimeListWidget::DisplayColumnHeaderMenu() {
314 QMenu *menu = new QMenu(this);
315 menu->setAttribute(Qt::WA_DeleteOnClose);
316 menu->setTitle(tr("Column visibility"));
317 menu->setToolTipsVisible(true);
318
319 for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++)
320 {
321 if (i == AnimeListWidgetModel::AL_TITLE)
322 continue;
323 const auto column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
324 QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
325 if (!checked && (VisibleColumnsCount() <= 1))
326 return;
327
328 setColumnHidden(i, !checked);
329
330 if (checked && (columnWidth(i) <= 5))
331 resizeColumnToContents(i);
332
333 // SaveSettings();
334 });
335 action->setCheckable(true);
336 action->setChecked(!isColumnHidden(i));
337 }
338
339 menu->addSeparator();
340 QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]()
341 {
342 for (int i = 0, count = header()->count(); i < count; ++i)
343 {
344 SetColumnDefaults();
345 }
346 // SaveSettings();
347 });
348
349 menu->popup(QCursor::pos());
350 }
351
352 void AnimeListWidget::DisplayListMenu() {
353 /* throw out any other garbage */
354 const QModelIndexList selected_items = selectionModel()->selectedRows();
355 if (selected_items.size() != 1 || !selected_items.first().isValid())
356 return;
357
358 const QModelIndex index = model->index(selected_items.first().row());
359 Anime* anime = model->GetAnimeFromIndex(index);
360 if (!anime)
361 return;
362
363 }
364
365 void AnimeListWidget::ItemDoubleClicked() {
366 /* throw out any other garbage */
367 const QModelIndexList selected_items = selectionModel()->selectedRows();
368 if (selected_items.size() != 1 || !selected_items.first().isValid())
369 return;
370
371 /* TODO: after we implement our sort model, we have to use mapToSource here... */
372 const QModelIndex index = model->index(selected_items.first().row());
373 Anime* anime = model->GetAnimeFromIndex(index);
374 if (!anime)
375 return;
376
377 /* todo: open information dialog... */
378 }
379
380 AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
381 : QTreeView(parent) {
382 model = new AnimeListWidgetModel(parent, alist);
383 this->setModel(model);
384 setUniformRowHeights(true);
385 setAllColumnsShowFocus(false);
386 setSortingEnabled(true);
387 setSelectionMode(QAbstractItemView::ExtendedSelection);
388 setItemsExpandable(false);
389 setRootIsDecorated(false);
390 setContextMenuPolicy(Qt::CustomContextMenu);
391 connect(this, &QAbstractItemView::doubleClicked, this, &ItemDoubleClicked);
392 connect(this, &QWidget::customContextMenuRequested, this, &DisplayListMenu);
393
394 /* Enter & return keys */
395 connect(new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut),
396 &QShortcut::activated, this, &ItemDoubleClicked);
397
398 connect(new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut),
399 &QShortcut::activated, this, &ItemDoubleClicked);
400
401 header()->setStretchLastSection(false);
402 header()->setContextMenuPolicy(Qt::CustomContextMenu);
403 connect(header(), &QWidget::customContextMenuRequested, this, &DisplayColumnHeaderMenu);
404 // if(!session.config.anime_list.columns) {
405 SetColumnDefaults();
406 // }
407 }
408
409 AnimeListPage::AnimeListPage(QWidget* parent) : QTabWidget (parent) {
410 setDocumentMode(true);
411 SyncAnimeList();
412 for (AnimeList& list : anime_lists) {
413 addTab(new AnimeListWidget(this, &list), QString::fromWCharArray(list.name.c_str()));
414 }
415 }
416
417 void AnimeListPage::SyncAnimeList() {
418 switch (session.config.service) {
419 case ANILIST: {
420 AniList anilist = AniList();
421 anilist.Authorize();
422 session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username);
423 FreeAnimeList();
424 anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
425 break;
426 }
427 }
428 }
429
430 void AnimeListPage::FreeAnimeList() {
431 if (anime_lists.size() > 0) {
432 /* FIXME: we may not need this, but to prevent memleaks
433 we should keep it until we're sure we don't */
434 for (auto& list : anime_lists) {
435 list.Clear();
436 }
437 anime_lists.clear();
438 }
439 }
440
441 int AnimeListPage::GetTotalAnimeAmount() {
442 int total = 0;
443 for (auto& list : anime_lists) {
444 total += list.Size();
445 }
446 return total;
447 }
448
449 int AnimeListPage::GetTotalEpisodeAmount() {
450 /* FIXME: this also needs to take into account rewatches... */
451 int total = 0;
452 for (auto& list : anime_lists) {
453 for (auto& anime : list) {
454 total += anime.progress;
455 }
456 }
457 return total;
458 }
459
460 /* Returns the total watched amount in minutes. */
461 int AnimeListPage::GetTotalWatchedAmount() {
462 int total = 0;
463 for (auto& list : anime_lists) {
464 for (auto& anime : list) {
465 total += anime.duration*anime.progress;
466 }
467 }
468 return total;
469 }
470
471 /* Returns the total planned amount in minutes.
472 Note that we should probably limit progress to the
473 amount of episodes, as AniList will let you
474 set episode counts up to 32768. But that should
475 rather be handled elsewhere. */
476 int AnimeListPage::GetTotalPlannedAmount() {
477 int total = 0;
478 for (auto& list : anime_lists) {
479 for (auto& anime : list) {
480 total += anime.duration*(anime.episodes-anime.progress);
481 }
482 }
483 return total;
484 }
485
486 double AnimeListPage::GetAverageScore() {
487 double avg = 0;
488 int amt = 0;
489 for (auto& list : anime_lists) {
490 for (auto& anime : list) {
491 avg += anime.score;
492 if (anime.score != 0)
493 amt++;
494 }
495 }
496 return avg/amt;
497 }
498
499 double AnimeListPage::GetScoreDeviation() {
500 double squares_sum = 0, avg = GetAverageScore();
501 int amt = 0;
502 for (auto& list : anime_lists) {
503 for (auto& anime : list) {
504 if (anime.score != 0) {
505 squares_sum += std::pow((double)anime.score - avg, 2);
506 amt++;
507 }
508 }
509 }
510 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
511 }
512
513 #include "moc_anime.cpp"