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