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