diff src/anime.cpp @ 2:23d0d9319a00

Update Also converted everything to LF from CRLF
author Paper <mrpapersonic@gmail.com>
date Sat, 12 Aug 2023 03:16:26 -0400
parents 1ae666fdf9e2
children 190ded9438c0
line wrap: on
line diff
--- a/src/anime.cpp	Tue Aug 08 19:49:15 2023 -0400
+++ b/src/anime.cpp	Sat Aug 12 03:16:26 2023 -0400
@@ -1,513 +1,539 @@
-#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"
+#include <chrono>
+#include <string>
+#include <vector>
+#include <cmath>
+#include "window.h"
+#include "anilist.h"
+#include "config.h"
+#include "anime.h"
+#include "date.h"
+#include "time_utils.h"
+#include "information.h"
+#include "ui_utils.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 = {
+	{UNKNOWN, "Unknown"},
+	{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;
+	updated = a.updated;
+	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);
+}
+
+int AnimeList::GetAnimeIndex(Anime& anime) const {
+	for (int i = 0; i < Size(); i++) {
+		if (&anime_list.at(i) == &anime) { // lazy
+			return i;
+		}
+	}
+	return -1;
+}
+
+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.english.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(list[index.row()].air_date.GetYear());
+			case AL_AVG_SCORE:
+				return list[index.row()].audience_score;
+			case AL_STARTED:
+				return list[index.row()].started.GetAsQDate();
+			case AL_COMPLETED:
+				return list[index.row()].completed.GetAsQDate();
+			case AL_UPDATED: {
+				if (list[index.row()].updated == 0)
+					return QString("-");
+				Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
+				return QString::fromStdString(duration.AsRelativeString());
+			}
+			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:
+			case AL_UPDATED:
+				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+			default:
+				break;
+		}
+	}
+	return QVariant();
+}
+
+void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
+	int i = list.GetAnimeIndex(anime);
+	emit dataChanged(index(i), index(i));
+}
+
+/* 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;
+
+	InformationDialog* dialog = new InformationDialog(*anime, model, this);
+
+    dialog->show();
+    dialog->raise();
+    dialog->activateWindow();
+}
+
+AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
+                               : QTreeView(parent) {
+	model = new AnimeListWidgetModel(parent, alist);
+	setModel(model);
+	setObjectName("listwidget");
+	setStyleSheet("QTreeView#listwidget{border-top:0px;}");
+	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(false);
+	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"