Mercurial > minori
diff src/anime.cpp @ 1:1ae666fdf9e2
*: initial commit
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Tue, 08 Aug 2023 19:49:15 -0400 |
parents | |
children | 23d0d9319a00 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/anime.cpp Tue Aug 08 19:49:15 2023 -0400 @@ -0,0 +1,513 @@ +#include <chrono> +#include <string> +#include <vector> +#include <cmath> +#include "window.h" +#include "anilist.h" +#include "config.h" +#include "anime.h" +//#include "information.h" + +std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = { + {CURRENT, "Watching"}, + {PLANNING, "Planning"}, + {COMPLETED, "Completed"}, + {DROPPED, "Dropped"}, + {PAUSED, "On hold"}, + {REPEATING, "Rewatching"} +}; + +std::map<enum AnimeAiringStatus, std::string> AnimeAiringToStringMap = { + {FINISHED, "Finished"}, + {RELEASING, "Airing"}, + {NOT_YET_RELEASED, "Not aired yet"}, + {CANCELLED, "Cancelled"}, + {HIATUS, "On hiatus"} +}; + +std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap = { + {WINTER, "Winter"}, + {SPRING, "Spring"}, + {SUMMER, "Summer"}, + {FALL, "Fall"} +}; + +std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap = { + {TV, "TV"}, + {TV_SHORT, "TV short"}, + {MOVIE, "Movie"}, + {SPECIAL, "Special"}, + {OVA, "OVA"}, + {ONA, "ONA"}, + {MUSIC, "Music video"}, + /* these should NEVER be in the list. naybe we should + remove them? */ + {MANGA, "Manga"}, + {NOVEL, "Novel"}, + {ONE_SHOT, "One-shot"} +}; + +Anime::Anime() {} +Anime::Anime(const Anime& a) { + status = a.status; + progress = a.progress; + score = a.score; + started = a.started; + completed = a.completed; + notes = a.notes; + id = a.id; + title = a.title; + episodes = a.episodes; + airing = a.airing; + air_date = a.air_date; + genres = a.genres; + producers = a.producers; + type = a.type; + season = a.season; + audience_score = a.audience_score; + synopsis = a.synopsis; + duration = a.duration; +} + +void AnimeList::Add(Anime& anime) { + if (anime_id_to_anime.contains(anime.id)) + return; + anime_list.push_back(anime); + anime_id_to_anime.emplace(anime.id, &anime); +} + +void AnimeList::Insert(size_t pos, Anime& anime) { + if (anime_id_to_anime.contains(anime.id)) + return; + anime_list.insert(anime_list.begin()+pos, anime); + anime_id_to_anime.emplace(anime.id, &anime); +} + +void AnimeList::Delete(size_t index) { + anime_list.erase(anime_list.begin()+index); +} + +void AnimeList::Clear() { + anime_list.clear(); +} + +size_t AnimeList::Size() const { + return anime_list.size(); +} + +std::vector<Anime>::iterator AnimeList::begin() noexcept { + return anime_list.begin(); +} + +std::vector<Anime>::iterator AnimeList::end() noexcept { + return anime_list.end(); +} + +std::vector<Anime>::const_iterator AnimeList::cbegin() noexcept { + return anime_list.cbegin(); +} + +std::vector<Anime>::const_iterator AnimeList::cend() noexcept { + return anime_list.cend(); +} + +AnimeList::AnimeList() {} +AnimeList::AnimeList(const AnimeList& l) { + for (int i = 0; i < l.Size(); i++) { + anime_list.push_back(Anime(l[i])); + } + name = l.name; +} + +AnimeList::~AnimeList() { + anime_list.clear(); + anime_list.shrink_to_fit(); +} + +Anime* AnimeList::AnimeById(int id) { + return anime_id_to_anime.contains(id) ? anime_id_to_anime[id] : nullptr; +} + +bool AnimeList::AnimeInList(int id) { + return anime_id_to_anime.contains(id); +} + +Anime& AnimeList::operator[](std::size_t index) { + return anime_list.at(index); +} + +const Anime& AnimeList::operator[](std::size_t index) const { + return anime_list.at(index); +} + +/* ------------------------------------------------------------------------- */ + +/* Thank you qBittorrent for having a great example of a + widget model. */ +AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist) + : QAbstractListModel(parent) + , list(*alist) { + return; +} + +int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const { + return list.Size(); +} + +int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const { + return NB_COLUMNS; +} + +QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const { + if (role == Qt::DisplayRole) { + switch (section) { + case AL_TITLE: + return tr("Anime title"); + case AL_PROGRESS: + return tr("Progress"); + case AL_TYPE: + return tr("Type"); + case AL_SCORE: + return tr("Score"); + case AL_SEASON: + return tr("Season"); + case AL_STARTED: + return tr("Date started"); + case AL_COMPLETED: + return tr("Date completed"); + case AL_NOTES: + return tr("Notes"); + case AL_AVG_SCORE: + return tr("Average score"); + case AL_UPDATED: + return tr("Last updated"); + default: + return {}; + } + } else if (role == Qt::TextAlignmentRole) { + switch (section) { + case AL_TITLE: + case AL_NOTES: + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case AL_PROGRESS: + case AL_TYPE: + case AL_SCORE: + case AL_AVG_SCORE: + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case AL_SEASON: + case AL_STARTED: + case AL_COMPLETED: + case AL_UPDATED: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: + return QAbstractListModel::headerData(section, orientation, role); + } + } + return QAbstractListModel::headerData(section, orientation, role); +} + +Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) { + return (!index.isValid()) ? &(list[index.row()]) : nullptr; +} + +QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) + return QVariant(); + if (role == Qt::DisplayRole) { + switch (index.column()) { + case AL_TITLE: + return QString::fromWCharArray(list[index.row()].title.c_str()); + case AL_PROGRESS: + return list[index.row()].progress; + case AL_SCORE: + return list[index.row()].score; + case AL_TYPE: + return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]); + case AL_SEASON: + return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number((int)list[index.row()].air_date.year()); + case AL_AVG_SCORE: + return list[index.row()].audience_score; + case AL_STARTED: + /* why c++20 chrono is stinky: the game */ + 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())); + case AL_COMPLETED: + 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())); + case AL_NOTES: + return QString::fromWCharArray(list[index.row()].notes.c_str()); + default: + return ""; + } + } else if (role == Qt::TextAlignmentRole) { + switch (index.column()) { + case AL_TITLE: + case AL_NOTES: + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case AL_PROGRESS: + case AL_TYPE: + case AL_SCORE: + case AL_AVG_SCORE: + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case AL_SEASON: + case AL_STARTED: + case AL_COMPLETED: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: + break; + } + } + return QVariant(); +} + +/* this should ALWAYS be called if the list is edited */ +void AnimeListWidgetModel::Update() { + +} + +/* Most of this stuff is const and/or should be edited in the Information dialog + +bool AnimeListWidgetModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (!index.isValid() || role != Qt::DisplayRole) + return false; + + Anime* const anime = &list[index.row()]; + + switch (index.column()) { + case AL_TITLE: + break; + case AL_CATEGORY: + break; + default: + return false; + } + + return true; +} +*/ + +int AnimeListWidget::VisibleColumnsCount() const { + int count = 0; + + for (int i = 0, end = header()->count(); i < end; i++) + { + if (!isColumnHidden(i)) + count++; + } + + return count; +} + +void AnimeListWidget::SetColumnDefaults() { + setColumnHidden(AnimeListWidgetModel::AL_SEASON, false); + setColumnHidden(AnimeListWidgetModel::AL_TYPE, false); + setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false); + setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false); + setColumnHidden(AnimeListWidgetModel::AL_SCORE, false); + setColumnHidden(AnimeListWidgetModel::AL_TITLE, false); + setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true); + setColumnHidden(AnimeListWidgetModel::AL_STARTED, true); + setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true); + setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true); + setColumnHidden(AnimeListWidgetModel::AL_NOTES, true); +} + +void AnimeListWidget::DisplayColumnHeaderMenu() { + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setTitle(tr("Column visibility")); + menu->setToolTipsVisible(true); + + for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) + { + if (i == AnimeListWidgetModel::AL_TITLE) + continue; + const auto column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) { + if (!checked && (VisibleColumnsCount() <= 1)) + return; + + setColumnHidden(i, !checked); + + if (checked && (columnWidth(i) <= 5)) + resizeColumnToContents(i); + + // SaveSettings(); + }); + action->setCheckable(true); + action->setChecked(!isColumnHidden(i)); + } + + menu->addSeparator(); + QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() + { + for (int i = 0, count = header()->count(); i < count; ++i) + { + SetColumnDefaults(); + } + // SaveSettings(); + }); + + menu->popup(QCursor::pos()); +} + +void AnimeListWidget::DisplayListMenu() { + /* throw out any other garbage */ + const QModelIndexList selected_items = selectionModel()->selectedRows(); + if (selected_items.size() != 1 || !selected_items.first().isValid()) + return; + + const QModelIndex index = model->index(selected_items.first().row()); + Anime* anime = model->GetAnimeFromIndex(index); + if (!anime) + return; + +} + +void AnimeListWidget::ItemDoubleClicked() { + /* throw out any other garbage */ + const QModelIndexList selected_items = selectionModel()->selectedRows(); + if (selected_items.size() != 1 || !selected_items.first().isValid()) + return; + + /* TODO: after we implement our sort model, we have to use mapToSource here... */ + const QModelIndex index = model->index(selected_items.first().row()); + Anime* anime = model->GetAnimeFromIndex(index); + if (!anime) + return; + + /* todo: open information dialog... */ +} + +AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist) + : QTreeView(parent) { + model = new AnimeListWidgetModel(parent, alist); + this->setModel(model); + setUniformRowHeights(true); + setAllColumnsShowFocus(false); + setSortingEnabled(true); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setItemsExpandable(false); + setRootIsDecorated(false); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QAbstractItemView::doubleClicked, this, &ItemDoubleClicked); + connect(this, &QWidget::customContextMenuRequested, this, &DisplayListMenu); + + /* Enter & return keys */ + connect(new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut), + &QShortcut::activated, this, &ItemDoubleClicked); + + connect(new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut), + &QShortcut::activated, this, &ItemDoubleClicked); + + header()->setStretchLastSection(false); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(header(), &QWidget::customContextMenuRequested, this, &DisplayColumnHeaderMenu); + // if(!session.config.anime_list.columns) { + SetColumnDefaults(); + // } +} + +AnimeListPage::AnimeListPage(QWidget* parent) : QTabWidget (parent) { + setDocumentMode(true); + SyncAnimeList(); + for (AnimeList& list : anime_lists) { + addTab(new AnimeListWidget(this, &list), QString::fromWCharArray(list.name.c_str())); + } +} + +void AnimeListPage::SyncAnimeList() { + switch (session.config.service) { + case ANILIST: { + AniList anilist = AniList(); + anilist.Authorize(); + session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username); + FreeAnimeList(); + anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id); + break; + } + } +} + +void AnimeListPage::FreeAnimeList() { + if (anime_lists.size() > 0) { + /* FIXME: we may not need this, but to prevent memleaks + we should keep it until we're sure we don't */ + for (auto& list : anime_lists) { + list.Clear(); + } + anime_lists.clear(); + } +} + +int AnimeListPage::GetTotalAnimeAmount() { + int total = 0; + for (auto& list : anime_lists) { + total += list.Size(); + } + return total; +} + +int AnimeListPage::GetTotalEpisodeAmount() { + /* FIXME: this also needs to take into account rewatches... */ + int total = 0; + for (auto& list : anime_lists) { + for (auto& anime : list) { + total += anime.progress; + } + } + return total; +} + +/* Returns the total watched amount in minutes. */ +int AnimeListPage::GetTotalWatchedAmount() { + int total = 0; + for (auto& list : anime_lists) { + for (auto& anime : list) { + total += anime.duration*anime.progress; + } + } + return total; +} + +/* Returns the total planned amount in minutes. + Note that we should probably limit progress to the + amount of episodes, as AniList will let you + set episode counts up to 32768. But that should + rather be handled elsewhere. */ +int AnimeListPage::GetTotalPlannedAmount() { + int total = 0; + for (auto& list : anime_lists) { + for (auto& anime : list) { + total += anime.duration*(anime.episodes-anime.progress); + } + } + return total; +} + +double AnimeListPage::GetAverageScore() { + double avg = 0; + int amt = 0; + for (auto& list : anime_lists) { + for (auto& anime : list) { + avg += anime.score; + if (anime.score != 0) + amt++; + } + } + return avg/amt; +} + +double AnimeListPage::GetScoreDeviation() { + double squares_sum = 0, avg = GetAverageScore(); + int amt = 0; + for (auto& list : anime_lists) { + for (auto& anime : list) { + if (anime.score != 0) { + squares_sum += std::pow((double)anime.score - avg, 2); + amt++; + } + } + } + return (amt > 0) ? std::sqrt(squares_sum / amt) : 0; +} + +#include "moc_anime.cpp"