changeset 10:4b198a111713

Update things actually compile now btw qttest wants to fuck over the model but that might be my fault so /shrug
author Paper <mrpapersonic@gmail.com>
date Sat, 16 Sep 2023 02:06:01 -0400
parents 5c0397762b53
children fc1bf97c528b
files CMakeLists.txt include/core/anime.h include/core/anime_db.h include/core/session.h include/core/strings.h include/gui/pages/anime_list.h include/gui/sidebar.h include/gui/translate/anime.h include/services/anilist.h include/services/services.h src/core/anime.cpp src/core/anime_db.cpp src/core/date.cpp src/gui/dialog/information.cpp src/gui/dialog/settings/application.cpp src/gui/dialog/settings/services.cpp src/gui/pages/anime_list.cpp src/gui/pages/statistics.cpp src/gui/translate/anime.cpp src/gui/window.cpp src/main.cpp src/services/anilist.cpp src/services/services.cpp
diffstat 23 files changed, 1237 insertions(+), 1164 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sun Sep 10 03:59:16 2023 -0400
+++ b/CMakeLists.txt	Sat Sep 16 02:06:01 2023 -0400
@@ -33,7 +33,11 @@
 	src/gui/pages/now_playing.cpp
 	src/gui/pages/statistics.cpp
 
+	# Translate
+	src/gui/translate/anime.cpp
+
 	# Services (only AniList for now)
+	src/services/services.cpp
 	src/services/anilist.cpp
 
 	# Qt resources
@@ -55,11 +59,12 @@
 set_property(TARGET weeaboo PROPERTY AUTOMOC ON)
 set_property(TARGET weeaboo PROPERTY AUTORCC ON)
 
-find_package(Qt5 COMPONENTS Widgets REQUIRED)
+find_package(Qt5 COMPONENTS Widgets Test REQUIRED)
 find_package(CURL REQUIRED)
 
 set(LIBRARIES
 	${Qt5Widgets_LIBRARIES}
+	${Qt5Test_LIBRARIES}
 	${CURL_LIBRARIES}
 	anitomy
 )
@@ -69,7 +74,7 @@
 	list(APPEND LIBRARIES ${COCOA_LIBRARY})
 endif()
 
-target_include_directories(weeaboo PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} PRIVATE include)
+target_include_directories(weeaboo PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Test_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} PRIVATE include)
 target_compile_options(weeaboo PRIVATE -Wall -Wextra -Wsuggest-override)
 if(APPLE)
 	target_compile_definitions(weeaboo PUBLIC MACOSX)
--- a/include/core/anime.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/core/anime.h	Sat Sep 16 02:06:01 2023 -0400
@@ -111,6 +111,7 @@
 		std::string GetUserNotes() const;
 
 		void SetUserStatus(ListStatus status);
+		void SetUserScore(int score);
 		void SetUserProgress(int progress);
 		void SetUserDateStarted(Date const& started);
 		void SetUserDateCompleted(Date const& completed);
@@ -163,7 +164,7 @@
 
 	private:
 		SeriesInformation info_;
-		std::unique_ptr<struct ListInformation> list_info_;
+		std::shared_ptr<struct ListInformation> list_info_;
 };
 
 } // namespace Anime
--- a/include/core/anime_db.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/core/anime_db.h	Sat Sep 16 02:06:01 2023 -0400
@@ -1,23 +1,23 @@
-#ifndef __core__anime_db_h
-#define __core__anime_db_h
-#include <unordered_map>
-
-namespace Anime {
-
-class Anime;
-
-class Database {
-	public:
-		std::unordered_map<int, Anime> items;
-		int GetTotalAnimeAmount();
-		int GetTotalEpisodeAmount();
-		int GetTotalWatchedAmount();
-		int GetTotalPlannedAmount();
-		double GetAverageScore();
-		double GetScoreDeviation();
-};
-
-inline Database db;
-
-} // namespace Anime
-#endif // __core__anime_db_h
+#ifndef __core__anime_db_h
+#define __core__anime_db_h
+#include "core/anime.h"
+#include <unordered_map>
+
+namespace Anime {
+
+class Database {
+	public:
+		std::unordered_map<int, Anime> items;
+		int GetTotalAnimeAmount();
+		int GetTotalEpisodeAmount();
+		int GetTotalWatchedAmount();
+		int GetTotalPlannedAmount();
+		double GetAverageScore();
+		double GetScoreDeviation();
+		int GetListsAnimeAmount(ListStatus status);
+};
+
+inline Database db;
+
+} // namespace Anime
+#endif // __core__anime_db_h
--- a/include/core/session.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/core/session.h	Sat Sep 16 02:06:01 2023 -0400
@@ -1,17 +1,17 @@
-#ifndef __core__session_h
-#define __core__session_h
-#include "core/config.h"
-#include <QElapsedTimer>
-
-struct Session {
-		Config config;
-		Session() { timer.start(); }
-		int uptime() { return timer.elapsed(); }
-
-	private:
-		QElapsedTimer timer;
-};
-
-extern Session session;
-
+#ifndef __core__session_h
+#define __core__session_h
+#include "core/config.h"
+#include <QElapsedTimer>
+
+struct Session {
+		Config config;
+		Session() { timer.start(); }
+		int uptime() { return timer.elapsed(); }
+
+	private:
+		QElapsedTimer timer;
+};
+
+extern Session session;
+
 #endif // __core__session_h
\ No newline at end of file
--- a/include/core/strings.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/core/strings.h	Sat Sep 16 02:06:01 2023 -0400
@@ -2,7 +2,7 @@
 #define __core__strings_h
 #include <string>
 #include <vector>
-namespace StringUtils {
+namespace Strings {
 /* Implode function: takes a vector of strings and turns it
    into a string, separated by delimiters. */
 std::string Implode(const std::vector<std::string>& vector, const std::string& delimiter);
--- a/include/gui/pages/anime_list.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/gui/pages/anime_list.h	Sat Sep 16 02:06:01 2023 -0400
@@ -1,92 +1,98 @@
-#ifndef __gui__pages__anime_list_h
-#define __gui__pages__anime_list_h
-#include "core/anime.h"
-#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QStyledItemDelegate>
-#include <QTreeView>
-#include <QWidget>
-#include <vector>
-
-class AnimeListWidgetDelegate : public QStyledItemDelegate {
-	Q_OBJECT
-
-	public:
-		explicit AnimeListWidgetDelegate(QObject* parent);
-
-		QWidget* createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const override;
-		void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
-};
-
-class AnimeListWidgetSortFilter : public QSortFilterProxyModel {
-	Q_OBJECT
-
-	public:
-		AnimeListWidgetSortFilter(QObject* parent = nullptr);
-
-	protected:
-		bool lessThan(const QModelIndex& l, const QModelIndex& r) const override;
-};
-
-class AnimeListWidgetModel : public QAbstractListModel {
-	Q_OBJECT
-
-	public:
-		enum columns {
-			AL_TITLE,
-			AL_PROGRESS,
-			AL_EPISODES,
-			AL_SCORE,
-			AL_AVG_SCORE,
-			AL_TYPE,
-			AL_SEASON,
-			AL_STARTED,
-			AL_COMPLETED,
-			AL_UPDATED,
-			AL_NOTES,
-			AL_ID, /* Note: This is only used in Qt::UserRole to make my life easier */
-
-			NB_COLUMNS
-		};
-
-		AnimeListWidgetModel(QWidget* parent);
-		~AnimeListWidgetModel() override = default;
-		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
-		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
-		QVariant data(const QModelIndex& index, int role) const override;
-		QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override;
-		void UpdateAnime(int id);
-};
-
-/* todo: rename these to "page" or something more
-   sensible than "widget" */
-class AnimeListWidget : public QWidget {
-	Q_OBJECT
-
-	public:
-		AnimeListWidget(QWidget* parent);
-		void UpdateAnimeList();
-		void Reset();
-
-	protected:
-		void paintEvent(QPaintEvent*) override;
-		void InitStyle(QStyleOptionTabWidgetFrame* option) const;
-		void InitBasicStyle(QStyleOptionTabWidgetFrame* option) const;
-		void SetupLayout();
-		void showEvent(QShowEvent*) override;
-		void resizeEvent(QResizeEvent* e) override;
-
-	private slots:
-		void DisplayColumnHeaderMenu();
-		void DisplayListMenu();
-		void ItemDoubleClicked();
-		void SetColumnDefaults();
-		int VisibleColumnsCount() const;
-
-	private:
-		QTabBar* tab_bar;
-		QTreeView* tree_view;
-		QRect panelRect;
-		AnimeListWidgetSortFilter* sort_models[5];
-};
+#ifndef __gui__pages__anime_list_h
+#define __gui__pages__anime_list_h
+#include "core/anime.h"
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QStyledItemDelegate>
+#include <QTreeView>
+#include <QWidget>
+#include <vector>
+
+class AnimeListWidgetDelegate : public QStyledItemDelegate {
+	Q_OBJECT
+
+	public:
+		explicit AnimeListWidgetDelegate(QObject* parent);
+
+		QWidget* createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const override;
+		void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+};
+
+class AnimeListWidgetSortFilter : public QSortFilterProxyModel {
+	Q_OBJECT
+
+	public:
+		AnimeListWidgetSortFilter(QObject* parent = nullptr);
+
+	protected:
+		bool lessThan(const QModelIndex& l, const QModelIndex& r) const override;
+};
+
+class AnimeListWidgetModel : public QAbstractListModel {
+	Q_OBJECT
+
+	public:
+		enum columns {
+			AL_TITLE,
+			AL_PROGRESS,
+			AL_EPISODES,
+			AL_SCORE,
+			AL_AVG_SCORE,
+			AL_TYPE,
+			AL_SEASON,
+			AL_STARTED,
+			AL_COMPLETED,
+			AL_UPDATED,
+			AL_NOTES,
+			AL_ID, /* Note: This is only used in Qt::UserRole to make my life easier */
+
+			NB_COLUMNS
+		};
+
+		AnimeListWidgetModel(QWidget* parent, Anime::ListStatus _status);
+		~AnimeListWidgetModel() override = default;
+		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+		QVariant data(const QModelIndex& index, int role) const override;
+		QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override;
+		void UpdateAnime(int id);
+		void RefreshList();
+		Anime::Anime* GetAnimeFromIndex(QModelIndex index);
+
+	private:
+		Anime::ListStatus status;
+		std::vector<Anime::Anime> list;
+};
+
+/* todo: rename these to "page" or something more
+   sensible than "widget" */
+class AnimeListWidget : public QWidget {
+	Q_OBJECT
+
+	public:
+		AnimeListWidget(QWidget* parent);
+		void RefreshList();
+		void Reset();
+
+	protected:
+		void paintEvent(QPaintEvent*) override;
+		void InitStyle(QStyleOptionTabWidgetFrame* option) const;
+		void InitBasicStyle(QStyleOptionTabWidgetFrame* option) const;
+		void SetupLayout();
+		void showEvent(QShowEvent*) override;
+		void resizeEvent(QResizeEvent* e) override;
+
+	private slots:
+		void DisplayColumnHeaderMenu();
+		void DisplayListMenu();
+		void ItemDoubleClicked();
+		void SetColumnDefaults();
+		int VisibleColumnsCount() const;
+
+	private:
+		QTabBar* tab_bar;
+		QTreeView* tree_view;
+		QRect panelRect;
+		AnimeListWidgetSortFilter* sort_models[5];
+};
 #endif // __gui__pages__anime_list_h
\ No newline at end of file
--- a/include/gui/sidebar.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/gui/sidebar.h	Sat Sep 16 02:06:01 2023 -0400
@@ -1,25 +1,25 @@
-#ifndef __gui__sidebar_h
-#define __gui__sidebar_h
-#include <QItemSelectionModel>
-#include <QListWidget>
-#include <QListWidgetItem>
-class SideBar : public QListWidget {
-		Q_OBJECT
-
-	public:
-		SideBar(QWidget* parent = nullptr);
-		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
-		QListWidgetItem* AddSeparator();
-		bool IndexIsSeparator(QModelIndex index) const;
-		static QIcon CreateIcon(const char* file);
-
-	signals:
-		void CurrentItemChanged(int index);
-
-	protected:
-		virtual void mouseMoveEvent(QMouseEvent* event) override;
-		QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& index,
-															 const QEvent* event) const override;
-		int RemoveSeparatorsFromIndex(int index);
-};
-#endif // __gui__sidebar_h
+#ifndef __gui__sidebar_h
+#define __gui__sidebar_h
+#include <QItemSelectionModel>
+#include <QListWidget>
+#include <QListWidgetItem>
+class SideBar : public QListWidget {
+		Q_OBJECT
+
+	public:
+		SideBar(QWidget* parent = nullptr);
+		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
+		QListWidgetItem* AddSeparator();
+		bool IndexIsSeparator(QModelIndex index) const;
+		static QIcon CreateIcon(const char* file);
+
+	signals:
+		void CurrentItemChanged(int index);
+
+	protected:
+		virtual void mouseMoveEvent(QMouseEvent* event) override;
+		QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& index,
+															 const QEvent* event) const override;
+		int RemoveSeparatorsFromIndex(int index);
+};
+#endif // __gui__sidebar_h
--- a/include/gui/translate/anime.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/gui/translate/anime.h	Sat Sep 16 02:06:01 2023 -0400
@@ -1,10 +1,10 @@
-#include "core/anime.h"
-
-namespace Translate {
-
-std::string TranslateListStatus(const Anime::ListStatus status);
-std::string TranslateSeriesFormat(const Anime::SeriesFormat format);
-std::string TranslateSeriesSeason(const Anime::SeriesSeason season);
-std::string TranslateSeriesStatus(const Anime::SeriesStatus status);
-
-} // namespace Translate
+#include "core/anime.h"
+
+namespace Translate {
+
+std::string TranslateListStatus(const Anime::ListStatus status);
+std::string TranslateSeriesFormat(const Anime::SeriesFormat format);
+std::string TranslateSeriesSeason(const Anime::SeriesSeason season);
+std::string TranslateSeriesStatus(const Anime::SeriesStatus status);
+
+} // namespace Translate
--- a/include/services/anilist.h	Sun Sep 10 03:59:16 2023 -0400
+++ b/include/services/anilist.h	Sat Sep 16 02:06:01 2023 -0400
@@ -3,8 +3,8 @@
 #include "core/anime.h"
 #include "core/json.h"
 #include <curl/curl.h>
-namespace AniList {
-int Authorize();
+namespace Services::AniList {
+int AuthorizeUser();
 
 /* Read queries */
 int GetAnimeList();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/services/services.h	Sat Sep 16 02:06:01 2023 -0400
@@ -0,0 +1,11 @@
+#ifndef __services__services_h
+#define __services__services_h
+
+namespace Services {
+
+void Synchronize();
+void Authorize();
+
+};
+
+#endif // __services__services_h
\ No newline at end of file
--- a/src/core/anime.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/core/anime.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -21,14 +21,10 @@
 }
 
 void Anime::AddToUserList() {
-	if (!list_info_.get())
-		return;
 	list_info_.reset(new ListInformation);
 }
 
 void Anime::RemoveFromUserList() {
-	if (list_info_.get())
-		return;
 	list_info_.reset();
 }
 
@@ -87,6 +83,11 @@
 	list_info_->status = status;
 }
 
+void Anime::SetUserScore(int score) {
+	assert(list_info_.get());
+	list_info_->score = score;
+}
+
 void Anime::SetUserProgress(int progress) {
 	assert(list_info_.get());
 	list_info_->progress = progress;
--- a/src/core/anime_db.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/core/anime_db.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,78 +1,89 @@
-#include "core/anime_db.h"
-#include "core/anime.h"
-
-namespace Anime {
-
-int Database::GetTotalAnimeAmount() {
-	int total = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.IsInUserList())
-			total++;
-	}
-	return total;
-}
-
-int Database::GetTotalEpisodeAmount() {
-	int total = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.IsInUserList()) {
-			total += anime.GetUserRewatchedTimes() * anime.GetEpisodes();
-			total += anime.GetUserProgress();
-		}
-	}
-	return total;
-}
-
-/* Returns the total watched amount in minutes. */
-int Database::GetTotalWatchedAmount() {
-	int total = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.IsInUserList()) {
-			total += anime.GetDuration() * anime.GetUserProgress();
-			total += anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes();
-		}
-	}
-	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 Database::GetTotalPlannedAmount() {
-	int total = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.IsInUserList())
-			total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress());
-	}
-	return total;
-}
-
-/* I'm sure many will appreciate this being called an
-   "average" instead of a "mean" */
-double Database::GetAverageScore() {
-	double avg = 0;
-	int amt = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.IsInUserList() && anime.GetUserScore()) {
-			avg += anime.GetUserScore();
-			amt++;
-		}
-	}
-	return avg / amt;
-}
-
-double Database::GetScoreDeviation() {
-	double squares_sum = 0, avg = GetAverageScore();
-	int amt = 0;
-	for (const auto& [id, anime] : items) {
-		if (anime.GetUserScore()) {
-			squares_sum += std::pow((double)anime.GetUserScore() - avg, 2);
-			amt++;
-		}
-	}
-	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
-}
-
+#include "core/anime_db.h"
+#include "core/anime.h"
+
+namespace Anime {
+
+int Database::GetTotalAnimeAmount() {
+	int total = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList())
+			total++;
+	}
+	return total;
+}
+
+int Database::GetListsAnimeAmount(ListStatus status) {
+	if (status == ListStatus::NOT_IN_LIST)
+		return 0;
+	int total = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList() && anime.GetUserStatus() == status)
+			total++;
+	}
+	return total;
+}
+
+int Database::GetTotalEpisodeAmount() {
+	int total = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList()) {
+			total += anime.GetUserRewatchedTimes() * anime.GetEpisodes();
+			total += anime.GetUserProgress();
+		}
+	}
+	return total;
+}
+
+/* Returns the total watched amount in minutes. */
+int Database::GetTotalWatchedAmount() {
+	int total = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList()) {
+			total += anime.GetDuration() * anime.GetUserProgress();
+			total += anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes();
+		}
+	}
+	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 Database::GetTotalPlannedAmount() {
+	int total = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList())
+			total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress());
+	}
+	return total;
+}
+
+/* I'm sure many will appreciate this being called an
+   "average" instead of a "mean" */
+double Database::GetAverageScore() {
+	double avg = 0;
+	int amt = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.IsInUserList() && anime.GetUserScore()) {
+			avg += anime.GetUserScore();
+			amt++;
+		}
+	}
+	return avg / amt;
+}
+
+double Database::GetScoreDeviation() {
+	double squares_sum = 0, avg = GetAverageScore();
+	int amt = 0;
+	for (const auto& [id, anime] : items) {
+		if (anime.GetUserScore()) {
+			squares_sum += std::pow((double)anime.GetUserScore() - avg, 2);
+			amt++;
+		}
+	}
+	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
+}
+
 } // namespace Anime
\ No newline at end of file
--- a/src/core/date.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/core/date.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -103,7 +103,10 @@
 }
 
 QDate Date::GetAsQDate() const {
-	return QDate(*year, *month, *day);
+	/* QDates don't (yet) support "missing" values */
+	if (year.get() && month.get() && day.get())
+		return QDate(*year, *month, *day);
+	else return QDate();
 }
 
 nlohmann::json Date::GetAsAniListJson() const {
--- a/src/gui/dialog/information.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/dialog/information.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -53,7 +53,7 @@
 
 	/* alt titles */
 	main_information_widget->layout()->addWidget(new UiUtils::SelectableTextParagraph(
-		"Alternative titles", QString::fromUtf8(StringUtils::Implode(anime.GetTitleSynonyms(), ", ").c_str()),
+		"Alternative titles", QString::fromUtf8(Strings::Implode(anime.GetTitleSynonyms(), ", ").c_str()),
 		main_information_widget));
 
 	/* details */
@@ -64,7 +64,7 @@
 				   << Translate::TranslateListStatus(anime.GetUserStatus()).c_str() << "\n"
 				   << Translate::TranslateSeriesSeason(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear()
 				   << "\n"
-				   << StringUtils::Implode(anime.GetGenres(), ", ").c_str() << "\n"
+				   << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
 				   << anime.GetAudienceScore() << "%";
 	main_information_widget->layout()->addWidget(new UiUtils::LabelledTextParagraph(
 		"Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data, main_information_widget));
--- a/src/gui/dialog/settings/application.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/dialog/settings/application.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,125 +1,125 @@
-#include "core/session.h"
-#include "gui/dialog/settings.h"
-#include <QCheckBox>
-#include <QComboBox>
-#include <QGroupBox>
-#include <QPushButton>
-#include <QSizePolicy>
-
-QWidget* SettingsPageApplication::CreateAnimeListWidget() {
-	QWidget* result = new QWidget(this);
-	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QGroupBox* actions_group_box = new QGroupBox(tr("Actions"), result);
-	actions_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	/* Actions/Double click */
-	QWidget* double_click_widget = new QWidget(actions_group_box);
-	QLabel* dc_combo_box_label = new QLabel(tr("Double click:"), double_click_widget);
-	QComboBox* dc_combo_box = new QComboBox(double_click_widget);
-	dc_combo_box->addItem(tr("View anime info"));
-
-	QVBoxLayout* double_click_layout = new QVBoxLayout;
-	double_click_layout->addWidget(dc_combo_box_label);
-	double_click_layout->addWidget(dc_combo_box);
-	double_click_layout->setMargin(0);
-	double_click_widget->setLayout(double_click_layout);
-
-	/* Actions/Middle click */
-	QWidget* middle_click_widget = new QWidget(actions_group_box);
-	QLabel* mc_combo_box_label = new QLabel(tr("Middle click:"), middle_click_widget);
-	QComboBox* mc_combo_box = new QComboBox(middle_click_widget);
-	mc_combo_box->addItem(tr("Play next episode"));
-
-	QVBoxLayout* middle_click_layout = new QVBoxLayout;
-	middle_click_layout->addWidget(mc_combo_box_label);
-	middle_click_layout->addWidget(mc_combo_box);
-	middle_click_layout->setMargin(0);
-	middle_click_widget->setLayout(middle_click_layout);
-
-	/* Actions */
-	QHBoxLayout* actions_layout = new QHBoxLayout;
-	actions_layout->addWidget(double_click_widget);
-	actions_layout->addWidget(middle_click_widget);
-	actions_group_box->setLayout(actions_layout);
-
-	QGroupBox* appearance_group_box = new QGroupBox(tr("Appearance"), result);
-	appearance_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QLabel* lang_combo_box_label = new QLabel(tr("Title language preference:"), appearance_group_box);
-	QComboBox* lang_combo_box = new QComboBox(appearance_group_box);
-	lang_combo_box->addItem(tr("Romaji"));
-	lang_combo_box->addItem(tr("Native"));
-	lang_combo_box->addItem(tr("English"));
-	connect(lang_combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
-			[this](int index) { language = static_cast<Anime::TitleLanguage>(index); });
-	lang_combo_box->setCurrentIndex(static_cast<int>(language));
-
-	QCheckBox* hl_anime_box =
-		new QCheckBox(tr("Highlight anime if next episode is available in library folders"), appearance_group_box);
-	QCheckBox* hl_above_anime_box = new QCheckBox(tr("Display highlighted anime above others"), appearance_group_box);
-	connect(hl_anime_box, &QCheckBox::stateChanged, this, [this, hl_above_anime_box](int state) {
-		highlight_anime_if_available = (state == Qt::Unchecked) ? false : true;
-		hl_above_anime_box->setEnabled(state);
-	});
-	connect(hl_above_anime_box, &QCheckBox::stateChanged, this,
-			[this](int state) { highlight_anime_if_available = (state == Qt::Unchecked) ? false : true; });
-	hl_anime_box->setCheckState(highlight_anime_if_available ? Qt::Checked : Qt::Unchecked);
-	hl_above_anime_box->setCheckState(highlighted_anime_above_others ? Qt::Checked : Qt::Unchecked);
-	hl_above_anime_box->setEnabled(hl_anime_box->checkState() != Qt::Unchecked);
-	hl_above_anime_box->setStyleSheet("margin-left: 10px;");
-
-	/* Appearance */
-	QVBoxLayout* appearance_layout = new QVBoxLayout;
-	appearance_layout->addWidget(lang_combo_box_label);
-	appearance_layout->addWidget(lang_combo_box);
-	appearance_layout->addWidget(hl_anime_box);
-	appearance_layout->addWidget(hl_above_anime_box);
-	appearance_group_box->setLayout(appearance_layout);
-
-	QGroupBox* progress_group_box = new QGroupBox(tr("Progress"), result);
-	progress_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QCheckBox* progress_display_aired_episodes =
-		new QCheckBox(tr("Display aired episodes (estimated)"), progress_group_box);
-	connect(progress_display_aired_episodes, &QCheckBox::stateChanged, this,
-			[this](int state) { display_aired_episodes = (state == Qt::Unchecked) ? false : true; });
-	progress_display_aired_episodes->setCheckState(display_aired_episodes ? Qt::Checked : Qt::Unchecked);
-
-	QCheckBox* progress_display_available_episodes =
-		new QCheckBox(tr("Display available episodes in library folders"), progress_group_box);
-	connect(progress_display_available_episodes, &QCheckBox::stateChanged, this,
-			[this](int state) { display_available_episodes = (state == Qt::Unchecked) ? false : true; });
-	progress_display_available_episodes->setCheckState(display_available_episodes ? Qt::Checked : Qt::Unchecked);
-
-	QVBoxLayout* progress_layout = new QVBoxLayout;
-	progress_layout->addWidget(progress_display_aired_episodes);
-	progress_layout->addWidget(progress_display_available_episodes);
-	progress_group_box->setLayout(progress_layout);
-
-	QVBoxLayout* full_layout = new QVBoxLayout;
-	full_layout->addWidget(actions_group_box);
-	full_layout->addWidget(appearance_group_box);
-	full_layout->addWidget(progress_group_box);
-	full_layout->setSpacing(10);
-	full_layout->addStretch();
-	result->setLayout(full_layout);
-	return result;
-}
-
-void SettingsPageApplication::SaveInfo() {
-	session.config.anime_list.language = language;
-	session.config.anime_list.highlighted_anime_above_others = highlighted_anime_above_others;
-	session.config.anime_list.highlight_anime_if_available = highlight_anime_if_available;
-	session.config.anime_list.display_aired_episodes = display_aired_episodes;
-	session.config.anime_list.display_available_episodes = display_available_episodes;
-}
-
-SettingsPageApplication::SettingsPageApplication(QWidget* parent) : SettingsPage(parent, tr("Application")) {
-	language = session.config.anime_list.language;
-	highlighted_anime_above_others = session.config.anime_list.highlighted_anime_above_others;
-	highlight_anime_if_available = session.config.anime_list.highlight_anime_if_available;
-	display_aired_episodes = session.config.anime_list.display_aired_episodes;
-	display_available_episodes = session.config.anime_list.display_available_episodes;
-	AddTab(CreateAnimeListWidget(), tr("Anime list"));
-}
+#include "core/session.h"
+#include "gui/dialog/settings.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QGroupBox>
+#include <QPushButton>
+#include <QSizePolicy>
+
+QWidget* SettingsPageApplication::CreateAnimeListWidget() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QGroupBox* actions_group_box = new QGroupBox(tr("Actions"), result);
+	actions_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	/* Actions/Double click */
+	QWidget* double_click_widget = new QWidget(actions_group_box);
+	QLabel* dc_combo_box_label = new QLabel(tr("Double click:"), double_click_widget);
+	QComboBox* dc_combo_box = new QComboBox(double_click_widget);
+	dc_combo_box->addItem(tr("View anime info"));
+
+	QVBoxLayout* double_click_layout = new QVBoxLayout;
+	double_click_layout->addWidget(dc_combo_box_label);
+	double_click_layout->addWidget(dc_combo_box);
+	double_click_layout->setMargin(0);
+	double_click_widget->setLayout(double_click_layout);
+
+	/* Actions/Middle click */
+	QWidget* middle_click_widget = new QWidget(actions_group_box);
+	QLabel* mc_combo_box_label = new QLabel(tr("Middle click:"), middle_click_widget);
+	QComboBox* mc_combo_box = new QComboBox(middle_click_widget);
+	mc_combo_box->addItem(tr("Play next episode"));
+
+	QVBoxLayout* middle_click_layout = new QVBoxLayout;
+	middle_click_layout->addWidget(mc_combo_box_label);
+	middle_click_layout->addWidget(mc_combo_box);
+	middle_click_layout->setMargin(0);
+	middle_click_widget->setLayout(middle_click_layout);
+
+	/* Actions */
+	QHBoxLayout* actions_layout = new QHBoxLayout;
+	actions_layout->addWidget(double_click_widget);
+	actions_layout->addWidget(middle_click_widget);
+	actions_group_box->setLayout(actions_layout);
+
+	QGroupBox* appearance_group_box = new QGroupBox(tr("Appearance"), result);
+	appearance_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* lang_combo_box_label = new QLabel(tr("Title language preference:"), appearance_group_box);
+	QComboBox* lang_combo_box = new QComboBox(appearance_group_box);
+	lang_combo_box->addItem(tr("Romaji"));
+	lang_combo_box->addItem(tr("Native"));
+	lang_combo_box->addItem(tr("English"));
+	connect(lang_combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+			[this](int index) { language = static_cast<Anime::TitleLanguage>(index); });
+	lang_combo_box->setCurrentIndex(static_cast<int>(language));
+
+	QCheckBox* hl_anime_box =
+		new QCheckBox(tr("Highlight anime if next episode is available in library folders"), appearance_group_box);
+	QCheckBox* hl_above_anime_box = new QCheckBox(tr("Display highlighted anime above others"), appearance_group_box);
+	connect(hl_anime_box, &QCheckBox::stateChanged, this, [this, hl_above_anime_box](int state) {
+		highlight_anime_if_available = (state == Qt::Unchecked) ? false : true;
+		hl_above_anime_box->setEnabled(state);
+	});
+	connect(hl_above_anime_box, &QCheckBox::stateChanged, this,
+			[this](int state) { highlight_anime_if_available = (state == Qt::Unchecked) ? false : true; });
+	hl_anime_box->setCheckState(highlight_anime_if_available ? Qt::Checked : Qt::Unchecked);
+	hl_above_anime_box->setCheckState(highlighted_anime_above_others ? Qt::Checked : Qt::Unchecked);
+	hl_above_anime_box->setEnabled(hl_anime_box->checkState() != Qt::Unchecked);
+	hl_above_anime_box->setStyleSheet("margin-left: 10px;");
+
+	/* Appearance */
+	QVBoxLayout* appearance_layout = new QVBoxLayout;
+	appearance_layout->addWidget(lang_combo_box_label);
+	appearance_layout->addWidget(lang_combo_box);
+	appearance_layout->addWidget(hl_anime_box);
+	appearance_layout->addWidget(hl_above_anime_box);
+	appearance_group_box->setLayout(appearance_layout);
+
+	QGroupBox* progress_group_box = new QGroupBox(tr("Progress"), result);
+	progress_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QCheckBox* progress_display_aired_episodes =
+		new QCheckBox(tr("Display aired episodes (estimated)"), progress_group_box);
+	connect(progress_display_aired_episodes, &QCheckBox::stateChanged, this,
+			[this](int state) { display_aired_episodes = (state == Qt::Unchecked) ? false : true; });
+	progress_display_aired_episodes->setCheckState(display_aired_episodes ? Qt::Checked : Qt::Unchecked);
+
+	QCheckBox* progress_display_available_episodes =
+		new QCheckBox(tr("Display available episodes in library folders"), progress_group_box);
+	connect(progress_display_available_episodes, &QCheckBox::stateChanged, this,
+			[this](int state) { display_available_episodes = (state == Qt::Unchecked) ? false : true; });
+	progress_display_available_episodes->setCheckState(display_available_episodes ? Qt::Checked : Qt::Unchecked);
+
+	QVBoxLayout* progress_layout = new QVBoxLayout;
+	progress_layout->addWidget(progress_display_aired_episodes);
+	progress_layout->addWidget(progress_display_available_episodes);
+	progress_group_box->setLayout(progress_layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(actions_group_box);
+	full_layout->addWidget(appearance_group_box);
+	full_layout->addWidget(progress_group_box);
+	full_layout->setSpacing(10);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+void SettingsPageApplication::SaveInfo() {
+	session.config.anime_list.language = language;
+	session.config.anime_list.highlighted_anime_above_others = highlighted_anime_above_others;
+	session.config.anime_list.highlight_anime_if_available = highlight_anime_if_available;
+	session.config.anime_list.display_aired_episodes = display_aired_episodes;
+	session.config.anime_list.display_available_episodes = display_available_episodes;
+}
+
+SettingsPageApplication::SettingsPageApplication(QWidget* parent) : SettingsPage(parent, tr("Application")) {
+	language = session.config.anime_list.language;
+	highlighted_anime_above_others = session.config.anime_list.highlighted_anime_above_others;
+	highlight_anime_if_available = session.config.anime_list.highlight_anime_if_available;
+	display_aired_episodes = session.config.anime_list.display_aired_episodes;
+	display_available_episodes = session.config.anime_list.display_available_episodes;
+	AddTab(CreateAnimeListWidget(), tr("Anime list"));
+}
--- a/src/gui/dialog/settings/services.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/dialog/settings/services.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,94 +1,94 @@
-#include "core/anime.h"
-#include "core/session.h"
-#include "gui/dialog/settings.h"
-#include "services/anilist.h"
-#include <QComboBox>
-#include <QGroupBox>
-#include <QPushButton>
-#include <QSizePolicy>
-
-QWidget* SettingsPageServices::CreateMainPage() {
-	QWidget* result = new QWidget(this);
-	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QGroupBox* sync_group_box = new QGroupBox(tr("Synchronization"), result);
-	sync_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QLabel* sync_combo_box_label = new QLabel(tr("Active service and metadata provider:"), sync_group_box);
-
-	QComboBox* sync_combo_box = new QComboBox(sync_group_box);
-	sync_combo_box->addItem(tr("AniList"));
-	connect(sync_combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
-			[this](int index) { service = static_cast<Anime::Services>(index + 1); });
-	sync_combo_box->setCurrentIndex(static_cast<int>(service) - 1);
-
-	QLabel* sync_note_label =
-		new QLabel(tr("Note: Weeaboo is unable to synchronize multiple services at the same time."), sync_group_box);
-
-	QVBoxLayout* sync_layout = new QVBoxLayout;
-	sync_layout->addWidget(sync_combo_box_label);
-	sync_layout->addWidget(sync_combo_box);
-	sync_layout->addWidget(sync_note_label);
-	sync_group_box->setLayout(sync_layout);
-
-	QVBoxLayout* full_layout = new QVBoxLayout;
-	full_layout->addWidget(sync_group_box);
-	full_layout->setSpacing(10);
-	full_layout->addStretch();
-	result->setLayout(full_layout);
-	return result;
-}
-
-QWidget* SettingsPageServices::CreateAniListPage() {
-	QWidget* result = new QWidget(this);
-	result->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
-	QGroupBox* group_box = new QGroupBox(tr("Account"), result);
-	group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-
-	QLabel* username_entry_label = new QLabel(tr("Username: (not your email address)"), group_box);
-
-	QWidget* auth_widget = new QWidget(group_box);
-	QLineEdit* username_entry = new QLineEdit(username, auth_widget);
-	connect(username_entry, &QLineEdit::editingFinished, this,
-			[this, username_entry] { username = username_entry->text(); });
-
-	QPushButton* auth_button = new QPushButton(auth_widget);
-	connect(auth_button, &QPushButton::clicked, this, [] { AniList::Authorize(); });
-	auth_button->setText(session.config.anilist.auth_token.empty() ? tr("Authorize...") : tr("Re-authorize..."));
-
-	QHBoxLayout* auth_layout = new QHBoxLayout;
-	auth_layout->addWidget(username_entry);
-	auth_layout->addWidget(auth_button);
-	auth_widget->setLayout(auth_layout);
-
-	QLabel* note_label = new QLabel(tr("<a href=\"http://anilist.co/\">Create a new AniList account</a>"), group_box);
-	note_label->setTextFormat(Qt::RichText);
-	note_label->setTextInteractionFlags(Qt::TextBrowserInteraction);
-	note_label->setOpenExternalLinks(true);
-
-	QVBoxLayout* layout = new QVBoxLayout;
-	layout->addWidget(username_entry_label);
-	layout->addWidget(auth_widget);
-	layout->addWidget(note_label);
-	group_box->setLayout(layout);
-
-	QVBoxLayout* full_layout = new QVBoxLayout;
-	full_layout->addWidget(group_box);
-	full_layout->setSpacing(10);
-	full_layout->addStretch();
-	result->setLayout(full_layout);
-	return result;
-}
-
-void SettingsPageServices::SaveInfo() {
-	session.config.anilist.username = username.toStdString();
-	session.config.service = service;
-}
-
-SettingsPageServices::SettingsPageServices(QWidget* parent) : SettingsPage(parent, tr("Services")) {
-	username = QString::fromUtf8(session.config.anilist.username.c_str());
-	service = session.config.service;
-	AddTab(CreateMainPage(), tr("Main"));
-	AddTab(CreateAniListPage(), tr("AniList"));
-}
+#include "core/anime.h"
+#include "core/session.h"
+#include "gui/dialog/settings.h"
+#include "services/anilist.h"
+#include <QComboBox>
+#include <QGroupBox>
+#include <QPushButton>
+#include <QSizePolicy>
+
+QWidget* SettingsPageServices::CreateMainPage() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QGroupBox* sync_group_box = new QGroupBox(tr("Synchronization"), result);
+	sync_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* sync_combo_box_label = new QLabel(tr("Active service and metadata provider:"), sync_group_box);
+
+	QComboBox* sync_combo_box = new QComboBox(sync_group_box);
+	sync_combo_box->addItem(tr("AniList"));
+	connect(sync_combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+			[this](int index) { service = static_cast<Anime::Services>(index + 1); });
+	sync_combo_box->setCurrentIndex(static_cast<int>(service) - 1);
+
+	QLabel* sync_note_label =
+		new QLabel(tr("Note: Weeaboo is unable to synchronize multiple services at the same time."), sync_group_box);
+
+	QVBoxLayout* sync_layout = new QVBoxLayout;
+	sync_layout->addWidget(sync_combo_box_label);
+	sync_layout->addWidget(sync_combo_box);
+	sync_layout->addWidget(sync_note_label);
+	sync_group_box->setLayout(sync_layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(sync_group_box);
+	full_layout->setSpacing(10);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+QWidget* SettingsPageServices::CreateAniListPage() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+	QGroupBox* group_box = new QGroupBox(tr("Account"), result);
+	group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* username_entry_label = new QLabel(tr("Username: (not your email address)"), group_box);
+
+	QWidget* auth_widget = new QWidget(group_box);
+	QLineEdit* username_entry = new QLineEdit(username, auth_widget);
+	connect(username_entry, &QLineEdit::editingFinished, this,
+			[this, username_entry] { username = username_entry->text(); });
+
+	QPushButton* auth_button = new QPushButton(auth_widget);
+	connect(auth_button, &QPushButton::clicked, this, [] { Services::AniList::AuthorizeUser(); });
+	auth_button->setText(session.config.anilist.auth_token.empty() ? tr("Authorize...") : tr("Re-authorize..."));
+
+	QHBoxLayout* auth_layout = new QHBoxLayout;
+	auth_layout->addWidget(username_entry);
+	auth_layout->addWidget(auth_button);
+	auth_widget->setLayout(auth_layout);
+
+	QLabel* note_label = new QLabel(tr("<a href=\"http://anilist.co/\">Create a new AniList account</a>"), group_box);
+	note_label->setTextFormat(Qt::RichText);
+	note_label->setTextInteractionFlags(Qt::TextBrowserInteraction);
+	note_label->setOpenExternalLinks(true);
+
+	QVBoxLayout* layout = new QVBoxLayout;
+	layout->addWidget(username_entry_label);
+	layout->addWidget(auth_widget);
+	layout->addWidget(note_label);
+	group_box->setLayout(layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(group_box);
+	full_layout->setSpacing(10);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+void SettingsPageServices::SaveInfo() {
+	session.config.anilist.username = username.toStdString();
+	session.config.service = service;
+}
+
+SettingsPageServices::SettingsPageServices(QWidget* parent) : SettingsPage(parent, tr("Services")) {
+	username = QString::fromUtf8(session.config.anilist.username.c_str());
+	service = session.config.service;
+	AddTab(CreateMainPage(), tr("Main"));
+	AddTab(CreateAniListPage(), tr("AniList"));
+}
--- a/src/gui/pages/anime_list.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/pages/anime_list.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,468 +1,479 @@
-/**
- * anime_list.cpp: defines the anime list page
- * and widgets.
- *
- * much of this file is based around
- * Qt's original QTabWidget implementation, because
- * I needed a somewhat native way to create a tabbed
- * widget with only one subwidget that worked exactly
- * like a native tabbed widget.
- **/
-#include "gui/pages/anime_list.h"
-#include "core/anime.h"
-#include "core/anime_db.h"
-#include "core/session.h"
-#include "core/time.h"
-#include "gui/dialog/information.h"
-#include "gui/translate/anime.h"
-#include "services/anilist.h"
-#include <QHBoxLayout>
-#include <QHeaderView>
-#include <QMenu>
-#include <QProgressBar>
-#include <QShortcut>
-#include <QStylePainter>
-#include <QStyledItemDelegate>
-#include <cmath>
-
-#if 0
-AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent) : QStyledItemDelegate(parent) {
-}
-
-QWidget* AnimeListWidgetDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
-	// no edit 4 u
-	return nullptr;
-}
-
-void AnimeListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
-									const QModelIndex& index) const {
-	switch (index.column()) {
-/*
-		case AnimeListWidgetModel::AL_PROGRESS: {
-			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
-			const int episodes =
-				static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
-
-			int text_width = 59;
-			QRectF text_rect(option.rect.x() + text_width, option.rect.y(), text_width, option.decorationSize.height());
-			painter->save();
-			painter->drawText(text_rect, "/", QTextOption(Qt::AlignCenter | Qt::AlignVCenter));
-			// drawText(const QRectF &rectangle, const QString &text, const QTextOption &option = QTextOption())
-			painter->drawText(QRectF(text_rect.x(), text_rect.y(), text_width / 2 - 2, text_rect.height()),
-							  QString::number(progress), QTextOption(Qt::AlignRight | Qt::AlignVCenter));
-			painter->drawText(
-				QRectF(text_rect.x() + text_width / 2 + 2, text_rect.y(), text_width / 2 - 2, text_rect.height()),
-				QString::number(episodes), QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
-			painter->restore();
-			QStyledItemDelegate::paint(painter, option, index);
-			break;
-		}
-*/
-		default: QStyledItemDelegate::paint(painter, option, index); break;
-	}
-}
-
-AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
-}
-
-bool AnimeListWidgetSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
-	QVariant left = sourceModel()->data(l, sortRole());
-	QVariant right = sourceModel()->data(r, sortRole());
-
-	switch (left.userType()) {
-		case QMetaType::Int:
-		case QMetaType::UInt:
-		case QMetaType::LongLong:
-		case QMetaType::ULongLong: return left.toInt() < right.toInt();
-		case QMetaType::QDate: return left.toDate() < right.toDate();
-		case QMetaType::QString:
-		default: return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
-	}
-}
-
-AnimeListWidgetModel::AnimeListWidgetModel(QWidget* parent) : QAbstractListModel(parent) {
-	return;
-}
-
-int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
-	int count = 0;
-	for (const auto& [id, anime] : Anime::db.items) {
-		if (anime.IsInUserList())
-			count++;
-	}
-	return count;
-	(void)(parent);
-}
-
-int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
-	return NB_COLUMNS;
-	(void)(parent);
-}
-
-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_EPISODES: return tr("Episodes");
-			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_EPISODES:
-			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);
-}
-
-QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
-	if (!index.isValid())
-		return QVariant();
-	switch (role) {
-		case Qt::DisplayRole:
-			switch (index.column()) {
-				case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
-				case AL_PROGRESS:
-					return QString::number(list[index.row()].progress) + "/" + QString::number(list[index.row()].episodes);
-				case AL_EPISODES: return list[index.row()].episodes;
-				case AL_SCORE: return list[index.row()].score;
-				case AL_TYPE: return QString::fromStdString(Translate::TranslateSeriesFormat(list[index.row()].type));
-				case AL_SEASON:
-					return QString::fromStdString(Translate::TranslateSeriesSeason(list[index.row()].season)) + " " +
-						   QString::number(list[index.row()].air_date.GetYear());
-				case AL_AVG_SCORE: return QString::number(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::fromUtf8(duration.AsRelativeString().c_str());
-				}
-				case AL_NOTES: return QString::fromUtf8(list[index.row()].notes.c_str());
-				default: return "";
-			}
-			break;
-		case Qt::UserRole:
-			switch (index.column()) {
-				case AL_ID: return 
-				case AL_PROGRESS: return list[index.row()].progress;
-				case AL_TYPE: return list[index.row()].type;
-				case AL_SEASON: return list[index.row()].air_date.GetAsQDate();
-				case AL_AVG_SCORE: return list[index.row()].audience_score;
-				case AL_UPDATED: return list[index.row()].updated;
-				default: return data(index, Qt::DisplayRole);
-			}
-			break;
-		case Qt::TextAlignmentRole:
-			switch (index.column()) {
-				case AL_TITLE:
-				case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
-				case AL_PROGRESS:
-				case AL_EPISODES:
-				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;
-			}
-			break;
-	}
-	return QVariant();
-}
-
-void AnimeListWidgetModel::UpdateAnime(int id) {
-	/* meh... it might be better to just redraw the entire list */
-	int i = 0;
-	for (const auto& [a_id, anime] : Anime:db.items) {
-		if (anime.IsInUserList() && a_id == id && anime.GetUserStatus() == Anime::ListStatus::WATCHING) {
-			emit dataChanged(index(i), index(i));
-		}
-		i++;
-	}
-}
-#endif
-
-int AnimeListWidget::VisibleColumnsCount() const {
-	int count = 0;
-
-	for (int i = 0, end = tree_view->header()->count(); i < end; i++) {
-		if (!tree_view->isColumnHidden(i))
-			count++;
-	}
-
-	return count;
-}
-
-void AnimeListWidget::SetColumnDefaults() {
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
-	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
-	tree_view->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 =
-			sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
-		QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
-			if (!checked && (VisibleColumnsCount() <= 1))
-				return;
-
-			tree_view->setColumnHidden(i, !checked);
-
-			if (checked && (tree_view->columnWidth(i) <= 5))
-				tree_view->resizeColumnToContents(i);
-
-			// SaveSettings();
-		});
-		action->setCheckable(true);
-		action->setChecked(!tree_view->isColumnHidden(i));
-	}
-
-	menu->addSeparator();
-	QAction* resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
-		for (int i = 0, count = tree_view->header()->count(); i < count; ++i) {
-			SetColumnDefaults();
-		}
-		// SaveSettings();
-	});
-	menu->popup(QCursor::pos());
-	(void)(resetAction);
-}
-
-void AnimeListWidget::DisplayListMenu() {
-	QMenu* menu = new QMenu(this);
-	menu->setAttribute(Qt::WA_DeleteOnClose);
-	menu->setTitle(tr("Column visibility"));
-	menu->setToolTipsVisible(true);
-
-	const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
-	if (!selection.indexes().first().isValid()) {
-		return;
-	}
-
-/*
-	QAction* action = menu->addAction("Information", [this, selection] {
-		const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
-									  ->index(selection.indexes().first().row());
-		Anime::Anime* anime =
-			((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
-		if (!anime) {
-			return;
-		}
-
-		InformationDialog* dialog = new InformationDialog(
-			*anime,
-			[this, anime] {
-				((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(*anime);
-			},
-			this);
-
-		dialog->show();
-		dialog->raise();
-		dialog->activateWindow();
-	});
-*/
-	menu->popup(QCursor::pos());
-}
-
-void AnimeListWidget::ItemDoubleClicked() {
-	/* throw out any other garbage */
-	const QItemSelection selection =
-		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
-	if (!selection.indexes().first().isValid()) {
-		return;
-	}
-
-/*
-	const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
-								  ->index(selection.indexes().first().row());
-	Anime::Anime* anime =
-		((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
-	if (!anime) {
-		return;
-	}
-
-	InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
-		((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(*anime);
-	}, this);
-
-	dialog->show();
-	dialog->raise();
-	dialog->activateWindow();
-*/
-}
-
-void AnimeListWidget::paintEvent(QPaintEvent*) {
-	QStylePainter p(this);
-
-	QStyleOptionTabWidgetFrame opt;
-	InitStyle(&opt);
-	opt.rect = panelRect;
-	p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
-}
-
-void AnimeListWidget::resizeEvent(QResizeEvent* e) {
-	QWidget::resizeEvent(e);
-	SetupLayout();
-}
-
-void AnimeListWidget::showEvent(QShowEvent*) {
-	SetupLayout();
-}
-
-void AnimeListWidget::InitBasicStyle(QStyleOptionTabWidgetFrame* option) const {
-	if (!option)
-		return;
-
-	option->initFrom(this);
-	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
-	option->shape = QTabBar::RoundedNorth;
-	option->tabBarRect = tab_bar->geometry();
-}
-
-void AnimeListWidget::InitStyle(QStyleOptionTabWidgetFrame* option) const {
-	if (!option)
-		return;
-
-	InitBasicStyle(option);
-
-	// int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
-	QSize t(0, tree_view->frameWidth());
-	if (tab_bar->isVisibleTo(this)) {
-		t = tab_bar->sizeHint();
-		t.setWidth(width());
-	}
-	option->tabBarSize = t;
-
-	QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
-	selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
-	option->selectedTabRect = selected_tab_rect;
-
-	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
-}
-
-void AnimeListWidget::SetupLayout() {
-	QStyleOptionTabWidgetFrame option;
-	InitStyle(&option);
-
-	QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
-	tabRect.setLeft(tabRect.left() + 1);
-	panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
-	QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);
-
-	tab_bar->setGeometry(tabRect);
-	tree_view->parentWidget()->setGeometry(contentsRect);
-}
-
-AnimeListWidget::AnimeListWidget(QWidget* parent) : QWidget(parent) {
-	/* Tab bar */
-	tab_bar = new QTabBar(this);
-	tab_bar->setExpanding(false);
-	tab_bar->setDrawBase(false);
-	for (int i = 0; i < ARRAYSIZE(sort_models); i++) {
-		tab_bar->addTab(QString::fromStdString(Translate::TranslateListStatus(Anime::ListStatuses[i])));
-
-		/* Tree view... */
-		QWidget* tree_widget = new QWidget(this);
-		tree_view = new QTreeView(tree_widget);
-		tree_view->setItemDelegate(new AnimeListWidgetDelegate(tree_view));
-		tree_view->setUniformRowHeights(true);
-		tree_view->setAllColumnsShowFocus(false);
-		tree_view->setAlternatingRowColors(true);
-		tree_view->setSortingEnabled(true);
-		tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
-		tree_view->setItemsExpandable(false);
-		tree_view->setRootIsDecorated(false);
-		tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
-		tree_view->setFrameShape(QFrame::NoFrame);
-
-		QHBoxLayout* layout = new QHBoxLayout;
-		layout->addWidget(tree_view);
-		layout->setMargin(0);
-		tree_widget->setLayout(layout);
-
-		/* Double click stuff */
-		connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
-		connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
-
-		/* Enter & return keys */
-		connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated,
-				this, &AnimeListWidget::ItemDoubleClicked);
-
-		connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated,
-				this, &AnimeListWidget::ItemDoubleClicked);
-
-		tree_view->header()->setStretchLastSection(false);
-		tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
-		connect(tree_view->header(), &QWidget::customContextMenuRequested, this,
-				&AnimeListWidget::DisplayColumnHeaderMenu);
-
-		connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) {
-			if (sort_models[index])
-				tree_view->setModel(sort_models[index]);
-		});
-
-		setFocusPolicy(Qt::TabFocus);
-		setFocusProxy(tab_bar);
-	}
-
-	void AnimeListWidget::UpdateAnimeList() {
-		for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) {
-			sort_models[i] = new AnimeListWidgetSortFilter(tree_view);
-			sort_models[i]->setSourceModel(new AnimeListWidgetModel(this, &anime_lists[i]));
-			sort_models[i]->setSortRole(Qt::UserRole);
-			sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive);
-		}
-		if (ARRAYSIZE(sort_models) > 0)
-			tree_view->setModel(sort_models[0]);
-		SetColumnDefaults();
-		SetupLayout();
-	}
-
-	void AnimeListWidget::Reset() {
-		while (tab_bar->count())
-			tab_bar->removeTab(0);
-		for (int i = 0; i < ARRAYSIZE(sort_models); i++)
-			delete sort_models[i];
-	}
-
-#include "gui/pages/moc_anime_list.cpp"
+/**
+ * anime_list.cpp: defines the anime list page
+ * and widgets.
+ *
+ * much of this file is based around
+ * Qt's original QTabWidget implementation, because
+ * I needed a somewhat native way to create a tabbed
+ * widget with only one subwidget that worked exactly
+ * like a native tabbed widget.
+ **/
+#include "gui/pages/anime_list.h"
+#include "core/anime.h"
+#include "core/anime_db.h"
+#include "core/session.h"
+#include "core/time.h"
+#include "gui/dialog/information.h"
+#include "gui/translate/anime.h"
+#include "services/anilist.h"
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QMenu>
+#include <QProgressBar>
+#include <QDebug>
+#include <QShortcut>
+#include <QStylePainter>
+#include <QStyledItemDelegate>
+#include <QAbstractItemModelTester>
+#include <cmath>
+
+AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent) : QStyledItemDelegate(parent) {
+}
+
+QWidget* AnimeListWidgetDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
+	// no edit 4 u
+	return nullptr;
+}
+
+void AnimeListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+									const QModelIndex& index) const {
+	switch (index.column()) {
+/*
+		case AnimeListWidgetModel::AL_PROGRESS: {
+			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
+			const int episodes =
+				static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
+
+			int text_width = 59;
+			QRectF text_rect(option.rect.x() + text_width, option.rect.y(), text_width, option.decorationSize.height());
+			painter->save();
+			painter->drawText(text_rect, "/", QTextOption(Qt::AlignCenter | Qt::AlignVCenter));
+			// drawText(const QRectF &rectangle, const QString &text, const QTextOption &option = QTextOption())
+			painter->drawText(QRectF(text_rect.x(), text_rect.y(), text_width / 2 - 2, text_rect.height()),
+							  QString::number(progress), QTextOption(Qt::AlignRight | Qt::AlignVCenter));
+			painter->drawText(
+				QRectF(text_rect.x() + text_width / 2 + 2, text_rect.y(), text_width / 2 - 2, text_rect.height()),
+				QString::number(episodes), QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
+			painter->restore();
+			QStyledItemDelegate::paint(painter, option, index);
+			break;
+		}
+*/
+		default: QStyledItemDelegate::paint(painter, option, index); break;
+	}
+}
+
+AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
+}
+
+bool AnimeListWidgetSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
+	QVariant left = sourceModel()->data(l, sortRole());
+	QVariant right = sourceModel()->data(r, sortRole());
+
+	switch (left.userType()) {
+		case QMetaType::Int:
+		case QMetaType::UInt:
+		case QMetaType::LongLong:
+		case QMetaType::ULongLong: return left.toInt() < right.toInt();
+		case QMetaType::QDate: return left.toDate() < right.toDate();
+		case QMetaType::QString:
+		default: return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
+	}
+}
+
+AnimeListWidgetModel::AnimeListWidgetModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) {
+	status = _status;
+	return;
+}
+
+int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
+	return list.size();
+	(void)(parent);
+}
+
+int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
+	return NB_COLUMNS;
+	(void)(parent);
+}
+
+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_EPISODES: return tr("Episodes");
+			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_EPISODES:
+			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);
+}
+
+QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
+	if (!index.isValid())
+		return QVariant();
+	switch (role) {
+		case Qt::DisplayRole:
+			switch (index.column()) {
+				case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
+				case AL_PROGRESS:
+					return QString::number(list[index.row()].GetUserProgress()) + "/" + QString::number(list[index.row()].GetEpisodes());
+				case AL_EPISODES: return list[index.row()].GetEpisodes();
+				case AL_SCORE: return list[index.row()].GetUserScore();
+				case AL_TYPE: return QString::fromStdString(Translate::TranslateSeriesFormat(list[index.row()].GetFormat()));
+				case AL_SEASON:
+					return QString::fromStdString(Translate::TranslateSeriesSeason(list[index.row()].GetSeason())) + " " +
+						   QString::number(list[index.row()].GetAirDate().GetYear());
+				case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
+				case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
+				case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate();
+				case AL_UPDATED: {
+					if (list[index.row()].GetUserTimeUpdated() == 0)
+						return QString("-");
+					Time::Duration duration(Time::GetSystemTime() - list[index.row()].GetUserTimeUpdated());
+					return QString::fromUtf8(duration.AsRelativeString().c_str());
+				}
+				case AL_NOTES: return QString::fromUtf8(list[index.row()].GetUserNotes().c_str());
+				default: return "";
+			}
+			break;
+		case Qt::UserRole:
+			switch (index.column()) {
+				case AL_ID: return list[index.row()].GetId();
+				case AL_PROGRESS: return list[index.row()].GetUserProgress();
+				case AL_TYPE: return static_cast<int>(list[index.row()].GetFormat());
+				case AL_SEASON: return list[index.row()].GetAirDate().GetAsQDate();
+				case AL_AVG_SCORE: return list[index.row()].GetAudienceScore();
+				case AL_UPDATED: return list[index.row()].GetUserTimeUpdated();
+				default: return data(index, Qt::DisplayRole);
+			}
+			break;
+		case Qt::TextAlignmentRole:
+			switch (index.column()) {
+				case AL_TITLE:
+				case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+				case AL_PROGRESS:
+				case AL_EPISODES:
+				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;
+			}
+			break;
+	}
+	return QVariant();
+}
+
+void AnimeListWidgetModel::UpdateAnime(int id) {
+	/* meh... it might be better to just reinit the entire list */
+	int i = 0;
+	for (const auto& [a_id, anime] : Anime::db.items) {
+		if (anime.IsInUserList() && a_id == id && anime.GetUserStatus() == status) {
+			emit dataChanged(index(i), index(i));
+		}
+		i++;
+	}
+}
+
+Anime::Anime* AnimeListWidgetModel::GetAnimeFromIndex(QModelIndex index) {
+	return &list.at(index.row());
+}
+
+void AnimeListWidgetModel::RefreshList() {
+	bool has_children = !!rowCount(index(0));
+	if (has_children) beginResetModel();
+	list.clear();
+
+	for (const auto& [id, anime] : Anime::db.items) {
+		if (anime.IsInUserList() && anime.GetUserStatus() == status) {
+			list.push_back(anime);
+		}
+	}
+	if (has_children) endResetModel();
+}
+
+int AnimeListWidget::VisibleColumnsCount() const {
+	int count = 0;
+
+	for (int i = 0, end = tree_view->header()->count(); i < end; i++) {
+		if (!tree_view->isColumnHidden(i))
+			count++;
+	}
+
+	return count;
+}
+
+void AnimeListWidget::SetColumnDefaults() {
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_ID, 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 || i == AnimeListWidgetModel::AL_ID)
+			continue;
+		const auto column_name =
+			sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
+		QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
+			if (!checked && (VisibleColumnsCount() <= 1))
+				return;
+
+			tree_view->setColumnHidden(i, !checked);
+
+			if (checked && (tree_view->columnWidth(i) <= 5))
+				tree_view->resizeColumnToContents(i);
+
+			// SaveSettings();
+		});
+		action->setCheckable(true);
+		action->setChecked(!tree_view->isColumnHidden(i));
+	}
+
+	menu->addSeparator();
+	QAction* resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
+		for (int i = 0, count = tree_view->header()->count(); i < count; ++i) {
+			SetColumnDefaults();
+		}
+		// SaveSettings();
+	});
+	menu->popup(QCursor::pos());
+	(void)(resetAction);
+}
+
+void AnimeListWidget::DisplayListMenu() {
+	QMenu* menu = new QMenu(this);
+	menu->setAttribute(Qt::WA_DeleteOnClose);
+	menu->setTitle(tr("Column visibility"));
+	menu->setToolTipsVisible(true);
+
+	const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+	if (!selection.indexes().first().isValid()) {
+		return;
+	}
+
+	QAction* action = menu->addAction("Information", [this, selection] {
+		const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
+									  ->index(selection.indexes().first().row());
+		Anime::Anime* anime =
+			((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
+		if (!anime) {
+			return;
+		}
+
+		InformationDialog* dialog = new InformationDialog(
+			*anime,
+			[this, anime] {
+				((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(anime->GetId());
+			},
+			this);
+
+		dialog->show();
+		dialog->raise();
+		dialog->activateWindow();
+	});
+	menu->popup(QCursor::pos());
+}
+
+void AnimeListWidget::ItemDoubleClicked() {
+	/* throw out any other garbage */
+	const QItemSelection selection =
+		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+	if (!selection.indexes().first().isValid()) {
+		return;
+	}
+
+	const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
+								  ->index(selection.indexes().first().row());
+	Anime::Anime* anime =
+		((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
+
+	InformationDialog* dialog = new InformationDialog(
+		*anime,
+		[this, anime] {
+			((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(anime->GetId());
+		},
+		this);
+
+	dialog->show();
+	dialog->raise();
+	dialog->activateWindow();
+}
+
+void AnimeListWidget::paintEvent(QPaintEvent*) {
+	QStylePainter p(this);
+
+	QStyleOptionTabWidgetFrame opt;
+	InitStyle(&opt);
+	opt.rect = panelRect;
+	p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
+}
+
+void AnimeListWidget::resizeEvent(QResizeEvent* e) {
+	QWidget::resizeEvent(e);
+	SetupLayout();
+}
+
+void AnimeListWidget::showEvent(QShowEvent*) {
+	SetupLayout();
+}
+
+void AnimeListWidget::InitBasicStyle(QStyleOptionTabWidgetFrame* option) const {
+	if (!option)
+		return;
+
+	option->initFrom(this);
+	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
+	option->shape = QTabBar::RoundedNorth;
+	option->tabBarRect = tab_bar->geometry();
+}
+
+void AnimeListWidget::InitStyle(QStyleOptionTabWidgetFrame* option) const {
+	if (!option)
+		return;
+
+	InitBasicStyle(option);
+
+	// int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
+	QSize t(0, tree_view->frameWidth());
+	if (tab_bar->isVisibleTo(this)) {
+		t = tab_bar->sizeHint();
+		t.setWidth(width());
+	}
+	option->tabBarSize = t;
+
+	QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
+	selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
+	option->selectedTabRect = selected_tab_rect;
+
+	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
+}
+
+void AnimeListWidget::SetupLayout() {
+	QStyleOptionTabWidgetFrame option;
+	InitStyle(&option);
+
+	QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
+	tabRect.setLeft(tabRect.left() + 1);
+	panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
+	QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);
+
+	tab_bar->setGeometry(tabRect);
+	tree_view->parentWidget()->setGeometry(contentsRect);
+}
+
+AnimeListWidget::AnimeListWidget(QWidget* parent) : QWidget(parent) {
+	/* Tab bar */
+	tab_bar = new QTabBar(this);
+	tab_bar->setExpanding(false);
+	tab_bar->setDrawBase(false);
+
+	/* Tree view... */
+	QWidget* tree_widget = new QWidget(this);
+	tree_view = new QTreeView(tree_widget);
+	tree_view->setItemDelegate(new AnimeListWidgetDelegate(tree_view));
+	tree_view->setUniformRowHeights(true);
+	tree_view->setAllColumnsShowFocus(false);
+	tree_view->setAlternatingRowColors(true);
+	tree_view->setSortingEnabled(true);
+	tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
+	tree_view->setItemsExpandable(false);
+	tree_view->setRootIsDecorated(false);
+	tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
+	tree_view->setFrameShape(QFrame::NoFrame);
+
+	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) {
+		tab_bar->addTab(QString::fromStdString(Translate::TranslateListStatus(Anime::ListStatuses[i])) + " (" + QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
+		sort_models[i] = new AnimeListWidgetSortFilter(tree_view);
+		AnimeListWidgetModel* model = new AnimeListWidgetModel(this, Anime::ListStatuses[i]);
+		new QAbstractItemModelTester(model, QAbstractItemModelTester::FailureReportingMode::Fatal, this);
+		sort_models[i]->setSourceModel(model);
+		sort_models[i]->setSortRole(Qt::UserRole);
+		sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive);
+	}
+
+	QHBoxLayout* layout = new QHBoxLayout;
+	layout->addWidget(tree_view);
+	layout->setMargin(0);
+	tree_widget->setLayout(layout);
+
+	/* Double click stuff */
+	connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
+	connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
+
+	/* Enter & return keys */
+	connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated,
+			this, &AnimeListWidget::ItemDoubleClicked);
+
+	connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated,
+			this, &AnimeListWidget::ItemDoubleClicked);
+
+	tree_view->header()->setStretchLastSection(false);
+	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
+	connect(tree_view->header(), &QWidget::customContextMenuRequested, this,
+			&AnimeListWidget::DisplayColumnHeaderMenu);
+
+	connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) {
+		if (sort_models[index])
+			tree_view->setModel(sort_models[index]);
+	});
+
+	setFocusPolicy(Qt::TabFocus);
+	setFocusProxy(tab_bar);
+}
+
+void AnimeListWidget::RefreshList() {
+	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) {
+		((AnimeListWidgetModel*)sort_models[i]->sourceModel())->RefreshList();
+	}
+}
+
+void AnimeListWidget::Reset() {
+	while (tab_bar->count())
+		tab_bar->removeTab(0);
+	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+		delete sort_models[i];
+}
+
+#include "gui/pages/moc_anime_list.cpp"
--- a/src/gui/pages/statistics.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/pages/statistics.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,7 +1,8 @@
 #include "gui/pages/statistics.h"
 #include "gui/pages/anime_list.h"
 #include "gui/ui_utils.h"
-#include "session.h"
+#include "core/session.h"
+#include "core/anime_db.h"
 #include <QString>
 #include <QTextDocument>
 #include <QTextStream>
@@ -48,9 +49,9 @@
 }
 
 /* me abusing macros :) */
-#define ADD_TIME_SEGMENT(r, x, s, p)                                                                                   \
+#define ADD_TIME_SEGMENT(r, x, s, p) {                                                                             \
 	if (x > 0)                                                                                                         \
-	r << x << ((x == 1) ? s : p)
+	r << x << ((x == 1) ? s : p); }
 std::string StatisticsWidget::MinutesToDateString(int minutes) {
 	/* ew */
 	int years = (minutes * (1 / 525949.2F));
@@ -68,25 +69,22 @@
 	return return_stream.str();
 }
 
-std::string StatisticsWidget::SecondsToDateString(int seconds) {
+std::string StatisticsWidget::SecondsToDateString(int sec) {
 	/* this is all fairly unnecessary, but works:tm: */
-	std::chrono::duration<int, std::ratio<1>> int_total_mins(seconds);
-	auto int_years = std::chrono::duration_cast<std::chrono::years>(int_total_mins);
-	auto int_months = std::chrono::duration_cast<std::chrono::months>(int_total_mins - int_years);
-	auto int_days = std::chrono::duration_cast<std::chrono::days>(int_total_mins - int_years - int_months);
-	auto int_hours = std::chrono::duration_cast<std::chrono::hours>(int_total_mins - int_years - int_months - int_days);
-	auto int_minutes = std::chrono::duration_cast<std::chrono::minutes>(int_total_mins - int_years - int_months -
-																		int_days - int_hours);
-	auto int_seconds = std::chrono::duration_cast<std::chrono::seconds>(int_total_mins - int_years - int_months -
-																		int_days - int_hours - int_minutes);
+	int years = sec * (1 / 31556952.0F);
+	int months = sec * (1 / 2629746.0F) - (years * 12);
+	int days = sec * (1 / 86400.0F) - (years * 365.2425F) - (months * 30.436875F);
+	int hours = sec * (1 / 3600.0F) - (years * 8765.82F) - (months * 730.485F) - (days * 24);
+	int minutes = (sec) * (1 / 60.0F) - (years * 525949.2F) - (months * 43829.1F) - (days * 1440.0F) - (hours * 60.0F);
+	int seconds = sec - (years * 31556952.0F) - (months * 2629746.0F) - (days * 86400.0F) - (hours * 3600.0F) - (minutes * 60.0F);
 	std::ostringstream return_stream;
-	ADD_TIME_SEGMENT(return_stream, int_years, " year ", " years ");
-	ADD_TIME_SEGMENT(return_stream, int_months, " month ", " months ");
-	ADD_TIME_SEGMENT(return_stream, int_days, " day ", " days ");
-	ADD_TIME_SEGMENT(return_stream, int_hours, " hour ", " hours ");
-	ADD_TIME_SEGMENT(return_stream, int_minutes, " minute ", " minutes ");
-	if (int_seconds.count() > 0 || return_stream.str().size() == 0)
-		return_stream << int_seconds.count() << ((int_seconds.count() == 1) ? " second" : " seconds");
+	ADD_TIME_SEGMENT(return_stream, years, " year ", " years ");
+	ADD_TIME_SEGMENT(return_stream, months, " month ", " months ");
+	ADD_TIME_SEGMENT(return_stream, days, " day ", " days ");
+	ADD_TIME_SEGMENT(return_stream, hours, " hour ", " hours ");
+	ADD_TIME_SEGMENT(return_stream, minutes, " minute ", " minutes ");
+	if (seconds > 0 || return_stream.str().size() == 0)
+		return_stream << seconds << ((seconds == 1) ? " second" : " seconds");
 	return return_stream.str();
 }
 #undef ADD_TIME_SEGMENT
@@ -95,12 +93,12 @@
 	/* Anime list */
 	QString string = "";
 	QTextStream ts(&string);
-	ts << Anime::db->GetTotalAnimeAmount() << '\n';
-	ts << Anime::db->GetTotalEpisodeAmount() << '\n';
-	ts << MinutesToDateString(Anime::db->GetTotalWatchedAmount()).c_str() << '\n';
-	ts << MinutesToDateString(Anime::db->GetTotalPlannedAmount()).c_str() << '\n';
-	ts << Anime::db->GetAverageScore() << '\n';
-	ts << Anime::db->GetScoreDeviation();
+	ts << Anime::db.GetTotalAnimeAmount() << '\n';
+	ts << Anime::db.GetTotalEpisodeAmount() << '\n';
+	ts << MinutesToDateString(Anime::db.GetTotalWatchedAmount()).c_str() << '\n';
+	ts << MinutesToDateString(Anime::db.GetTotalPlannedAmount()).c_str() << '\n';
+	ts << Anime::db.GetAverageScore() << '\n';
+	ts << Anime::db.GetScoreDeviation();
 	UiUtils::SetPlainTextEditData(anime_list_data, string);
 
 	/* Application */
@@ -108,4 +106,4 @@
 	UiUtils::SetPlainTextEditData(application_data, QString(SecondsToDateString(session.uptime() / 1000).c_str()));
 }
 
-#include "gui/pages/moc_statistics.h"
+#include "gui/pages/moc_statistics.cpp"
--- a/src/gui/translate/anime.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/translate/anime.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,54 +1,54 @@
-#include "core/anime.h"
-
-namespace Translate {
-
-std::string TranslateListStatus(const Anime::ListStatus status) {
-	switch (status) {
-		case Anime::ListStatus::NOT_IN_LIST: return "Not in list";
-		case Anime::ListStatus::CURRENT: return "Currently watching";
-		case Anime::ListStatus::PLANNING: return "Plan to watch";
-		case Anime::ListStatus::COMPLETED: return "Completed";
-		case Anime::ListStatus::DROPPED: return "Dropped";
-		case Anime::ListStatus::PAUSED: return "On hold";
-		default: return "";
-	}
-}
-
-std::string TranslateSeriesFormat(const Anime::SeriesFormat format) {
-	switch (format) {
-		case Anime::SeriesFormat::UNKNOWN: return "Unknown";
-		case Anime::SeriesFormat::TV: return "TV";
-		case Anime::SeriesFormat::TV_SHORT: return "TV short";
-		case Anime::SeriesFormat::OVA: return "OVA";
-		case Anime::SeriesFormat::MOVIE: return "Movie";
-		case Anime::SeriesFormat::SPECIAL: return "Special";
-		case Anime::SeriesFormat::ONA: return "ONA";
-		case Anime::SeriesFormat::MUSIC: return "Music";
-		default: return "";
-	}
-}
-
-std::string TranslateSeriesSeason(const Anime::SeriesSeason season) {
-	switch (season) {
-		case Anime::SeriesSeason::UNKNOWN: return "Unknown";
-		case Anime::SeriesSeason::WINTER: return "Winter";
-		case Anime::SeriesSeason::SUMMER: return "Summer";
-		case Anime::SeriesSeason::FALL: return "Fall";
-		case Anime::SeriesSeason::SPRING: return "Spring";
-		default: return "";
-	}
-}
-
-std::string TranslateSeriesStatus(const Anime::SeriesStatus status) {
-	switch (status) {
-		case Anime::SeriesStatus::UNKNOWN: return "Unknown";
-		case Anime::SeriesStatus::RELEASING: return "Currently airing";
-		case Anime::SeriesStatus::FINISHED: return "Finished airing";
-		case Anime::SeriesStatus::NOT_YET_RELEASED: return "Not yet aired";
-		case Anime::SeriesStatus::CANCELLED: return "Cancelled";
-		case Anime::SeriesStatus::HIATUS: return "On hiatus";
-		default: return "";
-	}
-}
-
+#include "core/anime.h"
+
+namespace Translate {
+
+std::string TranslateListStatus(const Anime::ListStatus status) {
+	switch (status) {
+		case Anime::ListStatus::NOT_IN_LIST: return "Not in list";
+		case Anime::ListStatus::CURRENT: return "Currently watching";
+		case Anime::ListStatus::PLANNING: return "Plan to watch";
+		case Anime::ListStatus::COMPLETED: return "Completed";
+		case Anime::ListStatus::DROPPED: return "Dropped";
+		case Anime::ListStatus::PAUSED: return "On hold";
+		default: return "";
+	}
+}
+
+std::string TranslateSeriesFormat(const Anime::SeriesFormat format) {
+	switch (format) {
+		case Anime::SeriesFormat::UNKNOWN: return "Unknown";
+		case Anime::SeriesFormat::TV: return "TV";
+		case Anime::SeriesFormat::TV_SHORT: return "TV short";
+		case Anime::SeriesFormat::OVA: return "OVA";
+		case Anime::SeriesFormat::MOVIE: return "Movie";
+		case Anime::SeriesFormat::SPECIAL: return "Special";
+		case Anime::SeriesFormat::ONA: return "ONA";
+		case Anime::SeriesFormat::MUSIC: return "Music";
+		default: return "";
+	}
+}
+
+std::string TranslateSeriesSeason(const Anime::SeriesSeason season) {
+	switch (season) {
+		case Anime::SeriesSeason::UNKNOWN: return "Unknown";
+		case Anime::SeriesSeason::WINTER: return "Winter";
+		case Anime::SeriesSeason::SUMMER: return "Summer";
+		case Anime::SeriesSeason::FALL: return "Fall";
+		case Anime::SeriesSeason::SPRING: return "Spring";
+		default: return "";
+	}
+}
+
+std::string TranslateSeriesStatus(const Anime::SeriesStatus status) {
+	switch (status) {
+		case Anime::SeriesStatus::UNKNOWN: return "Unknown";
+		case Anime::SeriesStatus::RELEASING: return "Currently airing";
+		case Anime::SeriesStatus::FINISHED: return "Finished airing";
+		case Anime::SeriesStatus::NOT_YET_RELEASED: return "Not yet aired";
+		case Anime::SeriesStatus::CANCELLED: return "Cancelled";
+		case Anime::SeriesStatus::HIATUS: return "On hiatus";
+		default: return "";
+	}
+}
+
 } // namespace Translate
\ No newline at end of file
--- a/src/gui/window.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/gui/window.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,6 +1,7 @@
 #include "gui/window.h"
 #include "core/config.h"
 #include "core/session.h"
+#include "services/services.h"
 #include "gui/dialog/settings.h"
 #include "gui/pages/anime_list.h"
 #include "gui/pages/now_playing.h"
@@ -24,17 +25,56 @@
    wxWidgets, but I thought the API was a little meh, so
    I switched to Qt. */
 
+enum class Pages {
+	NOW_PLAYING,
+
+	ANIME_LIST,
+	HISTORY,
+	STATISTICS,
+
+	SEARCH,
+	SEASONS,
+	TORRENTS
+};
+
 MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
 	main_widget = new QWidget(parent);
+
+	SideBar* sidebar = new SideBar(main_widget);
+	sidebar->AddItem("Now Playing", SideBar::CreateIcon(":/icons/16x16/film.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem("Anime List", SideBar::CreateIcon(":/icons/16x16/document-list.png"));
+	sidebar->AddItem("History", SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
+	sidebar->AddItem("Statistics", SideBar::CreateIcon(":/icons/16x16/chart.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem("Search", SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
+	sidebar->AddItem("Seasons", SideBar::CreateIcon(":/icons/16x16/calendar.png"));
+	sidebar->AddItem("Torrents", SideBar::CreateIcon(":/icons/16x16/feed.png"));
+	sidebar->setFixedWidth(128);
+	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
+
+	QStackedWidget* stack = new QStackedWidget(main_widget);
+	stack->addWidget(new NowPlayingWidget(main_widget));
+	stack->addWidget(new AnimeListWidget(main_widget));
+	stack->addWidget(new StatisticsWidget(main_widget));
+
+	connect(sidebar, &SideBar::CurrentItemChanged, stack, [stack](int index) {
+		switch (index) {
+			case 0:
+			case 1: stack->setCurrentIndex(index); break;
+			case 3: stack->setCurrentIndex(2); break;
+			default: break;
+		}
+	});
+	sidebar->setCurrentRow(2);
+
 	/* Menu Bar */
 	QAction* action;
 	QMenuBar* menubar = new QMenuBar(parent);
 	QMenu* menu = menubar->addMenu("&File");
 	QMenu* submenu = menu->addMenu("&Library folders");
-	action = new QAction("&Add new folder...");
-	submenu->addAction(action);
-	action = new QAction("&Scan available episodes");
-	menu->addAction(action);
+	action = submenu->addAction("&Add new folder...");
+	action = menu->addAction("&Scan available episodes");
 
 	menu->addSeparator();
 
@@ -44,7 +84,10 @@
 	action = menu->addAction("E&xit", qApp, &QApplication::quit);
 
 	menu = menubar->addMenu("&Services");
-	action = new QAction("Synchronize &list");
+	action = menu->addAction("Synchronize &list", [this, stack] {
+		Services::Synchronize();
+		((AnimeListWidget*)stack->widget((int)Pages::ANIME_LIST))->RefreshList();
+	});
 
 	menu->addSeparator();
 
@@ -85,34 +128,6 @@
 
 	setMenuBar(menubar);
 
-	SideBar* sidebar = new SideBar(main_widget);
-	sidebar->AddItem("Now Playing", SideBar::CreateIcon(":/icons/16x16/film.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem("Anime List", SideBar::CreateIcon(":/icons/16x16/document-list.png"));
-	sidebar->AddItem("History", SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
-	sidebar->AddItem("Statistics", SideBar::CreateIcon(":/icons/16x16/chart.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem("Search", SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
-	sidebar->AddItem("Seasons", SideBar::CreateIcon(":/icons/16x16/calendar.png"));
-	sidebar->AddItem("Torrents", SideBar::CreateIcon(":/icons/16x16/feed.png"));
-	sidebar->setFixedWidth(128);
-	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
-
-	QStackedWidget* stack = new QStackedWidget(main_widget);
-	stack->addWidget(new NowPlayingWidget(parent));
-	stack->addWidget(new AnimeListWidget(parent));
-	stack->addWidget(new StatisticsWidget(parent));
-
-	connect(sidebar, &SideBar::CurrentItemChanged, stack, [stack](int index) {
-		switch (index) {
-			case 0:
-			case 1: stack->setCurrentIndex(index); break;
-			case 3: stack->setCurrentIndex(2); break;
-			default: break;
-		}
-	});
-	sidebar->setCurrentRow(2);
-
 	QHBoxLayout* layout = new QHBoxLayout(main_widget);
 	layout->addWidget(sidebar, 0, Qt::AlignLeft | Qt::AlignTop);
 	layout->addWidget(stack);
--- a/src/main.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/main.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,19 +1,19 @@
-#include "core/session.h"
-#include "gui/window.h"
-#include <QApplication>
-
-Session session;
-
-int main(int argc, char** argv) {
-	QApplication app(argc, argv);
-
-	session.config.Load();
-
-	MainWindow window;
-
-	window.resize(941, 750);
-	window.setWindowTitle("Weeaboo");
-	window.show();
-
-	return app.exec();
+#include "core/session.h"
+#include "gui/window.h"
+#include <QApplication>
+
+Session session;
+
+int main(int argc, char** argv) {
+	QApplication app(argc, argv);
+
+	session.config.Load();
+
+	MainWindow window;
+
+	window.resize(941, 750);
+	window.setWindowTitle("Weeaboo");
+	window.show();
+
+	return app.exec();
 }
\ No newline at end of file
--- a/src/services/anilist.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/services/anilist.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,5 +1,6 @@
 #include "services/anilist.h"
 #include "core/anime.h"
+#include "core/anime_db.h"
 #include "core/config.h"
 #include "core/json.h"
 #include "core/session.h"
@@ -8,6 +9,7 @@
 #include <QInputDialog>
 #include <QLineEdit>
 #include <QMessageBox>
+#include <QUrl>
 #include <chrono>
 #include <curl/curl.h>
 #include <exception>
@@ -18,17 +20,17 @@
 
 class Account {
 	public:
-		std::string Username() const { return session.anilist.username; }
-		void SetUsername(std::string const& username) { session.anilist.username = username; }
+		std::string Username() const { return session.config.anilist.username; }
+		void SetUsername(std::string const& username) { session.config.anilist.username = username; }
 
-		int UserId() const { return session.anilist.user_id; }
-		void SetUserId(const int id) { session.anilist.user_id = id; }
+		int UserId() const { return session.config.anilist.user_id; }
+		void SetUserId(const int id) { session.config.anilist.user_id = id; }
 
-		std::string AuthToken() const { return session.anilist.auth_token; }
-		void SetAuthToken(std::string const& auth_token) { session.anilist.auth_token = auth_token; }
+		std::string AuthToken() const { return session.config.anilist.auth_token; }
+		void SetAuthToken(std::string const& auth_token) { session.config.anilist.auth_token = auth_token; }
 
 		bool Authenticated() const { return !AuthToken().empty(); }
-}
+};
 
 static Account account;
 
@@ -68,55 +70,55 @@
 	return "";
 }
 
-/* Maps to convert string forms to our internal enums */
+/* TODO: Move to Translate */
 
-std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = {
-	{"CURRENT",	CURRENT  },
-	  {"PLANNING",  PLANNING },
-	  {"COMPLETED", COMPLETED},
-	{"DROPPED",	DROPPED  },
-	  {"PAUSED",	 PAUSED   },
-	  {"REPEATING", REPEATING}
+std::map<std::string, Anime::ListStatus> AniListStringToAnimeWatchingMap = {
+	{"CURRENT",	Anime::ListStatus::CURRENT  },
+	  {"PLANNING",  Anime::ListStatus::PLANNING },
+	  {"COMPLETED", Anime::ListStatus::COMPLETED},
+	{"DROPPED",	Anime::ListStatus::DROPPED  },
+	  {"PAUSED",	 Anime::ListStatus::PAUSED   },
+	  {"REPEATING", Anime::ListStatus::CURRENT}
 };
 
-std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
-	{CURRENT,	  "CURRENT"  },
-	  {PLANNING,	 "PLANNING" },
-	  {COMPLETED, "COMPLETED"},
-	{DROPPED,	  "DROPPED"  },
-	  {PAUSED,	   "PAUSED"   },
-	  {REPEATING, "REPEATING"}
+std::map<Anime::ListStatus, std::string> AniListAnimeWatchingToStringMap = {
+	{Anime::ListStatus::CURRENT,	  "CURRENT"  },
+	  {Anime::ListStatus::PLANNING,	 "PLANNING" },
+	  {Anime::ListStatus::COMPLETED, "COMPLETED"},
+	{Anime::ListStatus::DROPPED,	  "DROPPED"  },
+	  {Anime::ListStatus::PAUSED,	   "PAUSED"   }
 };
 
-std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = {
-	{"FINISHED",		 FINISHED		 },
-	{"RELEASING",		  RELEASING	   },
-	{"NOT_YET_RELEASED", NOT_YET_RELEASED},
-	{"CANCELLED",		  CANCELLED	   },
-	{"HIATUS",		   HIATUS			 }
+std::map<std::string, Anime::SeriesStatus> AniListStringToAnimeAiringMap = {
+	{"FINISHED",		 Anime::SeriesStatus::FINISHED		 },
+	{"RELEASING",		  Anime::SeriesStatus::RELEASING	   },
+	{"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED},
+	{"CANCELLED",		  Anime::SeriesStatus::CANCELLED	   },
+	{"HIATUS",		   Anime::SeriesStatus::HIATUS			 }
 };
 
-std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = {
-	{"WINTER", WINTER},
-	{"SPRING", SPRING},
-	{"SUMMER", SUMMER},
-	{"FALL",	 FALL	 }
+std::map<std::string, Anime::SeriesSeason> AniListStringToAnimeSeasonMap = {
+	{"WINTER", Anime::SeriesSeason::WINTER},
+	{"SPRING", Anime::SeriesSeason::SPRING},
+	{"SUMMER", Anime::SeriesSeason::SUMMER},
+	{"FALL",	 Anime::SeriesSeason::FALL	 }
 };
 
-std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = {
-	{"TV",	   TV		 },
-	  {"TV_SHORT", TV_SHORT},
-	  {"MOVIE",	MOVIE	 },
-	{"SPECIAL",	SPECIAL },
-	  {"OVA",	  OVA	 },
-	{"ONA",		ONA	   },
-	  {"MUSIC",	MUSIC	 },
-	  {"MANGA",	MANGA	 },
-	{"NOVEL",	  NOVEL   },
-	  {"ONE_SHOT", ONE_SHOT}
+std::map<std::string, enum Anime::SeriesFormat> AniListStringToAnimeFormatMap = {
+	{"TV",	   Anime::SeriesFormat::TV		 },
+	  {"TV_SHORT", Anime::SeriesFormat::TV_SHORT},
+	  {"MOVIE",	Anime::SeriesFormat::MOVIE	 },
+	{"SPECIAL",	Anime::SeriesFormat::SPECIAL },
+	  {"OVA",	  Anime::SeriesFormat::OVA	 },
+	{"ONA",		Anime::SeriesFormat::ONA	   },
+	  {"MUSIC",	Anime::SeriesFormat::MUSIC	 },
+	  {"MANGA",	Anime::SeriesFormat::MANGA	 },
+	{"NOVEL",	  Anime::SeriesFormat::NOVEL   },
+	  {"ONE_SHOT", Anime::SeriesFormat::ONE_SHOT}
 };
 
-void ParseDate(const nlohmann::json& json, Date& date) {
+Date ParseDate(const nlohmann::json& json) {
+	Date date;
 	if (json.contains("/year"_json_pointer) && json["/year"_json_pointer].is_number())
 		date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
 	else
@@ -131,6 +133,7 @@
 		date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
 	else
 		date.VoidDay();
+	return date;
 }
 
 void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
@@ -151,43 +154,50 @@
 	anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
 	anime.SetFormat(AniListStringToAnimeFormatMap[JSON::GetString(json, "/format"_json_pointer)]);
 
-	anime.SetListStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]);
+	anime.SetAiringStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]);
 
-	ParseDate(json["/startDate"_json_pointer], anime.air_date);
+	anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
 
 	anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
 	anime.SetSeason(AniListStringToAnimeSeasonMap[JSON::GetString(json, "/season"_json_pointer)]);
 	anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer));
-	anime.SetSynopsis(StringUtils::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer)));
+	anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer)));
 
 	if (json.contains("/genres"_json_pointer) && json["/genres"_json_pointer].is_array())
 		anime.SetGenres(json["/genres"_json_pointer].get<std::vector<std::string>>());
 	if (json.contains("/synonyms"_json_pointer) && json["/synonyms"_json_pointer].is_array())
-		anime.SetSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
-	return 1;
+		anime.SetTitleSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
+	return id;
 }
 
-int ParseListItem(const nlohmann::json& json, Anime::Anime& anime) {
-	anime.SetScore(JSON::GetInt(entry.value(), "/score"_json_pointer));
-	anime.SetProgress(JSON::GetInt(entry.value(), "/progress"_json_pointer));
-	anime.SetStatus(AniListStringToAnimeWatchingMap[JSON::GetString(entry.value(), "/status"_json_pointer)]);
-	anime.SetNotes(JSON::GetString(entry.value(), "/notes"_json_pointer));
+int ParseListItem(const nlohmann::json& json) {
+	int id = ParseMediaJson(json["media"]);
+
+	Anime::Anime& anime = Anime::db.items[id];
+
+	anime.AddToUserList();
 
-	ParseDate(json["/startedAt"_json_pointer], anime.started);
-	ParseDate(json["/completedAt"_json_pointer], anime.completed);
+	anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer));
+	anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer));
+	anime.SetUserStatus(AniListStringToAnimeWatchingMap[JSON::GetString(json, "/status"_json_pointer)]);
+	anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer));
 
-	anime.SetUpdated(JSON::GetInt(entry.value(), "/updatedAt"_json_pointer));
+	anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
+	anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
 
-	return ParseMediaJson(json["media"], anime);
+	anime.SetUserTimeUpdated(JSON::GetInt(json, "/updatedAt"_json_pointer));
+
+	return id;
 }
 
 int ParseList(const nlohmann::json& json) {
 	for (const auto& entry : json["entries"].items()) {
 		ParseListItem(entry.value());
 	}
+	return 1;
 }
 
-int GetAnimeList(int id) {
+int GetAnimeList() {
 	/* NOTE: these should be in the qrc file */
 	const std::string query = "query ($id: Int) {\n"
 							  "  MediaListCollection (userId: $id, type: ANIME) {\n"
@@ -196,6 +206,7 @@
 							  "      entries {\n"
 							  "        score\n"
 							  "        notes\n"
+							  "        status\n"
 							  "        progress\n"
 							  "        startedAt {\n"
 							  "          year\n"
@@ -238,7 +249,7 @@
 	nlohmann::json json = {
 		{"query", query},
 		{"variables", {
-			{"id", id}
+			{"id", account.UserId()}
 		}}
 	};
 	// clang-format on
@@ -248,13 +259,12 @@
 	/* TODO: make sure that we actually need the wstring converter and see
 	   if we can just get wide strings back from nlohmann::json */
 	for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
-
-		ParseList(list.entry());
+		ParseList(list.value());
 	}
 	return 1;
 }
 
-int UpdateAnimeEntry(const Anime& anime) {
+int UpdateAnimeEntry(const Anime::Anime& anime) {
 	/**
 	 * possible values:
 	 * 
@@ -285,11 +295,11 @@
 	nlohmann::json json = {
 		{"query", query},
 		{"variables", {
-			{"media_id", anime.id},
-			{"progress", anime.progress},
-			{"status",   AnimeWatchingToStringMap[anime.status]},
-			{"score",    anime.score},
-			{"notes",    anime.notes}
+			{"media_id", anime.GetId()},
+			{"progress", anime.GetUserProgress()},
+			{"status",   AniListAnimeWatchingToStringMap[anime.GetUserStatus()]},
+			{"score",    anime.GetUserScore()},
+			{"notes",    anime.GetUserNotes()}
 		}}
 	};
 	// clang-format on
@@ -300,7 +310,7 @@
 int ParseUser(const nlohmann::json& json) {
 	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
 	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
-	account.SetAuthenticated(true);
+	return account.UserId();
 }
 
 int AuthorizeUser() {
@@ -314,7 +324,6 @@
 	if (ok && !token.isEmpty())
 		account.SetAuthToken(token.toStdString());
 	else { // fail
-		account.SetAuthenticated(false);
 		return 0;
 	}
 	const std::string query = "query {\n"
@@ -330,7 +339,7 @@
 		{"query", query}
 	};
 	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
-	ParseUser(json["Viewer"]) account.SetAuthenticated(true);
+	ParseUser(json["Viewer"]);
 	return 1;
 }
 
--- a/src/services/services.cpp	Sun Sep 10 03:59:16 2023 -0400
+++ b/src/services/services.cpp	Sat Sep 16 02:06:01 2023 -0400
@@ -1,19 +1,21 @@
-#include "session.h"
-
-namespace Services {
-
-void Synchronize() {
-	switch (session.config.service) {
-		case ANILIST: AniList::GetAnimeList(); break;
-		default: break;
-	}
-}
-
-void Authorize() {
-	switch (session.config.service) {
-		case ANILIST: AniList::AuthorizeUser(); break;
-		default: break;
-	}
-}
-
-} // namespace Services
\ No newline at end of file
+#include "core/session.h"
+#include "services/anilist.h"
+#include "services/services.h"
+
+namespace Services {
+
+void Synchronize() {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: AniList::GetAnimeList(); break;
+		default: break;
+	}
+}
+
+void Authorize() {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: AniList::AuthorizeUser(); break;
+		default: break;
+	}
+}
+
+}; // namespace Services
\ No newline at end of file