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"