changeset 81:9b2b41f83a5e

boring: mass rename to cc because this is a very unix-y project, it makes more sense to use the 'cc' extension
author Paper <mrpapersonic@gmail.com>
date Mon, 23 Oct 2023 12:07:27 -0400
parents 825506f0e221
children 8b65c417c225
files CMakeLists.txt src/core/anime.cc src/core/anime.cpp src/core/anime_db.cc src/core/anime_db.cpp src/core/config.cc src/core/config.cpp src/core/date.cc src/core/date.cpp src/core/filesystem.cc src/core/filesystem.cpp src/core/http.cc src/core/http.cpp src/core/json.cc src/core/json.cpp src/core/strings.cc src/core/strings.cpp src/core/time.cc src/core/time.cpp src/gui/dark_theme.cc src/gui/dark_theme.cpp src/gui/dialog/about.cc src/gui/dialog/about.cpp src/gui/dialog/information.cc src/gui/dialog/information.cpp src/gui/dialog/settings.cc src/gui/dialog/settings.cpp src/gui/dialog/settings/application.cc src/gui/dialog/settings/application.cpp src/gui/dialog/settings/services.cc src/gui/dialog/settings/services.cpp src/gui/pages/anime_list.cc src/gui/pages/anime_list.cpp src/gui/pages/history.cc src/gui/pages/history.cpp src/gui/pages/now_playing.cc src/gui/pages/now_playing.cpp src/gui/pages/search.cc src/gui/pages/search.cpp src/gui/pages/seasons.cc src/gui/pages/seasons.cpp src/gui/pages/statistics.cc src/gui/pages/statistics.cpp src/gui/pages/torrents.cc src/gui/pages/torrents.cpp src/gui/translate/anilist.cc src/gui/translate/anilist.cpp src/gui/translate/anime.cc src/gui/translate/anime.cpp src/gui/widgets/anime_info.cc src/gui/widgets/anime_info.cpp src/gui/widgets/clickable_label.cc src/gui/widgets/clickable_label.cpp src/gui/widgets/optional_date.cc src/gui/widgets/optional_date.cpp src/gui/widgets/poster.cc src/gui/widgets/poster.cpp src/gui/widgets/sidebar.cc src/gui/widgets/sidebar.cpp src/gui/widgets/text.cc src/gui/widgets/text.cpp src/gui/window.cc src/gui/window.cpp src/main.cc src/main.cpp src/services/anilist.cc src/services/anilist.cpp src/services/services.cc src/services/services.cpp src/sys/win32/dark_theme.cc src/sys/win32/dark_theme.cpp src/track/media.cc src/track/media.cpp
diffstat 73 files changed, 4050 insertions(+), 4050 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Oct 13 13:15:19 2023 -0400
+++ b/CMakeLists.txt	Mon Oct 23 12:07:27 2023 -0400
@@ -48,58 +48,58 @@
 
 set(SRC_FILES
 	# Main entrypoint
-	src/main.cpp
+	src/main.cc
 
 	# Core files and datatype declarations...
-	src/core/anime.cpp
-	src/core/anime_db.cpp
-	src/core/config.cpp
-	src/core/date.cpp
-	src/core/filesystem.cpp
-	src/core/http.cpp
-	src/core/json.cpp
-	src/core/strings.cpp
-	src/core/time.cpp
+	src/core/anime.cc
+	src/core/anime_db.cc
+	src/core/config.cc
+	src/core/date.cc
+	src/core/filesystem.cc
+	src/core/http.cc
+	src/core/json.cc
+	src/core/strings.cc
+	src/core/time.cc
 
 	# Main window
-	src/gui/window.cpp
-	src/gui/dark_theme.cpp
+	src/gui/window.cc
+	src/gui/dark_theme.cc
 
 	# Main window pages
-	src/gui/pages/anime_list.cpp
-	src/gui/pages/now_playing.cpp
-	src/gui/pages/statistics.cpp
-	src/gui/pages/search.cpp
-	src/gui/pages/seasons.cpp
-	src/gui/pages/torrents.cpp
-	src/gui/pages/history.cpp
+	src/gui/pages/anime_list.cc
+	src/gui/pages/now_playing.cc
+	src/gui/pages/statistics.cc
+	src/gui/pages/search.cc
+	src/gui/pages/seasons.cc
+	src/gui/pages/torrents.cc
+	src/gui/pages/history.cc
 
 
 	# Custom widgets
-	src/gui/widgets/anime_info.cpp
-	src/gui/widgets/poster.cpp
-	src/gui/widgets/clickable_label.cpp
-	src/gui/widgets/sidebar.cpp
-	src/gui/widgets/text.cpp
-	src/gui/widgets/optional_date.cpp
+	src/gui/widgets/anime_info.cc
+	src/gui/widgets/poster.cc
+	src/gui/widgets/clickable_label.cc
+	src/gui/widgets/sidebar.cc
+	src/gui/widgets/text.cc
+	src/gui/widgets/optional_date.cc
 
 	# Dialogs
-	src/gui/dialog/about.cpp
-	src/gui/dialog/information.cpp
-	src/gui/dialog/settings.cpp
-	src/gui/dialog/settings/application.cpp
-	src/gui/dialog/settings/services.cpp
+	src/gui/dialog/about.cc
+	src/gui/dialog/information.cc
+	src/gui/dialog/settings.cc
+	src/gui/dialog/settings/application.cc
+	src/gui/dialog/settings/services.cc
 
 	# Translate
-	src/gui/translate/anime.cpp
-	src/gui/translate/anilist.cpp
+	src/gui/translate/anime.cc
+	src/gui/translate/anilist.cc
 
 	# Services (only AniList for now)
-	src/services/services.cpp
-	src/services/anilist.cpp
+	src/services/services.cc
+	src/services/anilist.cc
 
 	# Tracking
-	src/track/media.cpp
+	src/track/media.cc
 
 	# Qt resources
 	rc/icons.qrc
@@ -112,7 +112,7 @@
 		src/sys/osx/filesystem.mm
 	)
 elseif(WIN32) # Windows
-	list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp)
+	list(APPEND SRC_FILES src/sys/win32/dark_theme.cc)
 endif()
 
 add_executable(minori ${SRC_FILES})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/anime.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,290 @@
+/*
+ * anime.cpp: defining of custom anime-related
+ * datatypes & variables
+ */
+#include "core/anime.h"
+#include "core/date.h"
+#include "core/session.h"
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <string>
+#include <vector>
+
+namespace Anime {
+
+/* User list data */
+bool Anime::IsInUserList() const {
+	if (list_info_.get())
+		return true;
+	return false;
+}
+
+void Anime::AddToUserList() {
+	list_info_.reset(new ListInformation);
+}
+
+void Anime::RemoveFromUserList() {
+	list_info_.reset();
+}
+
+ListStatus Anime::GetUserStatus() const {
+	assert(list_info_.get());
+	return list_info_->status;
+}
+
+int Anime::GetUserProgress() const {
+	assert(list_info_.get());
+	return list_info_->progress;
+}
+
+int Anime::GetUserScore() const {
+	assert(list_info_.get());
+	return list_info_->score;
+}
+
+Date Anime::GetUserDateStarted() const {
+	assert(list_info_.get());
+	return list_info_->started;
+}
+
+Date Anime::GetUserDateCompleted() const {
+	assert(list_info_.get());
+	return list_info_->completed;
+}
+
+bool Anime::GetUserIsPrivate() const {
+	assert(list_info_.get());
+	return list_info_->is_private;
+}
+
+unsigned int Anime::GetUserRewatchedTimes() const {
+	assert(list_info_.get());
+	return list_info_->rewatched_times;
+}
+
+bool Anime::GetUserIsRewatching() const {
+	assert(list_info_.get());
+	return list_info_->rewatching;
+}
+
+uint64_t Anime::GetUserTimeUpdated() const {
+	assert(list_info_.get());
+	return list_info_->updated;
+}
+
+std::string Anime::GetUserNotes() const {
+	assert(list_info_.get());
+	return list_info_->notes;
+}
+
+void Anime::SetUserStatus(ListStatus status) {
+	assert(list_info_.get());
+	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;
+}
+
+void Anime::SetUserDateStarted(Date const& started) {
+	assert(list_info_.get());
+	list_info_->started = started;
+}
+
+void Anime::SetUserDateCompleted(Date const& completed) {
+	assert(list_info_.get());
+	list_info_->completed = completed;
+}
+
+void Anime::SetUserIsPrivate(bool is_private) {
+	assert(list_info_.get());
+	list_info_->is_private = is_private;
+}
+
+void Anime::SetUserRewatchedTimes(int rewatched) {
+	assert(list_info_.get());
+	list_info_->rewatched_times = rewatched;
+}
+
+void Anime::SetUserIsRewatching(bool rewatching) {
+	assert(list_info_.get());
+	list_info_->rewatching = rewatching;
+}
+
+void Anime::SetUserTimeUpdated(uint64_t updated) {
+	assert(list_info_.get());
+	list_info_->updated = updated;
+}
+
+void Anime::SetUserNotes(std::string const& notes) {
+	assert(list_info_.get());
+	list_info_->notes = notes;
+}
+
+/* Series data */
+int Anime::GetId() const {
+	return info_.id;
+}
+
+std::string Anime::GetRomajiTitle() const {
+	return info_.title.romaji;
+}
+
+std::string Anime::GetEnglishTitle() const {
+	return info_.title.english;
+}
+
+std::string Anime::GetNativeTitle() const {
+	return info_.title.native;
+}
+
+std::vector<std::string> Anime::GetTitleSynonyms() const {
+	std::vector<std::string> result;
+#define IN_VECTOR(v, k) (std::count(v.begin(), v.end(), k))
+#define ADD_TO_SYNONYMS(v, k) \
+	if (!k.empty() && !IN_VECTOR(v, k) && k != GetUserPreferredTitle()) \
+	v.push_back(k)
+	ADD_TO_SYNONYMS(result, info_.title.english);
+	ADD_TO_SYNONYMS(result, info_.title.romaji);
+	ADD_TO_SYNONYMS(result, info_.title.native);
+	for (auto& synonym : info_.synonyms) {
+		ADD_TO_SYNONYMS(result, synonym);
+	}
+#undef ADD_TO_SYNONYMS
+#undef IN_VECTOR
+	return result;
+}
+
+int Anime::GetEpisodes() const {
+	return info_.episodes;
+}
+
+SeriesStatus Anime::GetAiringStatus() const {
+	return info_.status;
+}
+
+Date Anime::GetAirDate() const {
+	return info_.air_date;
+}
+
+std::vector<std::string> Anime::GetGenres() const {
+	return info_.genres;
+}
+
+std::vector<std::string> Anime::GetProducers() const {
+	return info_.producers;
+}
+
+SeriesFormat Anime::GetFormat() const {
+	return info_.format;
+}
+
+SeriesSeason Anime::GetSeason() const {
+	return info_.season;
+}
+
+int Anime::GetAudienceScore() const {
+	return info_.audience_score;
+}
+
+std::string Anime::GetSynopsis() const {
+	return info_.synopsis;
+}
+
+int Anime::GetDuration() const {
+	return info_.duration;
+}
+
+std::string Anime::GetPosterUrl() const {
+	return info_.poster_url;
+}
+
+std::string Anime::GetServiceUrl() const {
+	return "https://anilist.co/anime/" + std::to_string(GetId());
+}
+
+void Anime::SetId(int id) {
+	info_.id = id;
+}
+
+void Anime::SetRomajiTitle(std::string const& title) {
+	info_.title.romaji = title;
+}
+
+void Anime::SetEnglishTitle(std::string const& title) {
+	info_.title.english = title;
+}
+
+void Anime::SetNativeTitle(std::string const& title) {
+	info_.title.native = title;
+}
+
+void Anime::SetTitleSynonyms(std::vector<std::string> const& synonyms) {
+	info_.synonyms = synonyms;
+}
+
+void Anime::AddTitleSynonym(std::string const& synonym) {
+	info_.synonyms.push_back(synonym);
+}
+
+void Anime::SetEpisodes(int episodes) {
+	info_.episodes = episodes;
+}
+
+void Anime::SetAiringStatus(SeriesStatus status) {
+	info_.status = status;
+}
+
+void Anime::SetAirDate(Date const& date) {
+	info_.air_date = date;
+}
+
+void Anime::SetGenres(std::vector<std::string> const& genres) {
+	info_.genres = genres;
+}
+
+void Anime::SetProducers(std::vector<std::string> const& producers) {
+	info_.producers = producers;
+}
+
+void Anime::SetFormat(SeriesFormat format) {
+	info_.format = format;
+}
+
+void Anime::SetSeason(SeriesSeason season) {
+	info_.season = season;
+}
+
+void Anime::SetAudienceScore(int audience_score) {
+	info_.audience_score = audience_score;
+}
+
+void Anime::SetSynopsis(std::string synopsis) {
+	info_.synopsis = synopsis;
+}
+
+void Anime::SetDuration(int duration) {
+	info_.duration = duration;
+}
+
+void Anime::SetPosterUrl(std::string url) {
+	info_.poster_url = url;
+}
+
+std::string Anime::GetUserPreferredTitle() const {
+	switch (session.config.anime_list.language) {
+		case TitleLanguage::NATIVE: return (GetNativeTitle().empty()) ? GetRomajiTitle() : GetNativeTitle();
+		case TitleLanguage::ENGLISH: return (GetEnglishTitle().empty()) ? GetRomajiTitle() : GetEnglishTitle();
+		default: break;
+	}
+	return GetRomajiTitle();
+}
+
+} // namespace Anime
--- a/src/core/anime.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,290 +0,0 @@
-/*
- * anime.cpp: defining of custom anime-related
- * datatypes & variables
- */
-#include "core/anime.h"
-#include "core/date.h"
-#include "core/session.h"
-#include <algorithm>
-#include <chrono>
-#include <cmath>
-#include <string>
-#include <vector>
-
-namespace Anime {
-
-/* User list data */
-bool Anime::IsInUserList() const {
-	if (list_info_.get())
-		return true;
-	return false;
-}
-
-void Anime::AddToUserList() {
-	list_info_.reset(new ListInformation);
-}
-
-void Anime::RemoveFromUserList() {
-	list_info_.reset();
-}
-
-ListStatus Anime::GetUserStatus() const {
-	assert(list_info_.get());
-	return list_info_->status;
-}
-
-int Anime::GetUserProgress() const {
-	assert(list_info_.get());
-	return list_info_->progress;
-}
-
-int Anime::GetUserScore() const {
-	assert(list_info_.get());
-	return list_info_->score;
-}
-
-Date Anime::GetUserDateStarted() const {
-	assert(list_info_.get());
-	return list_info_->started;
-}
-
-Date Anime::GetUserDateCompleted() const {
-	assert(list_info_.get());
-	return list_info_->completed;
-}
-
-bool Anime::GetUserIsPrivate() const {
-	assert(list_info_.get());
-	return list_info_->is_private;
-}
-
-unsigned int Anime::GetUserRewatchedTimes() const {
-	assert(list_info_.get());
-	return list_info_->rewatched_times;
-}
-
-bool Anime::GetUserIsRewatching() const {
-	assert(list_info_.get());
-	return list_info_->rewatching;
-}
-
-uint64_t Anime::GetUserTimeUpdated() const {
-	assert(list_info_.get());
-	return list_info_->updated;
-}
-
-std::string Anime::GetUserNotes() const {
-	assert(list_info_.get());
-	return list_info_->notes;
-}
-
-void Anime::SetUserStatus(ListStatus status) {
-	assert(list_info_.get());
-	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;
-}
-
-void Anime::SetUserDateStarted(Date const& started) {
-	assert(list_info_.get());
-	list_info_->started = started;
-}
-
-void Anime::SetUserDateCompleted(Date const& completed) {
-	assert(list_info_.get());
-	list_info_->completed = completed;
-}
-
-void Anime::SetUserIsPrivate(bool is_private) {
-	assert(list_info_.get());
-	list_info_->is_private = is_private;
-}
-
-void Anime::SetUserRewatchedTimes(int rewatched) {
-	assert(list_info_.get());
-	list_info_->rewatched_times = rewatched;
-}
-
-void Anime::SetUserIsRewatching(bool rewatching) {
-	assert(list_info_.get());
-	list_info_->rewatching = rewatching;
-}
-
-void Anime::SetUserTimeUpdated(uint64_t updated) {
-	assert(list_info_.get());
-	list_info_->updated = updated;
-}
-
-void Anime::SetUserNotes(std::string const& notes) {
-	assert(list_info_.get());
-	list_info_->notes = notes;
-}
-
-/* Series data */
-int Anime::GetId() const {
-	return info_.id;
-}
-
-std::string Anime::GetRomajiTitle() const {
-	return info_.title.romaji;
-}
-
-std::string Anime::GetEnglishTitle() const {
-	return info_.title.english;
-}
-
-std::string Anime::GetNativeTitle() const {
-	return info_.title.native;
-}
-
-std::vector<std::string> Anime::GetTitleSynonyms() const {
-	std::vector<std::string> result;
-#define IN_VECTOR(v, k) (std::count(v.begin(), v.end(), k))
-#define ADD_TO_SYNONYMS(v, k) \
-	if (!k.empty() && !IN_VECTOR(v, k) && k != GetUserPreferredTitle()) \
-	v.push_back(k)
-	ADD_TO_SYNONYMS(result, info_.title.english);
-	ADD_TO_SYNONYMS(result, info_.title.romaji);
-	ADD_TO_SYNONYMS(result, info_.title.native);
-	for (auto& synonym : info_.synonyms) {
-		ADD_TO_SYNONYMS(result, synonym);
-	}
-#undef ADD_TO_SYNONYMS
-#undef IN_VECTOR
-	return result;
-}
-
-int Anime::GetEpisodes() const {
-	return info_.episodes;
-}
-
-SeriesStatus Anime::GetAiringStatus() const {
-	return info_.status;
-}
-
-Date Anime::GetAirDate() const {
-	return info_.air_date;
-}
-
-std::vector<std::string> Anime::GetGenres() const {
-	return info_.genres;
-}
-
-std::vector<std::string> Anime::GetProducers() const {
-	return info_.producers;
-}
-
-SeriesFormat Anime::GetFormat() const {
-	return info_.format;
-}
-
-SeriesSeason Anime::GetSeason() const {
-	return info_.season;
-}
-
-int Anime::GetAudienceScore() const {
-	return info_.audience_score;
-}
-
-std::string Anime::GetSynopsis() const {
-	return info_.synopsis;
-}
-
-int Anime::GetDuration() const {
-	return info_.duration;
-}
-
-std::string Anime::GetPosterUrl() const {
-	return info_.poster_url;
-}
-
-std::string Anime::GetServiceUrl() const {
-	return "https://anilist.co/anime/" + std::to_string(GetId());
-}
-
-void Anime::SetId(int id) {
-	info_.id = id;
-}
-
-void Anime::SetRomajiTitle(std::string const& title) {
-	info_.title.romaji = title;
-}
-
-void Anime::SetEnglishTitle(std::string const& title) {
-	info_.title.english = title;
-}
-
-void Anime::SetNativeTitle(std::string const& title) {
-	info_.title.native = title;
-}
-
-void Anime::SetTitleSynonyms(std::vector<std::string> const& synonyms) {
-	info_.synonyms = synonyms;
-}
-
-void Anime::AddTitleSynonym(std::string const& synonym) {
-	info_.synonyms.push_back(synonym);
-}
-
-void Anime::SetEpisodes(int episodes) {
-	info_.episodes = episodes;
-}
-
-void Anime::SetAiringStatus(SeriesStatus status) {
-	info_.status = status;
-}
-
-void Anime::SetAirDate(Date const& date) {
-	info_.air_date = date;
-}
-
-void Anime::SetGenres(std::vector<std::string> const& genres) {
-	info_.genres = genres;
-}
-
-void Anime::SetProducers(std::vector<std::string> const& producers) {
-	info_.producers = producers;
-}
-
-void Anime::SetFormat(SeriesFormat format) {
-	info_.format = format;
-}
-
-void Anime::SetSeason(SeriesSeason season) {
-	info_.season = season;
-}
-
-void Anime::SetAudienceScore(int audience_score) {
-	info_.audience_score = audience_score;
-}
-
-void Anime::SetSynopsis(std::string synopsis) {
-	info_.synopsis = synopsis;
-}
-
-void Anime::SetDuration(int duration) {
-	info_.duration = duration;
-}
-
-void Anime::SetPosterUrl(std::string url) {
-	info_.poster_url = url;
-}
-
-std::string Anime::GetUserPreferredTitle() const {
-	switch (session.config.anime_list.language) {
-		case TitleLanguage::NATIVE: return (GetNativeTitle().empty()) ? GetRomajiTitle() : GetNativeTitle();
-		case TitleLanguage::ENGLISH: return (GetEnglishTitle().empty()) ? GetRomajiTitle() : GetEnglishTitle();
-		default: break;
-	}
-	return GetRomajiTitle();
-}
-
-} // namespace Anime
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/anime_db.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,108 @@
+#include "core/anime_db.h"
+#include "core/anime.h"
+#include "core/strings.h"
+#include <QDebug>
+
+namespace Anime {
+
+int Database::GetTotalAnimeAmount() {
+	int total = 0;
+	for (const auto& a : items) {
+		if (a.second.IsInUserList())
+			total++;
+	}
+	return total;
+}
+
+int Database::GetListsAnimeAmount(ListStatus status) {
+	if (status == ListStatus::NOT_IN_LIST)
+		return 0;
+	int total = 0;
+	for (const auto& a : items) {
+		if (a.second.IsInUserList() && a.second.GetUserStatus() == status)
+			total++;
+	}
+	return total;
+}
+
+int Database::GetTotalEpisodeAmount() {
+	int total = 0;
+	for (const auto& a : items) {
+		if (a.second.IsInUserList()) {
+			total += a.second.GetUserRewatchedTimes() * a.second.GetEpisodes();
+			total += a.second.GetUserProgress();
+		}
+	}
+	return total;
+}
+
+/* Returns the total watched amount in minutes. */
+int Database::GetTotalWatchedAmount() {
+	int total = 0;
+	for (const auto& a : items) {
+		if (a.second.IsInUserList()) {
+			total += a.second.GetDuration() * a.second.GetUserProgress();
+			total += a.second.GetEpisodes() * a.second.GetDuration() * a.second.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& a : items) {
+		if (a.second.IsInUserList())
+			total += a.second.GetDuration() * (a.second.GetEpisodes() - a.second.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& a : items) {
+		if (a.second.IsInUserList() && a.second.GetUserScore()) {
+			avg += a.second.GetUserScore();
+			amt++;
+		}
+	}
+	return avg / amt;
+}
+
+double Database::GetScoreDeviation() {
+	double squares_sum = 0, avg = GetAverageScore();
+	int amt = 0;
+	for (const auto& a : items) {
+		if (a.second.IsInUserList() && a.second.GetUserScore()) {
+			squares_sum += std::pow(static_cast<double>(a.second.GetUserScore()) - avg, 2);
+			amt++;
+		}
+	}
+	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
+}
+
+int Database::GetAnimeFromTitle(std::string title) {
+	if (title.empty())
+		return 0;
+	for (const auto& a : items) {
+		if (a.second.GetUserPreferredTitle().find(title) != std::string::npos)
+			return a.second.GetId();
+		for (const auto& t : a.second.GetTitleSynonyms()) {
+			if (t.find(title) != std::string::npos) {
+				return a.second.GetId();
+			}
+		}
+	}
+	return 0;
+}
+
+Database db;
+
+} // namespace Anime
\ No newline at end of file
--- a/src/core/anime_db.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-#include "core/anime_db.h"
-#include "core/anime.h"
-#include "core/strings.h"
-#include <QDebug>
-
-namespace Anime {
-
-int Database::GetTotalAnimeAmount() {
-	int total = 0;
-	for (const auto& a : items) {
-		if (a.second.IsInUserList())
-			total++;
-	}
-	return total;
-}
-
-int Database::GetListsAnimeAmount(ListStatus status) {
-	if (status == ListStatus::NOT_IN_LIST)
-		return 0;
-	int total = 0;
-	for (const auto& a : items) {
-		if (a.second.IsInUserList() && a.second.GetUserStatus() == status)
-			total++;
-	}
-	return total;
-}
-
-int Database::GetTotalEpisodeAmount() {
-	int total = 0;
-	for (const auto& a : items) {
-		if (a.second.IsInUserList()) {
-			total += a.second.GetUserRewatchedTimes() * a.second.GetEpisodes();
-			total += a.second.GetUserProgress();
-		}
-	}
-	return total;
-}
-
-/* Returns the total watched amount in minutes. */
-int Database::GetTotalWatchedAmount() {
-	int total = 0;
-	for (const auto& a : items) {
-		if (a.second.IsInUserList()) {
-			total += a.second.GetDuration() * a.second.GetUserProgress();
-			total += a.second.GetEpisodes() * a.second.GetDuration() * a.second.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& a : items) {
-		if (a.second.IsInUserList())
-			total += a.second.GetDuration() * (a.second.GetEpisodes() - a.second.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& a : items) {
-		if (a.second.IsInUserList() && a.second.GetUserScore()) {
-			avg += a.second.GetUserScore();
-			amt++;
-		}
-	}
-	return avg / amt;
-}
-
-double Database::GetScoreDeviation() {
-	double squares_sum = 0, avg = GetAverageScore();
-	int amt = 0;
-	for (const auto& a : items) {
-		if (a.second.IsInUserList() && a.second.GetUserScore()) {
-			squares_sum += std::pow(static_cast<double>(a.second.GetUserScore()) - avg, 2);
-			amt++;
-		}
-	}
-	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
-}
-
-int Database::GetAnimeFromTitle(std::string title) {
-	if (title.empty())
-		return 0;
-	for (const auto& a : items) {
-		if (a.second.GetUserPreferredTitle().find(title) != std::string::npos)
-			return a.second.GetId();
-		for (const auto& t : a.second.GetTitleSynonyms()) {
-			if (t.find(title) != std::string::npos) {
-				return a.second.GetId();
-			}
-		}
-	}
-	return 0;
-}
-
-Database db;
-
-} // namespace Anime
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/config.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,106 @@
+/**
+ * config.cpp:
+ * parses the config... lol
+ **/
+#include "core/config.h"
+#include "core/anime.h"
+#include "core/filesystem.h"
+#include "core/json.h"
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <fstream>
+#include <limits.h>
+
+std::map<std::string, Themes> StringToTheme = {
+    {"Default", Themes::OS   },
+    {"Light",   Themes::LIGHT},
+    {"Dark",    Themes::DARK }
+};
+
+std::map<Themes, std::string> ThemeToString = {
+    {Themes::OS,    "Default"},
+    {Themes::LIGHT, "Light"  },
+    {Themes::DARK,  "Dark"   }
+};
+
+std::map<Anime::Services, std::string> ServiceToString{
+    {Anime::Services::NONE,    "None"   },
+    {Anime::Services::ANILIST, "AniList"}
+};
+
+std::map<std::string, Anime::Services> StringToService{
+    {"None",    Anime::Services::NONE   },
+    {"AniList", Anime::Services::ANILIST}
+};
+
+std::map<Anime::TitleLanguage, std::string> AnimeTitleToStringMap = {
+    {Anime::TitleLanguage::ROMAJI,  "Romaji" },
+    {Anime::TitleLanguage::NATIVE,  "Native" },
+    {Anime::TitleLanguage::ENGLISH, "English"}
+};
+
+std::map<std::string, Anime::TitleLanguage> StringToAnimeTitleMap = {
+    {"Romaji",  Anime::TitleLanguage::ROMAJI },
+    {"Native",  Anime::TitleLanguage::NATIVE },
+    {"English", Anime::TitleLanguage::ENGLISH}
+};
+
+int Config::Load() {
+	Filesystem::Path cfg_path = Filesystem::GetConfigPath();
+	if (!cfg_path.Exists())
+		return 0;
+	std::ifstream config(cfg_path.GetPath(), std::ifstream::in);
+	auto config_js = nlohmann::json::parse(config);
+	service = StringToService[JSON::GetString(config_js, "/General/Service"_json_pointer)];
+	anime_list.language =
+	    StringToAnimeTitleMap[JSON::GetString(config_js, "/Anime List/Title language"_json_pointer, "Romaji")];
+	anime_list.display_aired_episodes =
+	    JSON::GetBoolean(config_js, "/Anime List/Display only aired episodes"_json_pointer, true);
+	anime_list.display_available_episodes =
+	    JSON::GetBoolean(config_js, "/Anime List/Display only available episodes in library"_json_pointer, true);
+	anime_list.highlight_anime_if_available =
+	    JSON::GetBoolean(config_js, "/Anime List/Highlight anime if available"_json_pointer, true);
+	anime_list.highlighted_anime_above_others =
+	    JSON::GetBoolean(config_js, "/Anime List/Display highlighted anime above others"_json_pointer);
+	anilist.auth_token = JSON::GetString(config_js, "/Authorization/AniList/Auth Token"_json_pointer);
+	anilist.username = JSON::GetString(config_js, "/Authorization/AniList/Username"_json_pointer);
+	anilist.user_id = JSON::GetInt(config_js, "/Authorization/AniList/User ID"_json_pointer);
+	theme = StringToTheme[JSON::GetString(config_js, "/Appearance/Theme"_json_pointer)];
+	config.close();
+	return 0;
+}
+
+int Config::Save() {
+	Filesystem::Path cfg_path = Filesystem::GetConfigPath();
+	if (!cfg_path.GetParent().Exists())
+		cfg_path.GetParent().CreateDirectories();
+	std::ofstream config(cfg_path.GetPath(), std::ofstream::out | std::ofstream::trunc);
+	/* clang-format off */
+	nlohmann::json config_js = {
+		{"General",	{
+			{"Service", ServiceToString[service]}
+		}},
+		{"Anime List", {
+			{"Title language", AnimeTitleToStringMap[anime_list.language]},
+			{"Display only aired episodes", anime_list.display_aired_episodes},
+			{"Display only available episodes in library", anime_list.display_available_episodes},
+			{"Highlight anime if available", anime_list.highlight_anime_if_available},
+			{"Display highlighted anime above others", anime_list.highlighted_anime_above_others}
+		}},
+		{"Authorization", {
+			{"AniList", {
+				{"Auth Token", anilist.auth_token},
+				{"Username", anilist.username},
+				{"User ID", anilist.user_id}
+			}}
+		}},
+		{"Appearance", {
+			{"Theme", ThemeToString[theme]}
+		}}
+	};
+	/* clang-format on */
+	config << std::setw(4) << config_js << std::endl;
+	config.close();
+	return 0;
+}
--- a/src/core/config.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-/**
- * config.cpp:
- * parses the config... lol
- **/
-#include "core/config.h"
-#include "core/anime.h"
-#include "core/filesystem.h"
-#include "core/json.h"
-#include <cstdlib>
-#include <cstring>
-#include <filesystem>
-#include <fstream>
-#include <limits.h>
-
-std::map<std::string, Themes> StringToTheme = {
-    {"Default", Themes::OS   },
-    {"Light",   Themes::LIGHT},
-    {"Dark",    Themes::DARK }
-};
-
-std::map<Themes, std::string> ThemeToString = {
-    {Themes::OS,    "Default"},
-    {Themes::LIGHT, "Light"  },
-    {Themes::DARK,  "Dark"   }
-};
-
-std::map<Anime::Services, std::string> ServiceToString{
-    {Anime::Services::NONE,    "None"   },
-    {Anime::Services::ANILIST, "AniList"}
-};
-
-std::map<std::string, Anime::Services> StringToService{
-    {"None",    Anime::Services::NONE   },
-    {"AniList", Anime::Services::ANILIST}
-};
-
-std::map<Anime::TitleLanguage, std::string> AnimeTitleToStringMap = {
-    {Anime::TitleLanguage::ROMAJI,  "Romaji" },
-    {Anime::TitleLanguage::NATIVE,  "Native" },
-    {Anime::TitleLanguage::ENGLISH, "English"}
-};
-
-std::map<std::string, Anime::TitleLanguage> StringToAnimeTitleMap = {
-    {"Romaji",  Anime::TitleLanguage::ROMAJI },
-    {"Native",  Anime::TitleLanguage::NATIVE },
-    {"English", Anime::TitleLanguage::ENGLISH}
-};
-
-int Config::Load() {
-	Filesystem::Path cfg_path = Filesystem::GetConfigPath();
-	if (!cfg_path.Exists())
-		return 0;
-	std::ifstream config(cfg_path.GetPath(), std::ifstream::in);
-	auto config_js = nlohmann::json::parse(config);
-	service = StringToService[JSON::GetString(config_js, "/General/Service"_json_pointer)];
-	anime_list.language =
-	    StringToAnimeTitleMap[JSON::GetString(config_js, "/Anime List/Title language"_json_pointer, "Romaji")];
-	anime_list.display_aired_episodes =
-	    JSON::GetBoolean(config_js, "/Anime List/Display only aired episodes"_json_pointer, true);
-	anime_list.display_available_episodes =
-	    JSON::GetBoolean(config_js, "/Anime List/Display only available episodes in library"_json_pointer, true);
-	anime_list.highlight_anime_if_available =
-	    JSON::GetBoolean(config_js, "/Anime List/Highlight anime if available"_json_pointer, true);
-	anime_list.highlighted_anime_above_others =
-	    JSON::GetBoolean(config_js, "/Anime List/Display highlighted anime above others"_json_pointer);
-	anilist.auth_token = JSON::GetString(config_js, "/Authorization/AniList/Auth Token"_json_pointer);
-	anilist.username = JSON::GetString(config_js, "/Authorization/AniList/Username"_json_pointer);
-	anilist.user_id = JSON::GetInt(config_js, "/Authorization/AniList/User ID"_json_pointer);
-	theme = StringToTheme[JSON::GetString(config_js, "/Appearance/Theme"_json_pointer)];
-	config.close();
-	return 0;
-}
-
-int Config::Save() {
-	Filesystem::Path cfg_path = Filesystem::GetConfigPath();
-	if (!cfg_path.GetParent().Exists())
-		cfg_path.GetParent().CreateDirectories();
-	std::ofstream config(cfg_path.GetPath(), std::ofstream::out | std::ofstream::trunc);
-	/* clang-format off */
-	nlohmann::json config_js = {
-		{"General",	{
-			{"Service", ServiceToString[service]}
-		}},
-		{"Anime List", {
-			{"Title language", AnimeTitleToStringMap[anime_list.language]},
-			{"Display only aired episodes", anime_list.display_aired_episodes},
-			{"Display only available episodes in library", anime_list.display_available_episodes},
-			{"Highlight anime if available", anime_list.highlight_anime_if_available},
-			{"Display highlighted anime above others", anime_list.highlighted_anime_above_others}
-		}},
-		{"Authorization", {
-			{"AniList", {
-				{"Auth Token", anilist.auth_token},
-				{"Username", anilist.username},
-				{"User ID", anilist.user_id}
-			}}
-		}},
-		{"Appearance", {
-			{"Theme", ThemeToString[theme]}
-		}}
-	};
-	/* clang-format on */
-	config << std::setw(4) << config_js << std::endl;
-	config.close();
-	return 0;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/date.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,122 @@
+#include "core/date.h"
+#include "core/json.h"
+#include <QDate>
+#include <QDebug>
+#include <algorithm>
+#include <cstdint>
+#include <tuple>
+
+/* An implementation of AniList's "fuzzy date" */
+
+#define CLAMP(x, low, high) (std::max(low, std::min(high, x)))
+
+Date::Date() {
+}
+
+Date::Date(unsigned int y) {
+	SetYear(y);
+}
+
+Date::Date(unsigned int y, unsigned int m, unsigned int d) {
+	SetYear(y);
+	SetMonth(m);
+	SetDay(d);
+}
+
+Date::Date(const QDate& date) {
+	SetYear(date.year());
+	SetMonth(date.month());
+	SetDay(date.day());
+}
+
+void Date::VoidYear() {
+	year.reset();
+}
+
+void Date::VoidMonth() {
+	month.reset();
+}
+
+void Date::VoidDay() {
+	day.reset();
+}
+
+void Date::SetYear(unsigned int y) {
+	year.reset(new unsigned int(y));
+}
+
+void Date::SetMonth(unsigned int m) {
+	month.reset(new unsigned int(CLAMP(m, 1U, 12U)));
+}
+
+void Date::SetDay(unsigned int d) {
+	day.reset(new unsigned int(CLAMP(d, 1U, 31U)));
+}
+
+unsigned int Date::GetYear() const {
+	unsigned int* ptr = year.get();
+	if (ptr != nullptr)
+		return *year;
+	return -1;
+}
+
+unsigned int Date::GetMonth() const {
+	unsigned int* ptr = month.get();
+	if (ptr != nullptr)
+		return *month;
+	return -1;
+}
+
+unsigned int Date::GetDay() const {
+	unsigned int* ptr = day.get();
+	if (ptr != nullptr)
+		return *day;
+	return -1;
+}
+
+bool Date::IsValid() const {
+	return year.get() && month.get() && day.get();
+}
+
+bool Date::operator<(const Date& other) const {
+	unsigned int y = GetYear(), m = GetMonth(), d = GetDay();
+	unsigned int o_y = other.GetYear(), o_m = other.GetMonth(), o_d = other.GetDay();
+	return std::tie(y, m, d) < std::tie(o_y, o_m, o_d);
+}
+
+bool Date::operator>(const Date& other) const {
+	return other < (*this);
+}
+
+bool Date::operator<=(const Date& other) const {
+	return !((*this) > other);
+}
+
+bool Date::operator>=(const Date& other) const {
+	return !((*this) < other);
+}
+
+QDate Date::GetAsQDate() const {
+	/* QDates don't support "missing" values, for good reason. */
+	if (IsValid())
+		return QDate(*year, *month, *day);
+	else
+		return QDate();
+}
+
+nlohmann::json Date::GetAsAniListJson() const {
+	nlohmann::json result = {};
+	if (year.get())
+		result["year"] = *year;
+	else
+		result["year"] = nullptr;
+	if (month.get())
+		result["month"] = *month;
+	else
+		result["month"] = nullptr;
+	if (day.get())
+		result["day"] = *day;
+	else
+		result["day"] = nullptr;
+	return result;
+}
--- a/src/core/date.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-#include "core/date.h"
-#include "core/json.h"
-#include <QDate>
-#include <QDebug>
-#include <algorithm>
-#include <cstdint>
-#include <tuple>
-
-/* An implementation of AniList's "fuzzy date" */
-
-#define CLAMP(x, low, high) (std::max(low, std::min(high, x)))
-
-Date::Date() {
-}
-
-Date::Date(unsigned int y) {
-	SetYear(y);
-}
-
-Date::Date(unsigned int y, unsigned int m, unsigned int d) {
-	SetYear(y);
-	SetMonth(m);
-	SetDay(d);
-}
-
-Date::Date(const QDate& date) {
-	SetYear(date.year());
-	SetMonth(date.month());
-	SetDay(date.day());
-}
-
-void Date::VoidYear() {
-	year.reset();
-}
-
-void Date::VoidMonth() {
-	month.reset();
-}
-
-void Date::VoidDay() {
-	day.reset();
-}
-
-void Date::SetYear(unsigned int y) {
-	year.reset(new unsigned int(y));
-}
-
-void Date::SetMonth(unsigned int m) {
-	month.reset(new unsigned int(CLAMP(m, 1U, 12U)));
-}
-
-void Date::SetDay(unsigned int d) {
-	day.reset(new unsigned int(CLAMP(d, 1U, 31U)));
-}
-
-unsigned int Date::GetYear() const {
-	unsigned int* ptr = year.get();
-	if (ptr != nullptr)
-		return *year;
-	return -1;
-}
-
-unsigned int Date::GetMonth() const {
-	unsigned int* ptr = month.get();
-	if (ptr != nullptr)
-		return *month;
-	return -1;
-}
-
-unsigned int Date::GetDay() const {
-	unsigned int* ptr = day.get();
-	if (ptr != nullptr)
-		return *day;
-	return -1;
-}
-
-bool Date::IsValid() const {
-	return year.get() && month.get() && day.get();
-}
-
-bool Date::operator<(const Date& other) const {
-	unsigned int y = GetYear(), m = GetMonth(), d = GetDay();
-	unsigned int o_y = other.GetYear(), o_m = other.GetMonth(), o_d = other.GetDay();
-	return std::tie(y, m, d) < std::tie(o_y, o_m, o_d);
-}
-
-bool Date::operator>(const Date& other) const {
-	return other < (*this);
-}
-
-bool Date::operator<=(const Date& other) const {
-	return !((*this) > other);
-}
-
-bool Date::operator>=(const Date& other) const {
-	return !((*this) < other);
-}
-
-QDate Date::GetAsQDate() const {
-	/* QDates don't support "missing" values, for good reason. */
-	if (IsValid())
-		return QDate(*year, *month, *day);
-	else
-		return QDate();
-}
-
-nlohmann::json Date::GetAsAniListJson() const {
-	nlohmann::json result = {};
-	if (year.get())
-		result["year"] = *year;
-	else
-		result["year"] = nullptr;
-	if (month.get())
-		result["month"] = *month;
-	else
-		result["month"] = nullptr;
-	if (day.get())
-		result["day"] = *day;
-	else
-		result["day"] = nullptr;
-	return result;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/filesystem.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,139 @@
+#ifdef WIN32
+#	include <shlobj.h>
+#elif defined(MACOSX)
+#	include "sys/osx/filesystem.h"
+#elif defined(__linux__)
+#	include <pwd.h>
+#	include <sys/types.h>
+#endif
+
+#ifdef WIN32
+#	define DELIM "\\"
+#else
+#	define DELIM "/"
+#	include <errno.h>
+#	include <unistd.h>
+#	include <sys/stat.h>
+#endif
+
+#include "core/filesystem.h"
+#include "core/config.h"
+#include "core/strings.h"
+#include <limits.h>
+
+namespace Filesystem {
+
+Path::Path() {
+	_path = "";
+}
+Path::Path(const std::string& path) {
+	_path = path;
+}
+Path::Path(const Path& path) {
+	_path = path.GetPath();
+}
+
+bool Path::CreateDirectories() const {
+	std::string temp = "";
+	size_t start;
+	size_t end = 0;
+	temp.append(_path.substr(0, _path.find_first_not_of(DELIM, end)));
+
+	while ((start = _path.find_first_not_of(DELIM, end)) != std::string::npos) {
+		end = _path.find(DELIM, start);
+		temp.append(_path.substr(start, end - start));
+#ifdef WIN32
+		if (!CreateDirectoryW(Strings::ToWstring(temp).c_str(), NULL) && GetLastError() == ERROR_PATH_NOT_FOUND)
+			/* ERROR_PATH_NOT_FOUND should NOT happen here */
+			return false;
+#else
+		struct stat st;
+		if (stat(temp.c_str(), &st) == -1)
+			mkdir(temp.c_str(), 0755);
+#endif
+		temp.append(DELIM);
+	}
+	return true;
+}
+
+bool Path::Exists() const {
+#if WIN32
+	std::wstring buf = Strings::ToWstring(_path);
+	return GetFileAttributesW(buf.c_str()) != INVALID_FILE_ATTRIBUTES;
+#else
+	struct stat st;
+	return stat(_path.c_str(), &st) == 0;
+#endif
+}
+
+std::string Path::Basename() const {
+	unsigned long long pos = _path.find_last_of(DELIM);
+	return pos != std::string::npos ? _path.substr(pos + 1, _path.length()) : "";
+}
+
+std::string Path::Stem() const {
+	std::string basename = Basename();
+	unsigned long long pos = basename.find_last_of(".");
+	return pos != std::string::npos ? basename.substr(0, pos) : "";
+}
+
+std::string Path::Extension() const {
+	std::string basename = Basename();
+	unsigned long long pos = basename.find_last_of(".");
+	return pos != std::string::npos ? basename.substr(pos + 1, basename.length()) : "";
+}
+
+Path Path::GetParent() const {
+	return _path.substr(0, _path.find_last_of(DELIM));
+}
+
+void Path::SetPath(const std::string& path) {
+	_path = path;
+}
+
+std::string Path::GetPath() const {
+	return _path;
+}
+
+Path GetDotPath(void) {
+	std::string ret = "";
+#ifdef WIN32
+	std::wstring buf(MAX_PATH, '\0');
+	if (SHGetFolderPathAndSubDirW(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, CONFIG_WDIR, &buf.front()) ==
+	    S_OK) {
+		buf.resize(buf.find('\0'));
+		ret += Strings::ToUtf8String(buf);
+	}
+#elif defined(MACOSX)
+	ret += osx::GetApplicationSupportDirectory();
+	ret += DELIM CONFIG_DIR;
+#else // just assume POSIX
+	if (getenv("HOME") != NULL)
+		ret += getenv("HOME");
+#	ifdef __linux__
+	else
+		ret += getpwuid(getuid())->pw_dir;
+#	endif // __linux__
+	if (!ret.empty())
+		ret += DELIM ".config" DELIM CONFIG_DIR;
+#endif     // !WIN32 && !MACOSX
+	return ret;
+}
+
+Path GetConfigPath(void) {
+	std::string ret = "";
+	ret += GetDotPath().GetPath();
+	if (!ret.empty())
+		ret += DELIM CONFIG_NAME;
+	return ret;
+}
+
+Path GetAnimeDBPath(void) {
+	std::string ret = "";
+	ret += GetDotPath().GetPath();
+	if (!ret.empty())
+		ret += DELIM "anime" DELIM "db.json";
+	return ret;
+}
+
+} // namespace Filesystem
--- a/src/core/filesystem.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,139 +0,0 @@
-#ifdef WIN32
-#	include <shlobj.h>
-#elif defined(MACOSX)
-#	include "sys/osx/filesystem.h"
-#elif defined(__linux__)
-#	include <pwd.h>
-#	include <sys/types.h>
-#endif
-
-#ifdef WIN32
-#	define DELIM "\\"
-#else
-#	define DELIM "/"
-#	include <errno.h>
-#	include <unistd.h>
-#	include <sys/stat.h>
-#endif
-
-#include "core/filesystem.h"
-#include "core/config.h"
-#include "core/strings.h"
-#include <limits.h>
-
-namespace Filesystem {
-
-Path::Path() {
-	_path = "";
-}
-Path::Path(const std::string& path) {
-	_path = path;
-}
-Path::Path(const Path& path) {
-	_path = path.GetPath();
-}
-
-bool Path::CreateDirectories() const {
-	std::string temp = "";
-	size_t start;
-	size_t end = 0;
-	temp.append(_path.substr(0, _path.find_first_not_of(DELIM, end)));
-
-	while ((start = _path.find_first_not_of(DELIM, end)) != std::string::npos) {
-		end = _path.find(DELIM, start);
-		temp.append(_path.substr(start, end - start));
-#ifdef WIN32
-		if (!CreateDirectoryW(Strings::ToWstring(temp).c_str(), NULL) && GetLastError() == ERROR_PATH_NOT_FOUND)
-			/* ERROR_PATH_NOT_FOUND should NOT happen here */
-			return false;
-#else
-		struct stat st;
-		if (stat(temp.c_str(), &st) == -1)
-			mkdir(temp.c_str(), 0755);
-#endif
-		temp.append(DELIM);
-	}
-	return true;
-}
-
-bool Path::Exists() const {
-#if WIN32
-	std::wstring buf = Strings::ToWstring(_path);
-	return GetFileAttributesW(buf.c_str()) != INVALID_FILE_ATTRIBUTES;
-#else
-	struct stat st;
-	return stat(_path.c_str(), &st) == 0;
-#endif
-}
-
-std::string Path::Basename() const {
-	unsigned long long pos = _path.find_last_of(DELIM);
-	return pos != std::string::npos ? _path.substr(pos + 1, _path.length()) : "";
-}
-
-std::string Path::Stem() const {
-	std::string basename = Basename();
-	unsigned long long pos = basename.find_last_of(".");
-	return pos != std::string::npos ? basename.substr(0, pos) : "";
-}
-
-std::string Path::Extension() const {
-	std::string basename = Basename();
-	unsigned long long pos = basename.find_last_of(".");
-	return pos != std::string::npos ? basename.substr(pos + 1, basename.length()) : "";
-}
-
-Path Path::GetParent() const {
-	return _path.substr(0, _path.find_last_of(DELIM));
-}
-
-void Path::SetPath(const std::string& path) {
-	_path = path;
-}
-
-std::string Path::GetPath() const {
-	return _path;
-}
-
-Path GetDotPath(void) {
-	std::string ret = "";
-#ifdef WIN32
-	std::wstring buf(MAX_PATH, '\0');
-	if (SHGetFolderPathAndSubDirW(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, CONFIG_WDIR, &buf.front()) ==
-	    S_OK) {
-		buf.resize(buf.find('\0'));
-		ret += Strings::ToUtf8String(buf);
-	}
-#elif defined(MACOSX)
-	ret += osx::GetApplicationSupportDirectory();
-	ret += DELIM CONFIG_DIR;
-#else // just assume POSIX
-	if (getenv("HOME") != NULL)
-		ret += getenv("HOME");
-#	ifdef __linux__
-	else
-		ret += getpwuid(getuid())->pw_dir;
-#	endif // __linux__
-	if (!ret.empty())
-		ret += DELIM ".config" DELIM CONFIG_DIR;
-#endif     // !WIN32 && !MACOSX
-	return ret;
-}
-
-Path GetConfigPath(void) {
-	std::string ret = "";
-	ret += GetDotPath().GetPath();
-	if (!ret.empty())
-		ret += DELIM CONFIG_NAME;
-	return ret;
-}
-
-Path GetAnimeDBPath(void) {
-	std::string ret = "";
-	ret += GetDotPath().GetPath();
-	if (!ret.empty())
-		ret += DELIM "anime" DELIM "db.json";
-	return ret;
-}
-
-} // namespace Filesystem
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/http.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,71 @@
+#include "core/http.h"
+#include "core/session.h"
+#include <QByteArray>
+#include <QMessageBox>
+#include <curl/curl.h>
+#include <string>
+#include <vector>
+
+namespace HTTP {
+
+static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) {
+	reinterpret_cast<QByteArray*>(userdata)->append(reinterpret_cast<char*>(contents), size * nmemb);
+	return size * nmemb;
+}
+
+QByteArray Get(std::string url, std::vector<std::string> headers) {
+	struct curl_slist* list = NULL;
+	QByteArray userdata;
+
+	CURL* curl = curl_easy_init();
+	if (curl) {
+		for (const std::string& h : headers) {
+			list = curl_slist_append(list, h.c_str());
+		}
+		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
+		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
+		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
+		/* Use system certs... useful on Windows. */
+		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
+		CURLcode res = curl_easy_perform(curl);
+		session.IncrementRequests();
+		curl_easy_cleanup(curl);
+		if (res != CURLE_OK) {
+			QMessageBox box(QMessageBox::Icon::Critical, "",
+			                QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+			box.exec();
+		}
+	}
+	return userdata;
+}
+
+QByteArray Post(std::string url, std::string data, std::vector<std::string> headers) {
+	struct curl_slist* list = NULL;
+	QByteArray userdata;
+
+	CURL* curl = curl_easy_init();
+	if (curl) {
+		for (const std::string& h : headers) {
+			list = curl_slist_append(list, h.c_str());
+		}
+		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
+		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
+		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
+		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
+		/* Use system certs... useful on Windows. */
+		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
+		CURLcode res = curl_easy_perform(curl);
+		session.IncrementRequests();
+		curl_easy_cleanup(curl);
+		if (res != CURLE_OK) {
+			QMessageBox box(QMessageBox::Icon::Critical, "",
+			                QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+			box.exec();
+		}
+	}
+	return userdata;
+}
+
+} // namespace HTTP
--- a/src/core/http.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-#include "core/http.h"
-#include "core/session.h"
-#include <QByteArray>
-#include <QMessageBox>
-#include <curl/curl.h>
-#include <string>
-#include <vector>
-
-namespace HTTP {
-
-static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) {
-	reinterpret_cast<QByteArray*>(userdata)->append(reinterpret_cast<char*>(contents), size * nmemb);
-	return size * nmemb;
-}
-
-QByteArray Get(std::string url, std::vector<std::string> headers) {
-	struct curl_slist* list = NULL;
-	QByteArray userdata;
-
-	CURL* curl = curl_easy_init();
-	if (curl) {
-		for (const std::string& h : headers) {
-			list = curl_slist_append(list, h.c_str());
-		}
-		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
-		/* Use system certs... useful on Windows. */
-		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
-		CURLcode res = curl_easy_perform(curl);
-		session.IncrementRequests();
-		curl_easy_cleanup(curl);
-		if (res != CURLE_OK) {
-			QMessageBox box(QMessageBox::Icon::Critical, "",
-			                QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
-			box.exec();
-		}
-	}
-	return userdata;
-}
-
-QByteArray Post(std::string url, std::string data, std::vector<std::string> headers) {
-	struct curl_slist* list = NULL;
-	QByteArray userdata;
-
-	CURL* curl = curl_easy_init();
-	if (curl) {
-		for (const std::string& h : headers) {
-			list = curl_slist_append(list, h.c_str());
-		}
-		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
-		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
-		/* Use system certs... useful on Windows. */
-		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
-		CURLcode res = curl_easy_perform(curl);
-		session.IncrementRequests();
-		curl_easy_cleanup(curl);
-		if (res != CURLE_OK) {
-			QMessageBox box(QMessageBox::Icon::Critical, "",
-			                QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
-			box.exec();
-		}
-	}
-	return userdata;
-}
-
-} // namespace HTTP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/json.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,33 @@
+#include "core/json.h"
+
+namespace JSON {
+
+std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, std::string def) {
+	if (json.contains(ptr) && json[ptr].is_string())
+		return json[ptr].get<std::string>();
+	else
+		return def;
+}
+
+int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, int def) {
+	if (json.contains(ptr) && json[ptr].is_number())
+		return json[ptr].get<int>();
+	else
+		return def;
+}
+
+bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, bool def) {
+	if (json.contains(ptr) && json[ptr].is_boolean())
+		return json[ptr].get<bool>();
+	else
+		return def;
+}
+
+double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, double def) {
+	if (json.contains(ptr) && json[ptr].is_number())
+		return json[ptr].get<double>();
+	else
+		return def;
+}
+
+} // namespace JSON
--- a/src/core/json.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-#include "core/json.h"
-
-namespace JSON {
-
-std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, std::string def) {
-	if (json.contains(ptr) && json[ptr].is_string())
-		return json[ptr].get<std::string>();
-	else
-		return def;
-}
-
-int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, int def) {
-	if (json.contains(ptr) && json[ptr].is_number())
-		return json[ptr].get<int>();
-	else
-		return def;
-}
-
-bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, bool def) {
-	if (json.contains(ptr) && json[ptr].is_boolean())
-		return json[ptr].get<bool>();
-	else
-		return def;
-}
-
-double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, double def) {
-	if (json.contains(ptr) && json[ptr].is_number())
-		return json[ptr].get<double>();
-	else
-		return def;
-}
-
-} // namespace JSON
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/strings.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,114 @@
+/**
+ * strings.cpp: Useful functions for manipulating strings
+ **/
+#include "core/strings.h"
+#include <QByteArray>
+#include <QString>
+#include <algorithm>
+#include <cctype>
+#include <codecvt>
+#include <locale>
+#include <string>
+#include <vector>
+
+namespace Strings {
+
+std::string Implode(const std::vector<std::string>& vector, const std::string& delimiter) {
+	if (vector.size() < 1)
+		return "-";
+	std::string out = "";
+	for (unsigned long long i = 0; i < vector.size(); i++) {
+		out.append(vector.at(i));
+		if (i < vector.size() - 1)
+			out.append(delimiter);
+	}
+	return out;
+}
+
+std::string ReplaceAll(const std::string& string, const std::string& find, const std::string& replace) {
+	std::string result;
+	size_t pos, find_len = find.size(), from = 0;
+	while ((pos = string.find(find, from)) != std::string::npos) {
+		result.append(string, from, pos - from);
+		result.append(replace);
+		from = pos + find_len;
+	}
+	result.append(string, from, std::string::npos);
+	return result;
+}
+
+/* this function probably fucks your RAM but whatevs */
+std::string SanitizeLineEndings(const std::string& string) {
+	std::string result(string);
+	result = ReplaceAll(result, "\r\n", "\n");
+	result = ReplaceAll(result, "<br>", "\n");
+	result = ReplaceAll(result, "\n\n\n", "\n\n");
+	return result;
+}
+
+std::string RemoveHtmlTags(const std::string& string) {
+	std::string html(string);
+	while (html.find("<") != std::string::npos) {
+		auto startpos = html.find("<");
+		auto endpos = html.find(">") + 1;
+
+		if (endpos != std::string::npos) {
+			html.erase(startpos, endpos - startpos);
+		}
+	}
+	return html;
+}
+
+std::string TextifySynopsis(const std::string& string) {
+	return RemoveHtmlTags(SanitizeLineEndings(string));
+}
+
+/* these functions suck for i18n!...
+    but we only use them with JSON
+    stuff anyway */
+std::string ToUpper(const std::string& string) {
+	std::string result(string);
+	std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); });
+	return result;
+}
+
+std::string ToLower(const std::string& string) {
+	std::string result(string);
+	std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::tolower(c); });
+	return result;
+}
+
+std::wstring ToWstring(const std::string& string) {
+	std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
+	return converter.from_bytes(string);
+}
+
+std::wstring ToWstring(const QString& string) {
+	std::wstring arr(string.size(), L'\0');
+	string.toWCharArray(&arr.front());
+	return arr;
+}
+
+std::string ToUtf8String(const std::wstring& wstring) {
+	std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
+	return converter.to_bytes(wstring);
+}
+
+std::string ToUtf8String(const QString& string) {
+	QByteArray ba = string.toUtf8();
+	return std::string(ba.constData(), ba.size());
+}
+
+std::string ToUtf8String(const QByteArray& ba) {
+	return std::string(ba.constData(), ba.size());
+}
+
+QString ToQString(const std::string& string) {
+	return QString::fromUtf8(string.c_str(), string.length());
+}
+
+QString ToQString(const std::wstring& wstring) {
+	return QString::fromWCharArray(wstring.c_str(), wstring.length());
+}
+
+} // namespace Strings
--- a/src/core/strings.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-/**
- * strings.cpp: Useful functions for manipulating strings
- **/
-#include "core/strings.h"
-#include <QByteArray>
-#include <QString>
-#include <algorithm>
-#include <cctype>
-#include <codecvt>
-#include <locale>
-#include <string>
-#include <vector>
-
-namespace Strings {
-
-std::string Implode(const std::vector<std::string>& vector, const std::string& delimiter) {
-	if (vector.size() < 1)
-		return "-";
-	std::string out = "";
-	for (unsigned long long i = 0; i < vector.size(); i++) {
-		out.append(vector.at(i));
-		if (i < vector.size() - 1)
-			out.append(delimiter);
-	}
-	return out;
-}
-
-std::string ReplaceAll(const std::string& string, const std::string& find, const std::string& replace) {
-	std::string result;
-	size_t pos, find_len = find.size(), from = 0;
-	while ((pos = string.find(find, from)) != std::string::npos) {
-		result.append(string, from, pos - from);
-		result.append(replace);
-		from = pos + find_len;
-	}
-	result.append(string, from, std::string::npos);
-	return result;
-}
-
-/* this function probably fucks your RAM but whatevs */
-std::string SanitizeLineEndings(const std::string& string) {
-	std::string result(string);
-	result = ReplaceAll(result, "\r\n", "\n");
-	result = ReplaceAll(result, "<br>", "\n");
-	result = ReplaceAll(result, "\n\n\n", "\n\n");
-	return result;
-}
-
-std::string RemoveHtmlTags(const std::string& string) {
-	std::string html(string);
-	while (html.find("<") != std::string::npos) {
-		auto startpos = html.find("<");
-		auto endpos = html.find(">") + 1;
-
-		if (endpos != std::string::npos) {
-			html.erase(startpos, endpos - startpos);
-		}
-	}
-	return html;
-}
-
-std::string TextifySynopsis(const std::string& string) {
-	return RemoveHtmlTags(SanitizeLineEndings(string));
-}
-
-/* these functions suck for i18n!...
-    but we only use them with JSON
-    stuff anyway */
-std::string ToUpper(const std::string& string) {
-	std::string result(string);
-	std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); });
-	return result;
-}
-
-std::string ToLower(const std::string& string) {
-	std::string result(string);
-	std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::tolower(c); });
-	return result;
-}
-
-std::wstring ToWstring(const std::string& string) {
-	std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
-	return converter.from_bytes(string);
-}
-
-std::wstring ToWstring(const QString& string) {
-	std::wstring arr(string.size(), L'\0');
-	string.toWCharArray(&arr.front());
-	return arr;
-}
-
-std::string ToUtf8String(const std::wstring& wstring) {
-	std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
-	return converter.to_bytes(wstring);
-}
-
-std::string ToUtf8String(const QString& string) {
-	QByteArray ba = string.toUtf8();
-	return std::string(ba.constData(), ba.size());
-}
-
-std::string ToUtf8String(const QByteArray& ba) {
-	return std::string(ba.constData(), ba.size());
-}
-
-QString ToQString(const std::string& string) {
-	return QString::fromUtf8(string.c_str(), string.length());
-}
-
-QString ToQString(const std::wstring& wstring) {
-	return QString::fromWCharArray(wstring.c_str(), wstring.length());
-}
-
-} // namespace Strings
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/time.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,64 @@
+#include "core/time.h"
+#include <cassert>
+#include <cmath>
+#include <cstdint>
+#include <ctime>
+#include <string>
+
+namespace Time {
+
+Duration::Duration(int64_t l) {
+	length = l;
+}
+
+std::string Duration::AsRelativeString() {
+	std::string result;
+
+	auto get = [](int64_t val, const std::string& s, const std::string& p) {
+		return std::to_string(val) + " " + (val == 1 ? s : p);
+	};
+
+	if (InSeconds() < 60)
+		result = get(InSeconds(), "second", "seconds");
+	else if (InMinutes() < 60)
+		result = get(InMinutes(), "minute", "minutes");
+	else if (InHours() < 24)
+		result = get(InHours(), "hour", "hours");
+	else if (InDays() < 28)
+		result = get(InDays(), "day", "days");
+	else if (InDays() < 365)
+		result = get(InDays() / 30, "month", "months");
+	else
+		result = get(InDays() / 365, "year", "years");
+
+	if (length < 0)
+		result = "In " + result;
+	else
+		result += " ago";
+
+	return result;
+}
+
+int64_t Duration::InSeconds() {
+	return length;
+}
+
+int64_t Duration::InMinutes() {
+	return std::llround(static_cast<double>(length) / 60.0);
+}
+
+int64_t Duration::InHours() {
+	return std::llround(static_cast<double>(length) / 3600.0);
+}
+
+int64_t Duration::InDays() {
+	return std::llround(static_cast<double>(length) / 86400.0);
+}
+
+int64_t GetSystemTime() {
+	assert(sizeof(int64_t) >= sizeof(time_t));
+	time_t t = std::time(nullptr);
+	return *reinterpret_cast<int64_t*>(&t);
+}
+
+} // namespace Time
\ No newline at end of file
--- a/src/core/time.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-#include "core/time.h"
-#include <cassert>
-#include <cmath>
-#include <cstdint>
-#include <ctime>
-#include <string>
-
-namespace Time {
-
-Duration::Duration(int64_t l) {
-	length = l;
-}
-
-std::string Duration::AsRelativeString() {
-	std::string result;
-
-	auto get = [](int64_t val, const std::string& s, const std::string& p) {
-		return std::to_string(val) + " " + (val == 1 ? s : p);
-	};
-
-	if (InSeconds() < 60)
-		result = get(InSeconds(), "second", "seconds");
-	else if (InMinutes() < 60)
-		result = get(InMinutes(), "minute", "minutes");
-	else if (InHours() < 24)
-		result = get(InHours(), "hour", "hours");
-	else if (InDays() < 28)
-		result = get(InDays(), "day", "days");
-	else if (InDays() < 365)
-		result = get(InDays() / 30, "month", "months");
-	else
-		result = get(InDays() / 365, "year", "years");
-
-	if (length < 0)
-		result = "In " + result;
-	else
-		result += " ago";
-
-	return result;
-}
-
-int64_t Duration::InSeconds() {
-	return length;
-}
-
-int64_t Duration::InMinutes() {
-	return std::llround(static_cast<double>(length) / 60.0);
-}
-
-int64_t Duration::InHours() {
-	return std::llround(static_cast<double>(length) / 3600.0);
-}
-
-int64_t Duration::InDays() {
-	return std::llround(static_cast<double>(length) / 86400.0);
-}
-
-int64_t GetSystemTime() {
-	assert(sizeof(int64_t) >= sizeof(time_t));
-	time_t t = std::time(nullptr);
-	return *reinterpret_cast<int64_t*>(&t);
-}
-
-} // namespace Time
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dark_theme.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,100 @@
+#include "core/config.h"
+#include "core/session.h"
+#include <QApplication>
+#include <QFile>
+#include <QTextStream>
+#ifdef MACOSX
+#	include "sys/osx/dark_theme.h"
+#else
+#	include "sys/win32/dark_theme.h"
+#endif
+
+namespace DarkTheme {
+
+bool IsInDarkMode() {
+	if (session.config.theme != Themes::OS)
+		return (session.config.theme == Themes::DARK);
+#ifdef MACOSX
+	if (osx::DarkThemeAvailable()) {
+		if (osx::IsInDarkTheme()) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+#elif defined(WIN32)
+	if (win32::DarkThemeAvailable()) {
+		if (win32::IsInDarkTheme()) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+#endif
+	return (session.config.theme == Themes::DARK);
+}
+
+/* this function is private, and should stay that way */
+void SetStyleSheet(enum Themes theme) {
+	switch (theme) {
+		case Themes::DARK: {
+			QFile f(":qdarkstyle/dark/darkstyle.qss");
+			if (!f.exists())
+				return; // fail
+			f.open(QFile::ReadOnly | QFile::Text);
+			QTextStream ts(&f);
+			qApp->setStyleSheet(ts.readAll());
+			break;
+		}
+		default: qApp->setStyleSheet(""); break;
+	}
+}
+
+void SetToDarkTheme() {
+	/* macOS >= 10.14 has its own global dark theme,
+	   use it :) */
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		osx::SetToDarkTheme();
+	else
+#endif
+		SetStyleSheet(Themes::DARK);
+}
+
+void SetToLightTheme() {
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		osx::SetToLightTheme();
+	else
+#endif
+		SetStyleSheet(Themes::LIGHT);
+}
+
+enum Themes GetCurrentOSTheme() {
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		return osx::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
+#elif defined(WIN32)
+	if (win32::DarkThemeAvailable())
+		return win32::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
+#endif
+	/* Currently OS detection only supports Windows and macOS.
+	   Please don't be shy if you're willing to port it to other OSes
+	   (or desktop environments, or window managers) */
+	return Themes::LIGHT;
+}
+
+void SetTheme(enum Themes theme) {
+	switch (theme) {
+		case Themes::LIGHT: SetToLightTheme(); break;
+		case Themes::DARK: SetToDarkTheme(); break;
+		case Themes::OS:
+			if (GetCurrentOSTheme() == Themes::LIGHT)
+				SetToLightTheme();
+			else
+				SetToDarkTheme();
+			break;
+	}
+}
+
+} // namespace DarkTheme
--- a/src/gui/dark_theme.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-#include "core/config.h"
-#include "core/session.h"
-#include <QApplication>
-#include <QFile>
-#include <QTextStream>
-#ifdef MACOSX
-#	include "sys/osx/dark_theme.h"
-#else
-#	include "sys/win32/dark_theme.h"
-#endif
-
-namespace DarkTheme {
-
-bool IsInDarkMode() {
-	if (session.config.theme != Themes::OS)
-		return (session.config.theme == Themes::DARK);
-#ifdef MACOSX
-	if (osx::DarkThemeAvailable()) {
-		if (osx::IsInDarkTheme()) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-#elif defined(WIN32)
-	if (win32::DarkThemeAvailable()) {
-		if (win32::IsInDarkTheme()) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-#endif
-	return (session.config.theme == Themes::DARK);
-}
-
-/* this function is private, and should stay that way */
-void SetStyleSheet(enum Themes theme) {
-	switch (theme) {
-		case Themes::DARK: {
-			QFile f(":qdarkstyle/dark/darkstyle.qss");
-			if (!f.exists())
-				return; // fail
-			f.open(QFile::ReadOnly | QFile::Text);
-			QTextStream ts(&f);
-			qApp->setStyleSheet(ts.readAll());
-			break;
-		}
-		default: qApp->setStyleSheet(""); break;
-	}
-}
-
-void SetToDarkTheme() {
-	/* macOS >= 10.14 has its own global dark theme,
-	   use it :) */
-#if MACOSX
-	if (osx::DarkThemeAvailable())
-		osx::SetToDarkTheme();
-	else
-#endif
-		SetStyleSheet(Themes::DARK);
-}
-
-void SetToLightTheme() {
-#if MACOSX
-	if (osx::DarkThemeAvailable())
-		osx::SetToLightTheme();
-	else
-#endif
-		SetStyleSheet(Themes::LIGHT);
-}
-
-enum Themes GetCurrentOSTheme() {
-#if MACOSX
-	if (osx::DarkThemeAvailable())
-		return osx::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
-#elif defined(WIN32)
-	if (win32::DarkThemeAvailable())
-		return win32::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
-#endif
-	/* Currently OS detection only supports Windows and macOS.
-	   Please don't be shy if you're willing to port it to other OSes
-	   (or desktop environments, or window managers) */
-	return Themes::LIGHT;
-}
-
-void SetTheme(enum Themes theme) {
-	switch (theme) {
-		case Themes::LIGHT: SetToLightTheme(); break;
-		case Themes::DARK: SetToDarkTheme(); break;
-		case Themes::OS:
-			if (GetCurrentOSTheme() == Themes::LIGHT)
-				SetToLightTheme();
-			else
-				SetToDarkTheme();
-			break;
-	}
-}
-
-} // namespace DarkTheme
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/about.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,137 @@
+#include "gui/dialog/about.h"
+#include "core/json.h"
+#include "core/version.h"
+#include "gui/widgets/text.h"
+#include "pugixml.hpp"
+#include <QFont>
+#include <QHBoxLayout>
+#include <QTextBrowser>
+#include <QTextCharFormat>
+#include <QTextCursor>
+#include <curl/curl.h>
+
+#define CONCAT_VERSION_NX(major, minor, patch) ("v" #major "." #minor "." #patch)
+
+#define CONCAT_VERSION(major, minor, patch) CONCAT_VERSION_NX(major, minor, patch)
+
+#define SET_TITLE_FONT(font, format, cursor) \
+	{ \
+		QFont fnt; \
+		fnt.setPixelSize(16); \
+		format.setFont(fnt); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_PARAGRAPH_FONT(font, format, cursor) \
+	{ \
+		QFont fnt; \
+		fnt.setPixelSize(12); \
+		format.setFont(fnt); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_FONT_BOLD(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setBold(true); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define UNSET_FONT_BOLD(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setBold(false); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_FORMAT_HYPERLINK(format, cursor, link) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setUnderline(true); \
+		format.setFont(font); \
+		format.setAnchor(true); \
+		format.setAnchorHref(link); \
+		cursor.setCharFormat(format); \
+	}
+#define UNSET_FORMAT_HYPERLINK(format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setUnderline(false); \
+		format.setFont(font); \
+		format.setAnchor(false); \
+		format.setAnchorHref(""); \
+		cursor.setCharFormat(format); \
+	}
+
+AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) {
+	setWindowTitle(tr("About Minori"));
+	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
+	QHBoxLayout* layout = new QHBoxLayout(this);
+
+	QPalette pal = QPalette();
+	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
+	setPalette(pal);
+	setAutoFillBackground(true);
+
+	QFont font;
+	QTextCharFormat format;
+	QTextBrowser* paragraph = new QTextBrowser(this);
+	paragraph->setOpenExternalLinks(true);
+	paragraph->setFrameShape(QFrame::NoFrame);
+	paragraph->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	paragraph->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	QTextCursor cursor = paragraph->textCursor();
+	SET_TITLE_FONT(font, format, cursor);
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText("Minori");
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(" " MINORI_VERSION);
+	SET_PARAGRAPH_FONT(font, format, cursor);
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Author:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	cursor.insertText(tr("Paper"));
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Third party components:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	SET_FORMAT_HYPERLINK(format, cursor, "https://github.com/nlohmann/json");
+	cursor.insertText(tr("JSON for Modern C++ ") + CONCAT_VERSION(NLOHMANN_JSON_VERSION_MAJOR,
+	                                                              NLOHMANN_JSON_VERSION_MINOR,
+	                                                              NLOHMANN_JSON_VERSION_PATCH));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertText(", ");
+	{
+		curl_version_info_data* data = curl_version_info(CURLVERSION_NOW);
+		SET_FORMAT_HYPERLINK(format, cursor, "https://curl.se/");
+		cursor.insertText(tr("libcurl v") + data->version);
+		UNSET_FORMAT_HYPERLINK(format, cursor);
+		cursor.insertText(", ");
+	}
+	SET_FORMAT_HYPERLINK(format, cursor, "https://p.yusukekamiyamane.com/");
+	cursor.insertText(tr("Fugue Icons ") + CONCAT_VERSION(3, 5, 6));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertText(", ");
+	SET_FORMAT_HYPERLINK(format, cursor, "https://pugixml.org/");
+	cursor.insertText(tr("pugixml v") + QString::number(PUGIXML_VERSION / 1000) + "." +
+	                  QString::number(PUGIXML_VERSION / 10 % 100) + "." + QString::number(PUGIXML_VERSION % 10));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertText(", ");
+	SET_FORMAT_HYPERLINK(format, cursor, "https://github.com/erengy/anitomy");
+	cursor.insertText(tr("Anitomy"));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Links:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	layout->addWidget(paragraph);
+}
--- a/src/gui/dialog/about.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
-#include "gui/dialog/about.h"
-#include "core/json.h"
-#include "core/version.h"
-#include "gui/widgets/text.h"
-#include "pugixml.hpp"
-#include <QFont>
-#include <QHBoxLayout>
-#include <QTextBrowser>
-#include <QTextCharFormat>
-#include <QTextCursor>
-#include <curl/curl.h>
-
-#define CONCAT_VERSION_NX(major, minor, patch) ("v" #major "." #minor "." #patch)
-
-#define CONCAT_VERSION(major, minor, patch) CONCAT_VERSION_NX(major, minor, patch)
-
-#define SET_TITLE_FONT(font, format, cursor) \
-	{ \
-		QFont fnt; \
-		fnt.setPixelSize(16); \
-		format.setFont(fnt); \
-		cursor.setCharFormat(format); \
-	}
-
-#define SET_PARAGRAPH_FONT(font, format, cursor) \
-	{ \
-		QFont fnt; \
-		fnt.setPixelSize(12); \
-		format.setFont(fnt); \
-		cursor.setCharFormat(format); \
-	}
-
-#define SET_FONT_BOLD(font, format, cursor) \
-	{ \
-		font = cursor.charFormat().font(); \
-		font.setBold(true); \
-		format.setFont(font); \
-		cursor.setCharFormat(format); \
-	}
-
-#define UNSET_FONT_BOLD(font, format, cursor) \
-	{ \
-		font = cursor.charFormat().font(); \
-		font.setBold(false); \
-		format.setFont(font); \
-		cursor.setCharFormat(format); \
-	}
-
-#define SET_FORMAT_HYPERLINK(format, cursor, link) \
-	{ \
-		font = cursor.charFormat().font(); \
-		font.setUnderline(true); \
-		format.setFont(font); \
-		format.setAnchor(true); \
-		format.setAnchorHref(link); \
-		cursor.setCharFormat(format); \
-	}
-#define UNSET_FORMAT_HYPERLINK(format, cursor) \
-	{ \
-		font = cursor.charFormat().font(); \
-		font.setUnderline(false); \
-		format.setFont(font); \
-		format.setAnchor(false); \
-		format.setAnchorHref(""); \
-		cursor.setCharFormat(format); \
-	}
-
-AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) {
-	setWindowTitle(tr("About Minori"));
-	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
-	QHBoxLayout* layout = new QHBoxLayout(this);
-
-	QPalette pal = QPalette();
-	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
-	setPalette(pal);
-	setAutoFillBackground(true);
-
-	QFont font;
-	QTextCharFormat format;
-	QTextBrowser* paragraph = new QTextBrowser(this);
-	paragraph->setOpenExternalLinks(true);
-	paragraph->setFrameShape(QFrame::NoFrame);
-	paragraph->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	paragraph->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	QTextCursor cursor = paragraph->textCursor();
-	SET_TITLE_FONT(font, format, cursor);
-	SET_FONT_BOLD(font, format, cursor);
-	cursor.insertText("Minori");
-	UNSET_FONT_BOLD(font, format, cursor);
-	cursor.insertText(" " MINORI_VERSION);
-	SET_PARAGRAPH_FONT(font, format, cursor);
-	cursor.insertBlock();
-	cursor.insertBlock();
-	SET_FONT_BOLD(font, format, cursor);
-	cursor.insertText(tr("Author:"));
-	UNSET_FONT_BOLD(font, format, cursor);
-	cursor.insertBlock();
-	cursor.insertText(tr("Paper"));
-	cursor.insertBlock();
-	cursor.insertBlock();
-	SET_FONT_BOLD(font, format, cursor);
-	cursor.insertText(tr("Third party components:"));
-	UNSET_FONT_BOLD(font, format, cursor);
-	cursor.insertBlock();
-	SET_FORMAT_HYPERLINK(format, cursor, "https://github.com/nlohmann/json");
-	cursor.insertText(tr("JSON for Modern C++ ") + CONCAT_VERSION(NLOHMANN_JSON_VERSION_MAJOR,
-	                                                              NLOHMANN_JSON_VERSION_MINOR,
-	                                                              NLOHMANN_JSON_VERSION_PATCH));
-	UNSET_FORMAT_HYPERLINK(format, cursor);
-	cursor.insertText(", ");
-	{
-		curl_version_info_data* data = curl_version_info(CURLVERSION_NOW);
-		SET_FORMAT_HYPERLINK(format, cursor, "https://curl.se/");
-		cursor.insertText(tr("libcurl v") + data->version);
-		UNSET_FORMAT_HYPERLINK(format, cursor);
-		cursor.insertText(", ");
-	}
-	SET_FORMAT_HYPERLINK(format, cursor, "https://p.yusukekamiyamane.com/");
-	cursor.insertText(tr("Fugue Icons ") + CONCAT_VERSION(3, 5, 6));
-	UNSET_FORMAT_HYPERLINK(format, cursor);
-	cursor.insertText(", ");
-	SET_FORMAT_HYPERLINK(format, cursor, "https://pugixml.org/");
-	cursor.insertText(tr("pugixml v") + QString::number(PUGIXML_VERSION / 1000) + "." +
-	                  QString::number(PUGIXML_VERSION / 10 % 100) + "." + QString::number(PUGIXML_VERSION % 10));
-	UNSET_FORMAT_HYPERLINK(format, cursor);
-	cursor.insertText(", ");
-	SET_FORMAT_HYPERLINK(format, cursor, "https://github.com/erengy/anitomy");
-	cursor.insertText(tr("Anitomy"));
-	UNSET_FORMAT_HYPERLINK(format, cursor);
-	cursor.insertBlock();
-	cursor.insertBlock();
-	SET_FONT_BOLD(font, format, cursor);
-	cursor.insertText(tr("Links:"));
-	UNSET_FONT_BOLD(font, format, cursor);
-	cursor.insertBlock();
-	layout->addWidget(paragraph);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/information.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,281 @@
+#include "gui/dialog/information.h"
+#include "core/anime.h"
+#include "core/anime_db.h"
+#include "core/array.h"
+#include "core/strings.h"
+#include "gui/pages/anime_list.h"
+#include "gui/translate/anime.h"
+#include "gui/widgets/anime_info.h"
+#include "gui/widgets/optional_date.h"
+#include "gui/widgets/poster.h"
+#include "gui/widgets/text.h"
+#include "gui/window.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDateEdit>
+#include <QDebug>
+#include <QDialogButtonBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPlainTextEdit>
+#include <QSpinBox>
+#include <QStringList>
+#include <QTextStream>
+#include <QVBoxLayout>
+#include <functional>
+
+/* TODO: Taiga disables rendering of the tab widget entirely when the anime is not part of a list,
+   which sucks. Think of a better way to implement this later. */
+void InformationDialog::SaveData() {
+	Anime::Anime& anime = Anime::db.items[id];
+	anime.SetUserProgress(progress);
+	anime.SetUserScore(score);
+	anime.SetUserIsRewatching(rewatching);
+	anime.SetUserStatus(status);
+	anime.SetUserNotes(notes);
+	anime.SetUserDateStarted(started);
+	anime.SetUserDateCompleted(completed);
+}
+
+InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
+    : QDialog(parent) {
+	setFixedSize(842, 613);
+	setWindowTitle(tr("Anime Information"));
+	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
+
+	{
+		QPalette pal(palette());
+		pal.setColor(QPalette::Window, pal.color(QPalette::Base));
+		setPalette(pal);
+	}
+
+	QWidget* widget = new QWidget(this);
+
+	/* "sidebar", includes... just the anime image :) */
+	QWidget* sidebar = new QWidget(widget);
+	QVBoxLayout* sidebar_layout = new QVBoxLayout(sidebar);
+	Poster* poster = new Poster(anime.GetId(), sidebar);
+	sidebar_layout->addWidget(poster);
+	sidebar_layout->setContentsMargins(0, 0, 0, 0);
+	sidebar_layout->addStretch();
+
+	/* main widget */
+	QWidget* main_widget = new QWidget(widget);
+
+	main_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+	id = anime.GetId();
+	/* anime title header text */
+	TextWidgets::Title* anime_title =
+	    new TextWidgets::Title(Strings::ToQString(anime.GetUserPreferredTitle()), main_widget);
+
+	/* tabbed widget */
+	QTabWidget* tabbed_widget = new QTabWidget(main_widget);
+	tabbed_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+
+	/* main info tab */
+	AnimeInfoWidget* main_information_widget = new AnimeInfoWidget(anime, tabbed_widget);
+
+	{
+		QPalette pal(main_information_widget->palette());
+		pal.setColor(QPalette::Base, pal.color(QPalette::Window));
+		main_information_widget->setPalette(pal);
+	}
+
+	QWidget* settings_widget = new QWidget(tabbed_widget);
+	settings_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
+	QVBoxLayout* settings_layout = new QVBoxLayout(settings_widget);
+	settings_layout->addWidget(new TextWidgets::Header(tr("Anime list"), settings_widget));
+
+	QWidget* sg_anime_list_content = new QWidget(settings_widget);
+
+	QVBoxLayout* al_layout = new QVBoxLayout(sg_anime_list_content);
+	al_layout->setSpacing(5);
+	al_layout->setContentsMargins(12, 0, 0, 0);
+
+#define LAYOUT_HORIZ_SPACING 25
+#define LAYOUT_VERT_SPACING  5
+#define LAYOUT_ITEM_WIDTH    175
+/* Creates a subsection that takes up whatever space is necessary */
+#define CREATE_FULL_WIDTH_SUBSECTION(x) \
+	{ \
+		QWidget* subsection = new QWidget(section); \
+		subsection->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); \
+		QVBoxLayout* subsection_layout = new QVBoxLayout(subsection); \
+		subsection_layout->setSpacing(LAYOUT_VERT_SPACING); \
+		subsection_layout->setContentsMargins(0, 0, 0, 0); \
+		x; \
+		layout->addWidget(subsection, 0, Qt::AlignBottom); \
+	}
+
+/* Creates a section in the parent `a` */
+#define CREATE_FULL_WIDTH_SECTION(a, x) \
+	{ \
+		QWidget* section = new QWidget(a); \
+		QHBoxLayout* layout = new QHBoxLayout(section); \
+		layout->setSpacing(LAYOUT_HORIZ_SPACING); \
+		layout->setContentsMargins(0, 0, 0, 0); \
+		x; \
+		a->layout()->addWidget(section); \
+	}
+
+/* Creates a subsection with a width of 175 */
+#define CREATE_SUBSECTION(x) CREATE_FULL_WIDTH_SUBSECTION(x subsection->setFixedWidth(LAYOUT_ITEM_WIDTH);)
+/* Creates a section in the parent `a` */
+#define CREATE_SECTION(a, x) CREATE_FULL_WIDTH_SECTION(a, x layout->addStretch();)
+
+	CREATE_SECTION(sg_anime_list_content, {
+		/* Episodes watched section */
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Episodes watched:"), subsection));
+
+			QSpinBox* spin_box = new QSpinBox(subsection);
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { progress = i; });
+			spin_box->setRange(0, anime.GetEpisodes());
+			spin_box->setSingleStep(1);
+			spin_box->setValue(progress = anime.GetUserProgress());
+			subsection_layout->addWidget(spin_box);
+		});
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr(" "), subsection));
+
+			QCheckBox* checkbox = new QCheckBox(tr("Rewatching"));
+			connect(checkbox, QOverload<int>::of(&QCheckBox::stateChanged), this,
+			        [this](int state) { rewatching = (state == Qt::Checked); });
+			checkbox->setCheckState(anime.GetUserIsRewatching() ? Qt::Checked : Qt::Unchecked);
+			subsection_layout->addWidget(checkbox);
+		});
+	});
+	CREATE_SECTION(sg_anime_list_content, {
+		/* Status & score section */
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Status:"), subsection));
+
+			QStringList string_list;
+			for (unsigned int i = 0; i < ARRAYSIZE(Anime::ListStatuses); i++)
+				string_list.append(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])));
+
+			QComboBox* combo_box = new QComboBox(subsection);
+			combo_box->addItems(string_list);
+			connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+			        [this](int i) { status = Anime::ListStatuses[i]; });
+			combo_box->setCurrentIndex(static_cast<int>(status = anime.GetUserStatus()) - 1);
+			subsection_layout->addWidget(combo_box);
+		});
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Score:"), subsection));
+
+			QSpinBox* spin_box = new QSpinBox(subsection);
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { score = i; });
+			spin_box->setRange(0, 100);
+			spin_box->setSingleStep(5);
+			spin_box->setValue(score = anime.GetUserScore());
+			subsection_layout->addWidget(spin_box);
+		});
+	});
+	CREATE_FULL_WIDTH_SECTION(sg_anime_list_content, {
+		/* Notes section */
+		CREATE_FULL_WIDTH_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Notes:"), subsection));
+
+			QLineEdit* line_edit = new QLineEdit(subsection);
+			connect(line_edit, &QLineEdit::textChanged, this, [this](const QString& text) {
+				/* this sucks but I don't really want to implement anything smarter :) */
+				notes = Strings::ToUtf8String(text);
+			});
+			line_edit->setText(Strings::ToQString(notes = anime.GetUserNotes()));
+			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
+			subsection_layout->addWidget(line_edit);
+		});
+	});
+	CREATE_SECTION(sg_anime_list_content, {
+		/* Dates section */
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Date started:"), subsection));
+
+			OptionalDate* date = new OptionalDate(true, subsection);
+			connect(date, &OptionalDate::DataChanged, this,
+			        [this](bool enabled, Date date) { started = (enabled) ? date : Date(); });
+			started = anime.GetUserDateStarted();
+			if (!started.IsValid()) {
+				date->SetEnabled(false);
+				started = anime.GetAirDate();
+			}
+			date->SetDate(started);
+			subsection_layout->addWidget(date);
+		});
+		CREATE_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Date completed:"), subsection));
+
+			OptionalDate* date = new OptionalDate(true, subsection);
+			connect(date, &OptionalDate::DataChanged, this,
+			        [this](bool enabled, Date date) { completed = (enabled) ? date : Date(); });
+			completed = anime.GetUserDateCompleted();
+			if (!completed.IsValid()) {
+				date->SetEnabled(false);
+				completed = anime.GetAirDate();
+			}
+			date->SetDate(completed);
+			subsection_layout->addWidget(date);
+		});
+	});
+
+	settings_layout->addWidget(sg_anime_list_content);
+
+	settings_layout->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
+
+	QWidget* sg_local_content = new QWidget(settings_widget);
+	QVBoxLayout* sg_local_layout = new QVBoxLayout(sg_local_content);
+	sg_local_layout->setSpacing(5);
+	sg_local_layout->setContentsMargins(12, 0, 0, 0);
+
+	CREATE_FULL_WIDTH_SECTION(sg_local_content, {
+		/* Alternative titles */
+		CREATE_FULL_WIDTH_SUBSECTION({
+			subsection_layout->addWidget(new QLabel(tr("Alternative titles:"), subsection));
+
+			QLineEdit* line_edit = new QLineEdit("", subsection);
+			line_edit->setPlaceholderText(
+			    tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
+			subsection_layout->addWidget(line_edit);
+
+			QCheckBox* checkbox = new QCheckBox(tr("Use the first alternative title to search for torrents"));
+			subsection_layout->addWidget(checkbox);
+		});
+	});
+#undef CREATE_SECTION
+#undef CREATE_SUBSECTION
+#undef CREATE_FULL_WIDTH_SECTION
+#undef CREATE_FULL_WIDTH_SUBSECTION
+
+	settings_layout->addWidget(sg_local_content);
+	settings_layout->addStretch();
+
+	tabbed_widget->addTab(main_information_widget, tr("Main information"));
+	tabbed_widget->addTab(settings_widget, tr("My list and settings"));
+
+	QVBoxLayout* main_layout = new QVBoxLayout(main_widget);
+	main_layout->addWidget(anime_title);
+	main_layout->addWidget(tabbed_widget);
+	main_layout->setContentsMargins(0, 0, 0, 0);
+
+	QHBoxLayout* layout = new QHBoxLayout(widget);
+	layout->addWidget(sidebar);
+	layout->addWidget(main_widget);
+	layout->setSpacing(12);
+
+	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+	connect(button_box, &QDialogButtonBox::accepted, this, [this, accept] {
+		SaveData();
+		accept();
+		QDialog::accept();
+	});
+	connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+	QVBoxLayout* buttons_layout = new QVBoxLayout(this);
+	buttons_layout->addWidget(widget);
+	buttons_layout->addWidget(button_box, 0, Qt::AlignBottom);
+}
+
+#include "gui/dialog/moc_information.cpp"
--- a/src/gui/dialog/information.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,281 +0,0 @@
-#include "gui/dialog/information.h"
-#include "core/anime.h"
-#include "core/anime_db.h"
-#include "core/array.h"
-#include "core/strings.h"
-#include "gui/pages/anime_list.h"
-#include "gui/translate/anime.h"
-#include "gui/widgets/anime_info.h"
-#include "gui/widgets/optional_date.h"
-#include "gui/widgets/poster.h"
-#include "gui/widgets/text.h"
-#include "gui/window.h"
-#include <QCheckBox>
-#include <QComboBox>
-#include <QDateEdit>
-#include <QDebug>
-#include <QDialogButtonBox>
-#include <QLabel>
-#include <QLineEdit>
-#include <QPlainTextEdit>
-#include <QSpinBox>
-#include <QStringList>
-#include <QTextStream>
-#include <QVBoxLayout>
-#include <functional>
-
-/* TODO: Taiga disables rendering of the tab widget entirely when the anime is not part of a list,
-   which sucks. Think of a better way to implement this later. */
-void InformationDialog::SaveData() {
-	Anime::Anime& anime = Anime::db.items[id];
-	anime.SetUserProgress(progress);
-	anime.SetUserScore(score);
-	anime.SetUserIsRewatching(rewatching);
-	anime.SetUserStatus(status);
-	anime.SetUserNotes(notes);
-	anime.SetUserDateStarted(started);
-	anime.SetUserDateCompleted(completed);
-}
-
-InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
-    : QDialog(parent) {
-	setFixedSize(842, 613);
-	setWindowTitle(tr("Anime Information"));
-	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
-
-	{
-		QPalette pal(palette());
-		pal.setColor(QPalette::Window, pal.color(QPalette::Base));
-		setPalette(pal);
-	}
-
-	QWidget* widget = new QWidget(this);
-
-	/* "sidebar", includes... just the anime image :) */
-	QWidget* sidebar = new QWidget(widget);
-	QVBoxLayout* sidebar_layout = new QVBoxLayout(sidebar);
-	Poster* poster = new Poster(anime.GetId(), sidebar);
-	sidebar_layout->addWidget(poster);
-	sidebar_layout->setContentsMargins(0, 0, 0, 0);
-	sidebar_layout->addStretch();
-
-	/* main widget */
-	QWidget* main_widget = new QWidget(widget);
-
-	main_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-
-	id = anime.GetId();
-	/* anime title header text */
-	TextWidgets::Title* anime_title =
-	    new TextWidgets::Title(Strings::ToQString(anime.GetUserPreferredTitle()), main_widget);
-
-	/* tabbed widget */
-	QTabWidget* tabbed_widget = new QTabWidget(main_widget);
-	tabbed_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
-
-	/* main info tab */
-	AnimeInfoWidget* main_information_widget = new AnimeInfoWidget(anime, tabbed_widget);
-
-	{
-		QPalette pal(main_information_widget->palette());
-		pal.setColor(QPalette::Base, pal.color(QPalette::Window));
-		main_information_widget->setPalette(pal);
-	}
-
-	QWidget* settings_widget = new QWidget(tabbed_widget);
-	settings_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-
-	QVBoxLayout* settings_layout = new QVBoxLayout(settings_widget);
-	settings_layout->addWidget(new TextWidgets::Header(tr("Anime list"), settings_widget));
-
-	QWidget* sg_anime_list_content = new QWidget(settings_widget);
-
-	QVBoxLayout* al_layout = new QVBoxLayout(sg_anime_list_content);
-	al_layout->setSpacing(5);
-	al_layout->setContentsMargins(12, 0, 0, 0);
-
-#define LAYOUT_HORIZ_SPACING 25
-#define LAYOUT_VERT_SPACING  5
-#define LAYOUT_ITEM_WIDTH    175
-/* Creates a subsection that takes up whatever space is necessary */
-#define CREATE_FULL_WIDTH_SUBSECTION(x) \
-	{ \
-		QWidget* subsection = new QWidget(section); \
-		subsection->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); \
-		QVBoxLayout* subsection_layout = new QVBoxLayout(subsection); \
-		subsection_layout->setSpacing(LAYOUT_VERT_SPACING); \
-		subsection_layout->setContentsMargins(0, 0, 0, 0); \
-		x; \
-		layout->addWidget(subsection, 0, Qt::AlignBottom); \
-	}
-
-/* Creates a section in the parent `a` */
-#define CREATE_FULL_WIDTH_SECTION(a, x) \
-	{ \
-		QWidget* section = new QWidget(a); \
-		QHBoxLayout* layout = new QHBoxLayout(section); \
-		layout->setSpacing(LAYOUT_HORIZ_SPACING); \
-		layout->setContentsMargins(0, 0, 0, 0); \
-		x; \
-		a->layout()->addWidget(section); \
-	}
-
-/* Creates a subsection with a width of 175 */
-#define CREATE_SUBSECTION(x) CREATE_FULL_WIDTH_SUBSECTION(x subsection->setFixedWidth(LAYOUT_ITEM_WIDTH);)
-/* Creates a section in the parent `a` */
-#define CREATE_SECTION(a, x) CREATE_FULL_WIDTH_SECTION(a, x layout->addStretch();)
-
-	CREATE_SECTION(sg_anime_list_content, {
-		/* Episodes watched section */
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Episodes watched:"), subsection));
-
-			QSpinBox* spin_box = new QSpinBox(subsection);
-			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { progress = i; });
-			spin_box->setRange(0, anime.GetEpisodes());
-			spin_box->setSingleStep(1);
-			spin_box->setValue(progress = anime.GetUserProgress());
-			subsection_layout->addWidget(spin_box);
-		});
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr(" "), subsection));
-
-			QCheckBox* checkbox = new QCheckBox(tr("Rewatching"));
-			connect(checkbox, QOverload<int>::of(&QCheckBox::stateChanged), this,
-			        [this](int state) { rewatching = (state == Qt::Checked); });
-			checkbox->setCheckState(anime.GetUserIsRewatching() ? Qt::Checked : Qt::Unchecked);
-			subsection_layout->addWidget(checkbox);
-		});
-	});
-	CREATE_SECTION(sg_anime_list_content, {
-		/* Status & score section */
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Status:"), subsection));
-
-			QStringList string_list;
-			for (unsigned int i = 0; i < ARRAYSIZE(Anime::ListStatuses); i++)
-				string_list.append(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])));
-
-			QComboBox* combo_box = new QComboBox(subsection);
-			combo_box->addItems(string_list);
-			connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
-			        [this](int i) { status = Anime::ListStatuses[i]; });
-			combo_box->setCurrentIndex(static_cast<int>(status = anime.GetUserStatus()) - 1);
-			subsection_layout->addWidget(combo_box);
-		});
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Score:"), subsection));
-
-			QSpinBox* spin_box = new QSpinBox(subsection);
-			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { score = i; });
-			spin_box->setRange(0, 100);
-			spin_box->setSingleStep(5);
-			spin_box->setValue(score = anime.GetUserScore());
-			subsection_layout->addWidget(spin_box);
-		});
-	});
-	CREATE_FULL_WIDTH_SECTION(sg_anime_list_content, {
-		/* Notes section */
-		CREATE_FULL_WIDTH_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Notes:"), subsection));
-
-			QLineEdit* line_edit = new QLineEdit(subsection);
-			connect(line_edit, &QLineEdit::textChanged, this, [this](const QString& text) {
-				/* this sucks but I don't really want to implement anything smarter :) */
-				notes = Strings::ToUtf8String(text);
-			});
-			line_edit->setText(Strings::ToQString(notes = anime.GetUserNotes()));
-			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
-			subsection_layout->addWidget(line_edit);
-		});
-	});
-	CREATE_SECTION(sg_anime_list_content, {
-		/* Dates section */
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Date started:"), subsection));
-
-			OptionalDate* date = new OptionalDate(true, subsection);
-			connect(date, &OptionalDate::DataChanged, this,
-			        [this](bool enabled, Date date) { started = (enabled) ? date : Date(); });
-			started = anime.GetUserDateStarted();
-			if (!started.IsValid()) {
-				date->SetEnabled(false);
-				started = anime.GetAirDate();
-			}
-			date->SetDate(started);
-			subsection_layout->addWidget(date);
-		});
-		CREATE_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Date completed:"), subsection));
-
-			OptionalDate* date = new OptionalDate(true, subsection);
-			connect(date, &OptionalDate::DataChanged, this,
-			        [this](bool enabled, Date date) { completed = (enabled) ? date : Date(); });
-			completed = anime.GetUserDateCompleted();
-			if (!completed.IsValid()) {
-				date->SetEnabled(false);
-				completed = anime.GetAirDate();
-			}
-			date->SetDate(completed);
-			subsection_layout->addWidget(date);
-		});
-	});
-
-	settings_layout->addWidget(sg_anime_list_content);
-
-	settings_layout->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
-
-	QWidget* sg_local_content = new QWidget(settings_widget);
-	QVBoxLayout* sg_local_layout = new QVBoxLayout(sg_local_content);
-	sg_local_layout->setSpacing(5);
-	sg_local_layout->setContentsMargins(12, 0, 0, 0);
-
-	CREATE_FULL_WIDTH_SECTION(sg_local_content, {
-		/* Alternative titles */
-		CREATE_FULL_WIDTH_SUBSECTION({
-			subsection_layout->addWidget(new QLabel(tr("Alternative titles:"), subsection));
-
-			QLineEdit* line_edit = new QLineEdit("", subsection);
-			line_edit->setPlaceholderText(
-			    tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
-			subsection_layout->addWidget(line_edit);
-
-			QCheckBox* checkbox = new QCheckBox(tr("Use the first alternative title to search for torrents"));
-			subsection_layout->addWidget(checkbox);
-		});
-	});
-#undef CREATE_SECTION
-#undef CREATE_SUBSECTION
-#undef CREATE_FULL_WIDTH_SECTION
-#undef CREATE_FULL_WIDTH_SUBSECTION
-
-	settings_layout->addWidget(sg_local_content);
-	settings_layout->addStretch();
-
-	tabbed_widget->addTab(main_information_widget, tr("Main information"));
-	tabbed_widget->addTab(settings_widget, tr("My list and settings"));
-
-	QVBoxLayout* main_layout = new QVBoxLayout(main_widget);
-	main_layout->addWidget(anime_title);
-	main_layout->addWidget(tabbed_widget);
-	main_layout->setContentsMargins(0, 0, 0, 0);
-
-	QHBoxLayout* layout = new QHBoxLayout(widget);
-	layout->addWidget(sidebar);
-	layout->addWidget(main_widget);
-	layout->setSpacing(12);
-
-	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
-	connect(button_box, &QDialogButtonBox::accepted, this, [this, accept] {
-		SaveData();
-		accept();
-		QDialog::accept();
-	});
-	connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
-
-	QVBoxLayout* buttons_layout = new QVBoxLayout(this);
-	buttons_layout->addWidget(widget);
-	buttons_layout->addWidget(button_box, 0, Qt::AlignBottom);
-}
-
-#include "gui/dialog/moc_information.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/settings.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,105 @@
+#include "gui/dialog/settings.h"
+#include "gui/widgets/sidebar.h"
+#include "gui/widgets/text.h"
+#include <QDialogButtonBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QStackedWidget>
+#include <QVBoxLayout>
+#include <QWidget>
+
+SettingsPage::SettingsPage(QWidget* parent, QString title) : QWidget(parent) {
+	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	page_title = new QLabel(title, this);
+	page_title->setWordWrap(false);
+	page_title->setFrameShape(QFrame::Panel);
+	page_title->setFrameShadow(QFrame::Sunken);
+
+	QFont font(page_title->font());
+	font.setPixelSize(12);
+	font.setWeight(QFont::Bold);
+	page_title->setFont(font);
+
+	QPalette pal = page_title->palette();
+	pal.setColor(QPalette::Window, QColor(0xAB, 0xAB, 0xAB));
+	pal.setColor(QPalette::WindowText, Qt::white);
+	page_title->setPalette(pal);
+	page_title->setAutoFillBackground(true);
+
+	page_title->setFixedHeight(23);
+	page_title->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
+	page_title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+
+	tab_widget = new QTabWidget(this);
+	tab_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	layout->setContentsMargins(0, 0, 0, 0);
+	layout->addWidget(page_title);
+	layout->addWidget(tab_widget);
+}
+
+void SettingsPage::SetTitle(QString title) {
+	page_title->setText(title);
+}
+
+void SettingsPage::AddTab(QWidget* tab, QString title) {
+	tab_widget->addTab(tab, title);
+}
+
+void SettingsPage::SaveInfo() {
+	// no-op... child classes will implement this
+}
+
+void SettingsDialog::OnOK() {
+	for (int i = 0; i < stacked->count(); i++) {
+		reinterpret_cast<SettingsPage*>(stacked->widget(i))->SaveInfo();
+	}
+	QDialog::accept();
+}
+
+SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) {
+	setFixedSize(755, 566);
+	setWindowTitle(tr("Settings"));
+	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
+	QWidget* widget = new QWidget(this);
+	widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	sidebar = new SideBar(widget);
+	sidebar->setCurrentItem(sidebar->AddItem(tr("Services"), SideBar::CreateIcon(":/icons/24x24/globe.png")));
+	// sidebar->AddItem(tr("Library"), SideBar::CreateIcon(":/icons/24x24/inbox-film.png"));
+	sidebar->AddItem(tr("Application"), SideBar::CreateIcon(":/icons/24x24/application-sidebar-list.png"));
+	// sidebar->AddItem(tr("Recognition"), SideBar::CreateIcon(":/icons/24x24/question.png"));
+	// sidebar->AddItem(tr("Sharing"), SideBar::CreateIcon(":/icons/24x24/megaphone.png"));
+	// sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/24x24/feed.png"));
+	// sidebar->AddItem(tr("Advanced"), SideBar::CreateIcon(":/icons/24x24/gear.png"));
+	sidebar->setIconSize(QSize(24, 24));
+	sidebar->setFrameShape(QFrame::Box);
+
+	QPalette pal(sidebar->palette());
+	sidebar->SetBackgroundColor(pal.color(QPalette::Base));
+
+	sidebar->setFixedWidth(158);
+	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+
+	stacked = new QStackedWidget(this);
+	stacked->addWidget(new SettingsPageServices(stacked));
+	stacked->addWidget(new SettingsPageApplication(stacked));
+	stacked->setCurrentIndex(0);
+
+	connect(sidebar, &QListWidget::currentRowChanged, stacked, &QStackedWidget::setCurrentIndex);
+
+	QHBoxLayout* layout = new QHBoxLayout(widget);
+	layout->addWidget(sidebar);
+	layout->addWidget(stacked);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+	connect(button_box, &QDialogButtonBox::accepted, this, &SettingsDialog::OnOK);
+	connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+	QVBoxLayout* buttons_layout = new QVBoxLayout(this);
+	buttons_layout->addWidget(widget);
+	buttons_layout->addWidget(button_box);
+}
+
+#include "gui/dialog/moc_settings.cpp"
--- a/src/gui/dialog/settings.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-#include "gui/dialog/settings.h"
-#include "gui/widgets/sidebar.h"
-#include "gui/widgets/text.h"
-#include <QDialogButtonBox>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QStackedWidget>
-#include <QVBoxLayout>
-#include <QWidget>
-
-SettingsPage::SettingsPage(QWidget* parent, QString title) : QWidget(parent) {
-	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-	page_title = new QLabel(title, this);
-	page_title->setWordWrap(false);
-	page_title->setFrameShape(QFrame::Panel);
-	page_title->setFrameShadow(QFrame::Sunken);
-
-	QFont font(page_title->font());
-	font.setPixelSize(12);
-	font.setWeight(QFont::Bold);
-	page_title->setFont(font);
-
-	QPalette pal = page_title->palette();
-	pal.setColor(QPalette::Window, QColor(0xAB, 0xAB, 0xAB));
-	pal.setColor(QPalette::WindowText, Qt::white);
-	page_title->setPalette(pal);
-	page_title->setAutoFillBackground(true);
-
-	page_title->setFixedHeight(23);
-	page_title->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
-	page_title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
-
-	tab_widget = new QTabWidget(this);
-	tab_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-
-	QVBoxLayout* layout = new QVBoxLayout(this);
-	layout->setContentsMargins(0, 0, 0, 0);
-	layout->addWidget(page_title);
-	layout->addWidget(tab_widget);
-}
-
-void SettingsPage::SetTitle(QString title) {
-	page_title->setText(title);
-}
-
-void SettingsPage::AddTab(QWidget* tab, QString title) {
-	tab_widget->addTab(tab, title);
-}
-
-void SettingsPage::SaveInfo() {
-	// no-op... child classes will implement this
-}
-
-void SettingsDialog::OnOK() {
-	for (int i = 0; i < stacked->count(); i++) {
-		reinterpret_cast<SettingsPage*>(stacked->widget(i))->SaveInfo();
-	}
-	QDialog::accept();
-}
-
-SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) {
-	setFixedSize(755, 566);
-	setWindowTitle(tr("Settings"));
-	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
-	QWidget* widget = new QWidget(this);
-	widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-	sidebar = new SideBar(widget);
-	sidebar->setCurrentItem(sidebar->AddItem(tr("Services"), SideBar::CreateIcon(":/icons/24x24/globe.png")));
-	// sidebar->AddItem(tr("Library"), SideBar::CreateIcon(":/icons/24x24/inbox-film.png"));
-	sidebar->AddItem(tr("Application"), SideBar::CreateIcon(":/icons/24x24/application-sidebar-list.png"));
-	// sidebar->AddItem(tr("Recognition"), SideBar::CreateIcon(":/icons/24x24/question.png"));
-	// sidebar->AddItem(tr("Sharing"), SideBar::CreateIcon(":/icons/24x24/megaphone.png"));
-	// sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/24x24/feed.png"));
-	// sidebar->AddItem(tr("Advanced"), SideBar::CreateIcon(":/icons/24x24/gear.png"));
-	sidebar->setIconSize(QSize(24, 24));
-	sidebar->setFrameShape(QFrame::Box);
-
-	QPalette pal(sidebar->palette());
-	sidebar->SetBackgroundColor(pal.color(QPalette::Base));
-
-	sidebar->setFixedWidth(158);
-	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
-
-	stacked = new QStackedWidget(this);
-	stacked->addWidget(new SettingsPageServices(stacked));
-	stacked->addWidget(new SettingsPageApplication(stacked));
-	stacked->setCurrentIndex(0);
-
-	connect(sidebar, &QListWidget::currentRowChanged, stacked, &QStackedWidget::setCurrentIndex);
-
-	QHBoxLayout* layout = new QHBoxLayout(widget);
-	layout->addWidget(sidebar);
-	layout->addWidget(stacked);
-	layout->setContentsMargins(0, 0, 0, 0);
-
-	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
-	connect(button_box, &QDialogButtonBox::accepted, this, &SettingsDialog::OnOK);
-	connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
-
-	QVBoxLayout* buttons_layout = new QVBoxLayout(this);
-	buttons_layout->addWidget(widget);
-	buttons_layout->addWidget(button_box);
-}
-
-#include "gui/dialog/moc_settings.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/settings/application.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,123 @@
+#include "core/session.h"
+#include "gui/dialog/settings.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <QSizePolicy>
+#include <QVBoxLayout>
+
+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_widget);
+	double_click_layout->addWidget(dc_combo_box_label);
+	double_click_layout->addWidget(dc_combo_box);
+	double_click_layout->setContentsMargins(0, 0, 0, 0);
+
+	/* 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_widget);
+	middle_click_layout->addWidget(mc_combo_box_label);
+	middle_click_layout->addWidget(mc_combo_box);
+	middle_click_layout->setContentsMargins(0, 0, 0, 0);
+
+	/* Actions */
+	QHBoxLayout* actions_layout = new QHBoxLayout(actions_group_box);
+	actions_layout->addWidget(double_click_widget);
+	actions_layout->addWidget(middle_click_widget);
+
+	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);
+		hl_above_anime_box->setEnabled(state);
+	});
+	connect(hl_above_anime_box, &QCheckBox::stateChanged, this,
+	        [this](int state) { highlight_anime_if_available = !(state == Qt::Unchecked); });
+	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->setContentsMargins(10, 0, 0, 0);
+
+	/* Appearance */
+	QVBoxLayout* appearance_layout = new QVBoxLayout(appearance_group_box);
+	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);
+
+	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); });
+	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); });
+	progress_display_available_episodes->setCheckState(display_available_episodes ? Qt::Checked : Qt::Unchecked);
+
+	QVBoxLayout* progress_layout = new QVBoxLayout(progress_group_box);
+	progress_layout->addWidget(progress_display_aired_episodes);
+	progress_layout->addWidget(progress_display_available_episodes);
+
+	QVBoxLayout* full_layout = new QVBoxLayout(result);
+	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();
+
+	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/application.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-#include "core/session.h"
-#include "gui/dialog/settings.h"
-#include <QCheckBox>
-#include <QComboBox>
-#include <QGroupBox>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QPushButton>
-#include <QSizePolicy>
-#include <QVBoxLayout>
-
-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_widget);
-	double_click_layout->addWidget(dc_combo_box_label);
-	double_click_layout->addWidget(dc_combo_box);
-	double_click_layout->setContentsMargins(0, 0, 0, 0);
-
-	/* 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_widget);
-	middle_click_layout->addWidget(mc_combo_box_label);
-	middle_click_layout->addWidget(mc_combo_box);
-	middle_click_layout->setContentsMargins(0, 0, 0, 0);
-
-	/* Actions */
-	QHBoxLayout* actions_layout = new QHBoxLayout(actions_group_box);
-	actions_layout->addWidget(double_click_widget);
-	actions_layout->addWidget(middle_click_widget);
-
-	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);
-		hl_above_anime_box->setEnabled(state);
-	});
-	connect(hl_above_anime_box, &QCheckBox::stateChanged, this,
-	        [this](int state) { highlight_anime_if_available = !(state == Qt::Unchecked); });
-	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->setContentsMargins(10, 0, 0, 0);
-
-	/* Appearance */
-	QVBoxLayout* appearance_layout = new QVBoxLayout(appearance_group_box);
-	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);
-
-	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); });
-	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); });
-	progress_display_available_episodes->setCheckState(display_available_episodes ? Qt::Checked : Qt::Unchecked);
-
-	QVBoxLayout* progress_layout = new QVBoxLayout(progress_group_box);
-	progress_layout->addWidget(progress_display_aired_episodes);
-	progress_layout->addWidget(progress_display_available_episodes);
-
-	QVBoxLayout* full_layout = new QVBoxLayout(result);
-	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();
-
-	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"));
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/settings/services.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,97 @@
+#include "core/anime.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "gui/dialog/settings.h"
+#include "services/anilist.h"
+#include <QComboBox>
+#include <QGroupBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QSizePolicy>
+#include <QVBoxLayout>
+
+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: Minori is unable to synchronize multiple services at the same time."), sync_group_box);
+
+	QVBoxLayout* sync_layout = new QVBoxLayout(sync_group_box);
+	sync_layout->addWidget(sync_combo_box_label);
+	sync_layout->addWidget(sync_combo_box);
+	sync_layout->addWidget(sync_note_label);
+
+	QVBoxLayout* full_layout = new QVBoxLayout(result);
+	full_layout->addWidget(sync_group_box);
+	full_layout->setSpacing(10);
+	full_layout->addStretch();
+
+	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);
+
+	/* this is outdated! usernames are retrieved through a request to AniList now.
+	       although that's a bit... erm... cancerous, maybe this method IS useful. IDK */
+	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_widget);
+	auth_layout->addWidget(username_entry);
+	auth_layout->addWidget(auth_button);
+
+	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(group_box);
+	layout->addWidget(username_entry_label);
+	layout->addWidget(auth_widget);
+	layout->addWidget(note_label);
+
+	QVBoxLayout* full_layout = new QVBoxLayout(result);
+	full_layout->addWidget(group_box);
+	full_layout->setSpacing(10);
+	full_layout->addStretch();
+	return result;
+}
+
+void SettingsPageServices::SaveInfo() {
+	// session.config.anilist.username =
+	Strings::ToUtf8String(username);
+	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/dialog/settings/services.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-#include "core/anime.h"
-#include "core/session.h"
-#include "core/strings.h"
-#include "gui/dialog/settings.h"
-#include "services/anilist.h"
-#include <QComboBox>
-#include <QGroupBox>
-#include <QLabel>
-#include <QLineEdit>
-#include <QPushButton>
-#include <QSizePolicy>
-#include <QVBoxLayout>
-
-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: Minori is unable to synchronize multiple services at the same time."), sync_group_box);
-
-	QVBoxLayout* sync_layout = new QVBoxLayout(sync_group_box);
-	sync_layout->addWidget(sync_combo_box_label);
-	sync_layout->addWidget(sync_combo_box);
-	sync_layout->addWidget(sync_note_label);
-
-	QVBoxLayout* full_layout = new QVBoxLayout(result);
-	full_layout->addWidget(sync_group_box);
-	full_layout->setSpacing(10);
-	full_layout->addStretch();
-
-	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);
-
-	/* this is outdated! usernames are retrieved through a request to AniList now.
-	       although that's a bit... erm... cancerous, maybe this method IS useful. IDK */
-	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_widget);
-	auth_layout->addWidget(username_entry);
-	auth_layout->addWidget(auth_button);
-
-	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(group_box);
-	layout->addWidget(username_entry_label);
-	layout->addWidget(auth_widget);
-	layout->addWidget(note_label);
-
-	QVBoxLayout* full_layout = new QVBoxLayout(result);
-	full_layout->addWidget(group_box);
-	full_layout->setSpacing(10);
-	full_layout->addStretch();
-	return result;
-}
-
-void SettingsPageServices::SaveInfo() {
-	// session.config.anilist.username =
-	Strings::ToUtf8String(username);
-	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"));
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/anime_list.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,504 @@
+/**
+ * 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/array.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "core/time.h"
+#include "gui/dialog/information.h"
+#include "gui/translate/anime.h"
+#include "services/services.h"
+#include <QDebug>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QMenu>
+#include <QProgressBar>
+#include <QShortcut>
+#include <QStylePainter>
+#include <QStyledItemDelegate>
+#include <QThreadPool>
+#include <set>
+
+AnimeListPageDelegate::AnimeListPageDelegate(QObject* parent) : QStyledItemDelegate(parent) {
+}
+
+QWidget* AnimeListPageDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
+	// no edit 4 u
+	return nullptr;
+}
+
+void AnimeListPageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+                                  const QModelIndex& index) const {
+	switch (index.column()) {
+#if 0
+		case AnimeListPageModel::AL_PROGRESS: {
+			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
+			const int episodes =
+			    static_cast<int>(index.siblingAtColumn(AnimeListPageModel::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, tr("/"), 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;
+		}
+#endif
+		default: QStyledItemDelegate::paint(painter, option, index); break;
+	}
+}
+
+AnimeListPageSortFilter::AnimeListPageSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
+}
+
+bool AnimeListPageSortFilter::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;
+	}
+}
+
+AnimeListPageModel::AnimeListPageModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) {
+	status = _status;
+	return;
+}
+
+int AnimeListPageModel::rowCount(const QModelIndex& parent) const {
+	return list.size();
+	(void)(parent);
+}
+
+int AnimeListPageModel::columnCount(const QModelIndex& parent) const {
+	return NB_COLUMNS;
+	(void)(parent);
+}
+
+QVariant AnimeListPageModel::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 AnimeListPageModel::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 Strings::ToQString(Translate::ToString(list[index.row()].GetFormat()));
+				case AL_SEASON:
+					return Strings::ToQString(Translate::ToString(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_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 QVariant::fromValue(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();
+}
+
+Anime::Anime* AnimeListPageModel::GetAnimeFromIndex(QModelIndex index) {
+	return &list.at(index.row());
+}
+
+void AnimeListPageModel::RefreshList() {
+	bool has_children = !!rowCount(index(0));
+	if (!has_children) {
+		beginInsertRows(QModelIndex(), 0, 0);
+		endInsertRows();
+	}
+
+	beginResetModel();
+
+	list.clear();
+
+	for (const auto& a : Anime::db.items) {
+		if (a.second.IsInUserList() && a.second.GetUserStatus() == status) {
+			list.push_back(a.second);
+		}
+	}
+
+	endResetModel();
+}
+
+int AnimeListPage::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 AnimeListPage::SetColumnDefaults() {
+	tree_view->setColumnHidden(AnimeListPageModel::AL_SEASON, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_TYPE, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_PROGRESS, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_SCORE, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_TITLE, false);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_EPISODES, true);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_AVG_SCORE, true);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_STARTED, true);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_COMPLETED, true);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, true);
+	tree_view->setColumnHidden(AnimeListPageModel::AL_NOTES, true);
+}
+
+void AnimeListPage::UpdateAnime(int id) {
+	QThreadPool::globalInstance()->start([this, id] {
+		Services::UpdateAnimeEntry(id);
+		Refresh();
+	});
+}
+
+void AnimeListPage::RemoveAnime(int id) {
+	Anime::Anime& anime = Anime::db.items[id];
+	anime.RemoveFromUserList();
+	Refresh();
+}
+
+void AnimeListPage::DisplayColumnHeaderMenu() {
+	QMenu* menu = new QMenu(this);
+	menu->setAttribute(Qt::WA_DeleteOnClose);
+	menu->setTitle(tr("Column visibility"));
+	menu->setToolTipsVisible(true);
+
+	for (int i = 0; i < AnimeListPageModel::NB_COLUMNS; i++) {
+		if (i == AnimeListPageModel::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 AnimeListPage::DisplayListMenu() {
+	QMenu* menu = new QMenu(this);
+	menu->setAttribute(Qt::WA_DeleteOnClose);
+	menu->setTitle(tr("Column visibility"));
+	menu->setToolTipsVisible(true);
+
+	AnimeListPageModel* source_model =
+	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+	const QItemSelection selection =
+	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+
+	std::set<Anime::Anime*> animes;
+	for (const auto& index : selection.indexes()) {
+		if (!index.isValid())
+			continue;
+		Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
+		if (anime)
+			animes.insert(anime);
+	}
+
+	QAction* action = menu->addAction(tr("Information"), [this, animes] {
+		for (auto& anime : animes) {
+			InformationDialog* dialog = new InformationDialog(
+			    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
+
+			dialog->show();
+			dialog->raise();
+			dialog->activateWindow();
+		}
+	});
+	menu->addSeparator();
+	action = menu->addAction(tr("Delete from list..."), [this, animes] {
+		for (auto& anime : animes) {
+			RemoveAnime(anime->GetId());
+		}
+	});
+	menu->popup(QCursor::pos());
+}
+
+void AnimeListPage::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;
+	}
+
+	AnimeListPageModel* source_model =
+	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+
+	const QModelIndex index = source_model->index(selection.indexes().first().row());
+	Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
+
+	InformationDialog* dialog = new InformationDialog(
+	    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
+
+	dialog->show();
+	dialog->raise();
+	dialog->activateWindow();
+}
+
+void AnimeListPage::paintEvent(QPaintEvent*) {
+	QStylePainter p(this);
+
+	QStyleOptionTabWidgetFrame opt;
+	InitStyle(&opt);
+	opt.rect = panelRect;
+	p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
+}
+
+void AnimeListPage::resizeEvent(QResizeEvent* e) {
+	QWidget::resizeEvent(e);
+	SetupLayout();
+}
+
+void AnimeListPage::showEvent(QShowEvent*) {
+	SetupLayout();
+}
+
+void AnimeListPage::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 AnimeListPage::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 AnimeListPage::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);
+}
+
+AnimeListPage::AnimeListPage(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 AnimeListPageDelegate(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(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
+		                QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
+		sort_models[i] = new AnimeListPageSortFilter(tree_view);
+		sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i]));
+		sort_models[i]->setSortRole(Qt::UserRole);
+		sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive);
+	}
+	tree_view->setModel(sort_models[0]);
+
+	QHBoxLayout* layout = new QHBoxLayout(tree_widget);
+	layout->addWidget(tree_view);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	/* Double click stuff */
+	connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListPage::ItemDoubleClicked);
+	connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayListMenu);
+
+	/* Enter & return keys */
+	connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
+	        &AnimeListPage::ItemDoubleClicked);
+
+	connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
+	        &AnimeListPage::ItemDoubleClicked);
+
+	tree_view->header()->setStretchLastSection(false);
+	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
+	connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayColumnHeaderMenu);
+
+	connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) {
+		if (sort_models[index])
+			tree_view->setModel(sort_models[index]);
+	});
+
+	SetColumnDefaults();
+	setFocusPolicy(Qt::TabFocus);
+	setFocusProxy(tab_bar);
+}
+
+void AnimeListPage::RefreshList() {
+	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+		reinterpret_cast<AnimeListPageModel*>(sort_models[i]->sourceModel())->RefreshList();
+}
+
+void AnimeListPage::RefreshTabs() {
+	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+		tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
+		                           QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
+}
+
+void AnimeListPage::Refresh() {
+	RefreshList();
+	RefreshTabs();
+}
+
+/* This function, really, really should not be called.
+   Ever. Why would you ever need to clear the anime list?
+   Also, this sucks. */
+void AnimeListPage::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/anime_list.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,504 +0,0 @@
-/**
- * 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/array.h"
-#include "core/session.h"
-#include "core/strings.h"
-#include "core/time.h"
-#include "gui/dialog/information.h"
-#include "gui/translate/anime.h"
-#include "services/services.h"
-#include <QDebug>
-#include <QHBoxLayout>
-#include <QHeaderView>
-#include <QMenu>
-#include <QProgressBar>
-#include <QShortcut>
-#include <QStylePainter>
-#include <QStyledItemDelegate>
-#include <QThreadPool>
-#include <set>
-
-AnimeListPageDelegate::AnimeListPageDelegate(QObject* parent) : QStyledItemDelegate(parent) {
-}
-
-QWidget* AnimeListPageDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
-	// no edit 4 u
-	return nullptr;
-}
-
-void AnimeListPageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
-                                  const QModelIndex& index) const {
-	switch (index.column()) {
-#if 0
-		case AnimeListPageModel::AL_PROGRESS: {
-			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
-			const int episodes =
-			    static_cast<int>(index.siblingAtColumn(AnimeListPageModel::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, tr("/"), 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;
-		}
-#endif
-		default: QStyledItemDelegate::paint(painter, option, index); break;
-	}
-}
-
-AnimeListPageSortFilter::AnimeListPageSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
-}
-
-bool AnimeListPageSortFilter::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;
-	}
-}
-
-AnimeListPageModel::AnimeListPageModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) {
-	status = _status;
-	return;
-}
-
-int AnimeListPageModel::rowCount(const QModelIndex& parent) const {
-	return list.size();
-	(void)(parent);
-}
-
-int AnimeListPageModel::columnCount(const QModelIndex& parent) const {
-	return NB_COLUMNS;
-	(void)(parent);
-}
-
-QVariant AnimeListPageModel::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 AnimeListPageModel::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 Strings::ToQString(Translate::ToString(list[index.row()].GetFormat()));
-				case AL_SEASON:
-					return Strings::ToQString(Translate::ToString(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_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 QVariant::fromValue(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();
-}
-
-Anime::Anime* AnimeListPageModel::GetAnimeFromIndex(QModelIndex index) {
-	return &list.at(index.row());
-}
-
-void AnimeListPageModel::RefreshList() {
-	bool has_children = !!rowCount(index(0));
-	if (!has_children) {
-		beginInsertRows(QModelIndex(), 0, 0);
-		endInsertRows();
-	}
-
-	beginResetModel();
-
-	list.clear();
-
-	for (const auto& a : Anime::db.items) {
-		if (a.second.IsInUserList() && a.second.GetUserStatus() == status) {
-			list.push_back(a.second);
-		}
-	}
-
-	endResetModel();
-}
-
-int AnimeListPage::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 AnimeListPage::SetColumnDefaults() {
-	tree_view->setColumnHidden(AnimeListPageModel::AL_SEASON, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_TYPE, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_PROGRESS, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_SCORE, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_TITLE, false);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_EPISODES, true);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_AVG_SCORE, true);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_STARTED, true);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_COMPLETED, true);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, true);
-	tree_view->setColumnHidden(AnimeListPageModel::AL_NOTES, true);
-}
-
-void AnimeListPage::UpdateAnime(int id) {
-	QThreadPool::globalInstance()->start([this, id] {
-		Services::UpdateAnimeEntry(id);
-		Refresh();
-	});
-}
-
-void AnimeListPage::RemoveAnime(int id) {
-	Anime::Anime& anime = Anime::db.items[id];
-	anime.RemoveFromUserList();
-	Refresh();
-}
-
-void AnimeListPage::DisplayColumnHeaderMenu() {
-	QMenu* menu = new QMenu(this);
-	menu->setAttribute(Qt::WA_DeleteOnClose);
-	menu->setTitle(tr("Column visibility"));
-	menu->setToolTipsVisible(true);
-
-	for (int i = 0; i < AnimeListPageModel::NB_COLUMNS; i++) {
-		if (i == AnimeListPageModel::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 AnimeListPage::DisplayListMenu() {
-	QMenu* menu = new QMenu(this);
-	menu->setAttribute(Qt::WA_DeleteOnClose);
-	menu->setTitle(tr("Column visibility"));
-	menu->setToolTipsVisible(true);
-
-	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
-	const QItemSelection selection =
-	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
-
-	std::set<Anime::Anime*> animes;
-	for (const auto& index : selection.indexes()) {
-		if (!index.isValid())
-			continue;
-		Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
-		if (anime)
-			animes.insert(anime);
-	}
-
-	QAction* action = menu->addAction(tr("Information"), [this, animes] {
-		for (auto& anime : animes) {
-			InformationDialog* dialog = new InformationDialog(
-			    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
-
-			dialog->show();
-			dialog->raise();
-			dialog->activateWindow();
-		}
-	});
-	menu->addSeparator();
-	action = menu->addAction(tr("Delete from list..."), [this, animes] {
-		for (auto& anime : animes) {
-			RemoveAnime(anime->GetId());
-		}
-	});
-	menu->popup(QCursor::pos());
-}
-
-void AnimeListPage::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;
-	}
-
-	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
-
-	const QModelIndex index = source_model->index(selection.indexes().first().row());
-	Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
-
-	InformationDialog* dialog = new InformationDialog(
-	    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
-
-	dialog->show();
-	dialog->raise();
-	dialog->activateWindow();
-}
-
-void AnimeListPage::paintEvent(QPaintEvent*) {
-	QStylePainter p(this);
-
-	QStyleOptionTabWidgetFrame opt;
-	InitStyle(&opt);
-	opt.rect = panelRect;
-	p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
-}
-
-void AnimeListPage::resizeEvent(QResizeEvent* e) {
-	QWidget::resizeEvent(e);
-	SetupLayout();
-}
-
-void AnimeListPage::showEvent(QShowEvent*) {
-	SetupLayout();
-}
-
-void AnimeListPage::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 AnimeListPage::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 AnimeListPage::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);
-}
-
-AnimeListPage::AnimeListPage(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 AnimeListPageDelegate(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(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
-		                QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
-		sort_models[i] = new AnimeListPageSortFilter(tree_view);
-		sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i]));
-		sort_models[i]->setSortRole(Qt::UserRole);
-		sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive);
-	}
-	tree_view->setModel(sort_models[0]);
-
-	QHBoxLayout* layout = new QHBoxLayout(tree_widget);
-	layout->addWidget(tree_view);
-	layout->setContentsMargins(0, 0, 0, 0);
-
-	/* Double click stuff */
-	connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListPage::ItemDoubleClicked);
-	connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayListMenu);
-
-	/* Enter & return keys */
-	connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
-
-	connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
-
-	tree_view->header()->setStretchLastSection(false);
-	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
-	connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayColumnHeaderMenu);
-
-	connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) {
-		if (sort_models[index])
-			tree_view->setModel(sort_models[index]);
-	});
-
-	SetColumnDefaults();
-	setFocusPolicy(Qt::TabFocus);
-	setFocusProxy(tab_bar);
-}
-
-void AnimeListPage::RefreshList() {
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
-		reinterpret_cast<AnimeListPageModel*>(sort_models[i]->sourceModel())->RefreshList();
-}
-
-void AnimeListPage::RefreshTabs() {
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
-		tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
-		                           QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
-}
-
-void AnimeListPage::Refresh() {
-	RefreshList();
-	RefreshTabs();
-}
-
-/* This function, really, really should not be called.
-   Ever. Why would you ever need to clear the anime list?
-   Also, this sucks. */
-void AnimeListPage::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"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/history.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,6 @@
+#include "gui/pages/history.h"
+
+HistoryPage::HistoryPage(QWidget* parent) : QWidget(parent) {
+}
+
+#include "gui/pages/moc_history.cpp"
--- a/src/gui/pages/history.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#include "gui/pages/history.h"
-
-HistoryPage::HistoryPage(QWidget* parent) : QWidget(parent) {
-}
-
-#include "gui/pages/moc_history.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/now_playing.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,109 @@
+#include "gui/pages/now_playing.h"
+#include "core/anime_db.h"
+#include "gui/widgets/anime_info.h"
+#include "gui/widgets/text.h"
+#include <QLabel>
+#include <QStackedWidget>
+#include <QVBoxLayout>
+#include <QWidget>
+
+/* This is here to make it easier to switch between the
+   "sub-pages", i.e., not playing and playing.
+
+   TODO: find a way to do this more efficiently */
+namespace NowPlayingPages {
+
+class Default : public QWidget {
+		Q_OBJECT
+
+	public:
+		Default(QWidget* parent = nullptr);
+};
+
+class Playing : public QWidget {
+		Q_OBJECT
+
+	public:
+		Playing(QWidget* parent = nullptr);
+		void SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info);
+		int GetPlayingAnime();
+
+	private:
+		int _id = 0;
+		int _episode = 0;
+		std::unique_ptr<TextWidgets::Title> _title = nullptr;
+		std::unique_ptr<AnimeInfoWidget> _info = nullptr;
+};
+
+Default::Default(QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	TextWidgets::Title* title = new TextWidgets::Title(tr("Now Playing"), this);
+	layout->addWidget(title);
+
+	layout->addStretch();
+}
+
+Playing::Playing(QWidget* parent) : QWidget(parent) {
+	QHBoxLayout* layout = new QHBoxLayout(this);
+
+	_title.reset(new TextWidgets::Title("\n", this));
+	layout->addWidget(_title.get());
+
+	_info.reset(new AnimeInfoWidget(this));
+	layout->addWidget(_info.get());
+
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+int Playing::GetPlayingAnime() {
+	return _id;
+}
+
+void Playing::SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info) {
+	if (id == _id || id <= 0)
+		return;
+	if (Anime::db.items.find(id) != Anime::db.items.end()) {
+		const Anime::Anime& anime = Anime::db.items[_id = id];
+		_title->setText(anime.GetUserPreferredTitle());
+		_info->SetAnime(anime);
+	}
+}
+
+} // namespace NowPlayingPages
+
+NowPlayingPage::NowPlayingPage(QWidget* parent) : QFrame(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	setFrameShape(QFrame::Box);
+	setFrameShadow(QFrame::Sunken);
+
+	QPalette pal = QPalette();
+	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
+	setPalette(pal);
+	setAutoFillBackground(true);
+
+	stack = new QStackedWidget(this);
+	stack->addWidget(new NowPlayingPages::Default(stack));
+	stack->addWidget(new NowPlayingPages::Playing(stack));
+	layout->addWidget(stack);
+
+	SetDefault();
+}
+
+void NowPlayingPage::SetDefault() {
+	stack->setCurrentIndex(0);
+}
+
+int NowPlayingPage::GetPlayingId() {
+	return reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->GetPlayingAnime();
+}
+
+void NowPlayingPage::SetPlaying(int id, const std::unordered_map<std::string, std::string>& info) {
+	reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->SetPlayingAnime(id, info);
+	stack->setCurrentIndex(1);
+}
+
+#include "gui/pages/moc_now_playing.cpp"
+#include "now_playing.moc"
--- a/src/gui/pages/now_playing.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-#include "gui/pages/now_playing.h"
-#include "core/anime_db.h"
-#include "gui/widgets/anime_info.h"
-#include "gui/widgets/text.h"
-#include <QLabel>
-#include <QStackedWidget>
-#include <QVBoxLayout>
-#include <QWidget>
-
-/* This is here to make it easier to switch between the
-   "sub-pages", i.e., not playing and playing.
-
-   TODO: find a way to do this more efficiently */
-namespace NowPlayingPages {
-
-class Default : public QWidget {
-		Q_OBJECT
-
-	public:
-		Default(QWidget* parent = nullptr);
-};
-
-class Playing : public QWidget {
-		Q_OBJECT
-
-	public:
-		Playing(QWidget* parent = nullptr);
-		void SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info);
-		int GetPlayingAnime();
-
-	private:
-		int _id = 0;
-		int _episode = 0;
-		std::unique_ptr<TextWidgets::Title> _title = nullptr;
-		std::unique_ptr<AnimeInfoWidget> _info = nullptr;
-};
-
-Default::Default(QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-	layout->setContentsMargins(0, 0, 0, 0);
-
-	TextWidgets::Title* title = new TextWidgets::Title(tr("Now Playing"), this);
-	layout->addWidget(title);
-
-	layout->addStretch();
-}
-
-Playing::Playing(QWidget* parent) : QWidget(parent) {
-	QHBoxLayout* layout = new QHBoxLayout(this);
-
-	_title.reset(new TextWidgets::Title("\n", this));
-	layout->addWidget(_title.get());
-
-	_info.reset(new AnimeInfoWidget(this));
-	layout->addWidget(_info.get());
-
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-int Playing::GetPlayingAnime() {
-	return _id;
-}
-
-void Playing::SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info) {
-	if (id == _id || id <= 0)
-		return;
-	if (Anime::db.items.find(id) != Anime::db.items.end()) {
-		const Anime::Anime& anime = Anime::db.items[_id = id];
-		_title->setText(anime.GetUserPreferredTitle());
-		_info->SetAnime(anime);
-	}
-}
-
-} // namespace NowPlayingPages
-
-NowPlayingPage::NowPlayingPage(QWidget* parent) : QFrame(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	setFrameShape(QFrame::Box);
-	setFrameShadow(QFrame::Sunken);
-
-	QPalette pal = QPalette();
-	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
-	setPalette(pal);
-	setAutoFillBackground(true);
-
-	stack = new QStackedWidget(this);
-	stack->addWidget(new NowPlayingPages::Default(stack));
-	stack->addWidget(new NowPlayingPages::Playing(stack));
-	layout->addWidget(stack);
-
-	SetDefault();
-}
-
-void NowPlayingPage::SetDefault() {
-	stack->setCurrentIndex(0);
-}
-
-int NowPlayingPage::GetPlayingId() {
-	return reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->GetPlayingAnime();
-}
-
-void NowPlayingPage::SetPlaying(int id, const std::unordered_map<std::string, std::string>& info) {
-	reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->SetPlayingAnime(id, info);
-	stack->setCurrentIndex(1);
-}
-
-#include "gui/pages/moc_now_playing.cpp"
-#include "now_playing.moc"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/search.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,6 @@
+#include "gui/pages/search.h"
+
+SearchPage::SearchPage(QWidget* parent) : QWidget(parent) {
+}
+
+#include "gui/pages/moc_search.cpp"
--- a/src/gui/pages/search.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#include "gui/pages/search.h"
-
-SearchPage::SearchPage(QWidget* parent) : QWidget(parent) {
-}
-
-#include "gui/pages/moc_search.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/seasons.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,6 @@
+#include "gui/pages/seasons.h"
+
+SeasonsPage::SeasonsPage(QWidget* parent) : QWidget(parent) {
+}
+
+#include "gui/pages/moc_seasons.cpp"
--- a/src/gui/pages/seasons.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#include "gui/pages/seasons.h"
-
-SeasonsPage::SeasonsPage(QWidget* parent) : QWidget(parent) {
-}
-
-#include "gui/pages/moc_seasons.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/statistics.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,113 @@
+#include "gui/pages/statistics.h"
+#include "core/anime_db.h"
+#include "core/session.h"
+#include "gui/pages/anime_list.h"
+#include "gui/widgets/text.h"
+#include <QString>
+#include <QTextDocument>
+#include <QTextStream>
+#include <QTimer>
+#include <QVBoxLayout>
+#include <QWidget>
+#include <sstream>
+
+StatisticsPage::StatisticsPage(QWidget* parent) : QFrame(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	setFrameShape(QFrame::Box);
+	setFrameShadow(QFrame::Sunken);
+
+	QPalette pal = QPalette();
+	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
+	setPalette(pal);
+	setAutoFillBackground(true);
+
+	TextWidgets::LabelledSection* anime_list_paragraph = new TextWidgets::LabelledSection(
+	    tr("Anime list"),
+	    tr("Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:"),
+	    "\n\n\n\n\n\n", this);
+	anime_list_data = anime_list_paragraph->GetParagraph();
+
+	TextWidgets::LabelledSection* application_paragraph =
+	    new TextWidgets::LabelledSection(tr("Minori"), tr("Uptime:\nRequests made:"), "\n\n", this);
+	application_data = application_paragraph->GetParagraph();
+
+	layout->addWidget(anime_list_paragraph);
+	layout->addWidget(application_paragraph);
+	layout->addStretch();
+
+	QTimer* timer = new QTimer(this);
+	connect(timer, &QTimer::timeout, this, [this] {
+		if (isVisible())
+			UpdateStatistics();
+	});
+	timer->start(1000); // update statistics every second
+}
+
+void StatisticsPage::showEvent(QShowEvent*) {
+	UpdateStatistics();
+}
+
+/* me abusing macros :) */
+#define ADD_TIME_SEGMENT(r, x, s, p) \
+	if (x > 0) \
+	r << x << ((x == 1) ? s : p)
+std::string StatisticsPage::MinutesToDateString(int minutes) {
+	/* ew */
+	int years = (minutes * (1 / 525949.2F));
+	int months = (minutes * (1 / 43829.1F)) - (years * 12);
+	int days = (minutes * (1 / 1440.0F)) - (years * 365.2425F) - (months * 30.436875F);
+	int hours = (minutes * (1 / 60.0F)) - (years * 8765.82F) - (months * 730.485F) - (days * 24);
+	int rest_minutes = (minutes) - (years * 525949.2F) - (months * 43829.1F) - (days * 1440) - (hours * 60);
+	std::ostringstream return_stream;
+	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 ");
+	if (rest_minutes > 0 || return_stream.str().size() == 0)
+		return_stream << rest_minutes << ((rest_minutes == 1) ? " minute" : " minutes");
+	return return_stream.str();
+}
+
+std::string StatisticsPage::SecondsToDateString(int sec) {
+	/* this is all fairly unnecessary, but works:tm: */
+	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, 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
+
+void StatisticsPage::UpdateStatistics() {
+	/* 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();
+	anime_list_data->SetText(string);
+
+	string = "";
+	ts << QString::fromUtf8(SecondsToDateString(session.uptime() / 1000).c_str()) << '\n';
+	ts << session.GetRequests();
+	/* Application */
+	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
+	application_data->SetText(string);
+}
+
+#include "gui/pages/moc_statistics.cpp"
--- a/src/gui/pages/statistics.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-#include "gui/pages/statistics.h"
-#include "core/anime_db.h"
-#include "core/session.h"
-#include "gui/pages/anime_list.h"
-#include "gui/widgets/text.h"
-#include <QString>
-#include <QTextDocument>
-#include <QTextStream>
-#include <QTimer>
-#include <QVBoxLayout>
-#include <QWidget>
-#include <sstream>
-
-StatisticsPage::StatisticsPage(QWidget* parent) : QFrame(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	setFrameShape(QFrame::Box);
-	setFrameShadow(QFrame::Sunken);
-
-	QPalette pal = QPalette();
-	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
-	setPalette(pal);
-	setAutoFillBackground(true);
-
-	TextWidgets::LabelledSection* anime_list_paragraph = new TextWidgets::LabelledSection(
-	    tr("Anime list"),
-	    tr("Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:"),
-	    "\n\n\n\n\n\n", this);
-	anime_list_data = anime_list_paragraph->GetParagraph();
-
-	TextWidgets::LabelledSection* application_paragraph =
-	    new TextWidgets::LabelledSection(tr("Minori"), tr("Uptime:\nRequests made:"), "\n\n", this);
-	application_data = application_paragraph->GetParagraph();
-
-	layout->addWidget(anime_list_paragraph);
-	layout->addWidget(application_paragraph);
-	layout->addStretch();
-
-	QTimer* timer = new QTimer(this);
-	connect(timer, &QTimer::timeout, this, [this] {
-		if (isVisible())
-			UpdateStatistics();
-	});
-	timer->start(1000); // update statistics every second
-}
-
-void StatisticsPage::showEvent(QShowEvent*) {
-	UpdateStatistics();
-}
-
-/* me abusing macros :) */
-#define ADD_TIME_SEGMENT(r, x, s, p) \
-	if (x > 0) \
-	r << x << ((x == 1) ? s : p)
-std::string StatisticsPage::MinutesToDateString(int minutes) {
-	/* ew */
-	int years = (minutes * (1 / 525949.2F));
-	int months = (minutes * (1 / 43829.1F)) - (years * 12);
-	int days = (minutes * (1 / 1440.0F)) - (years * 365.2425F) - (months * 30.436875F);
-	int hours = (minutes * (1 / 60.0F)) - (years * 8765.82F) - (months * 730.485F) - (days * 24);
-	int rest_minutes = (minutes) - (years * 525949.2F) - (months * 43829.1F) - (days * 1440) - (hours * 60);
-	std::ostringstream return_stream;
-	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 ");
-	if (rest_minutes > 0 || return_stream.str().size() == 0)
-		return_stream << rest_minutes << ((rest_minutes == 1) ? " minute" : " minutes");
-	return return_stream.str();
-}
-
-std::string StatisticsPage::SecondsToDateString(int sec) {
-	/* this is all fairly unnecessary, but works:tm: */
-	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, 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
-
-void StatisticsPage::UpdateStatistics() {
-	/* 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();
-	anime_list_data->SetText(string);
-
-	string = "";
-	ts << QString::fromUtf8(SecondsToDateString(session.uptime() / 1000).c_str()) << '\n';
-	ts << session.GetRequests();
-	/* Application */
-	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
-	application_data->SetText(string);
-}
-
-#include "gui/pages/moc_statistics.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/pages/torrents.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,6 @@
+#include "gui/pages/torrents.h"
+
+TorrentsPage::TorrentsPage(QWidget* parent) : QWidget(parent) {
+}
+
+#include "gui/pages/moc_torrents.cpp"
--- a/src/gui/pages/torrents.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#include "gui/pages/torrents.h"
-
-TorrentsPage::TorrentsPage(QWidget* parent) : QWidget(parent) {
-}
-
-#include "gui/pages/moc_torrents.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/translate/anilist.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,53 @@
+#include "gui/translate/anilist.h"
+
+namespace Translate {
+namespace AniList {
+
+Anime::SeriesStatus ToSeriesStatus(std::string status) {
+	const std::unordered_map<std::string, Anime::SeriesStatus> map = {
+	    {"FINISHED",         Anime::SeriesStatus::FINISHED        },
+	    {"RELEASING",        Anime::SeriesStatus::RELEASING       },
+	    {"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED},
+	    {"CANCELLED",        Anime::SeriesStatus::CANCELLED       },
+	    {"HIATUS",           Anime::SeriesStatus::HIATUS          }
+    };
+
+	if (map.find(status) == map.end())
+		return Anime::SeriesStatus::UNKNOWN;
+	return map.at(status);
+}
+
+Anime::SeriesSeason ToSeriesSeason(std::string season) {
+	const std::unordered_map<std::string, Anime::SeriesSeason> map = {
+	    {"WINTER", Anime::SeriesSeason::WINTER},
+	    {"SPRING", Anime::SeriesSeason::SPRING},
+	    {"SUMMER", Anime::SeriesSeason::SUMMER},
+	    {"FALL",   Anime::SeriesSeason::FALL  }
+    };
+
+	if (map.find(season) == map.end())
+		return Anime::SeriesSeason::UNKNOWN;
+	return map.at(season);
+}
+
+Anime::SeriesFormat ToSeriesFormat(std::string format) {
+	const std::unordered_map<std::string, enum Anime::SeriesFormat> map = {
+	    {"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}
+    };
+
+	if (map.find(format) == map.end())
+		return Anime::SeriesFormat::UNKNOWN;
+	return map.at(format);
+}
+
+} // namespace AniList
+} // namespace Translate
--- a/src/gui/translate/anilist.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-#include "gui/translate/anilist.h"
-
-namespace Translate {
-namespace AniList {
-
-Anime::SeriesStatus ToSeriesStatus(std::string status) {
-	const std::unordered_map<std::string, Anime::SeriesStatus> map = {
-	    {"FINISHED",         Anime::SeriesStatus::FINISHED        },
-	    {"RELEASING",        Anime::SeriesStatus::RELEASING       },
-	    {"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED},
-	    {"CANCELLED",        Anime::SeriesStatus::CANCELLED       },
-	    {"HIATUS",           Anime::SeriesStatus::HIATUS          }
-    };
-
-	if (map.find(status) == map.end())
-		return Anime::SeriesStatus::UNKNOWN;
-	return map.at(status);
-}
-
-Anime::SeriesSeason ToSeriesSeason(std::string season) {
-	const std::unordered_map<std::string, Anime::SeriesSeason> map = {
-	    {"WINTER", Anime::SeriesSeason::WINTER},
-	    {"SPRING", Anime::SeriesSeason::SPRING},
-	    {"SUMMER", Anime::SeriesSeason::SUMMER},
-	    {"FALL",   Anime::SeriesSeason::FALL  }
-    };
-
-	if (map.find(season) == map.end())
-		return Anime::SeriesSeason::UNKNOWN;
-	return map.at(season);
-}
-
-Anime::SeriesFormat ToSeriesFormat(std::string format) {
-	const std::unordered_map<std::string, enum Anime::SeriesFormat> map = {
-	    {"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}
-    };
-
-	if (map.find(format) == map.end())
-		return Anime::SeriesFormat::UNKNOWN;
-	return map.at(format);
-}
-
-} // namespace AniList
-} // namespace Translate
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/translate/anime.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,57 @@
+#include "core/anime.h"
+#include "core/strings.h"
+#include "gui/translate/anime.h"
+#include <QCoreApplication>
+
+namespace Translate {
+
+std::string ToString(const Anime::ListStatus status) {
+	switch (status) {
+		case Anime::ListStatus::NOT_IN_LIST: return Strings::ToUtf8String(QCoreApplication::tr("Not in list"));
+		case Anime::ListStatus::CURRENT: return Strings::ToUtf8String(QCoreApplication::tr("Currently watching"));
+		case Anime::ListStatus::PLANNING: return Strings::ToUtf8String(QCoreApplication::tr("Plan to watch"));
+		case Anime::ListStatus::COMPLETED: return Strings::ToUtf8String(QCoreApplication::tr("Completed"));
+		case Anime::ListStatus::DROPPED: return Strings::ToUtf8String(QCoreApplication::tr("Dropped"));
+		case Anime::ListStatus::PAUSED: return Strings::ToUtf8String(QCoreApplication::tr("On hold"));
+		default: return "";
+	}
+}
+
+std::string ToString(const Anime::SeriesFormat format) {
+	switch (format) {
+		case Anime::SeriesFormat::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesFormat::TV: return Strings::ToUtf8String(QCoreApplication::tr("TV"));
+		case Anime::SeriesFormat::TV_SHORT: return Strings::ToUtf8String(QCoreApplication::tr("TV short"));
+		case Anime::SeriesFormat::OVA: return Strings::ToUtf8String(QCoreApplication::tr("OVA"));
+		case Anime::SeriesFormat::MOVIE: return Strings::ToUtf8String(QCoreApplication::tr("Movie"));
+		case Anime::SeriesFormat::SPECIAL: return Strings::ToUtf8String(QCoreApplication::tr("Special"));
+		case Anime::SeriesFormat::ONA: return Strings::ToUtf8String(QCoreApplication::tr("ONA"));
+		case Anime::SeriesFormat::MUSIC: return Strings::ToUtf8String(QCoreApplication::tr("Music"));
+		default: return "";
+	}
+}
+
+std::string ToString(const Anime::SeriesSeason season) {
+	switch (season) {
+		case Anime::SeriesSeason::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesSeason::WINTER: return Strings::ToUtf8String(QCoreApplication::tr("Winter"));
+		case Anime::SeriesSeason::SUMMER: return Strings::ToUtf8String(QCoreApplication::tr("Summer"));
+		case Anime::SeriesSeason::FALL: return Strings::ToUtf8String(QCoreApplication::tr("Fall"));
+		case Anime::SeriesSeason::SPRING: return Strings::ToUtf8String(QCoreApplication::tr("Spring"));
+		default: return "";
+	}
+}
+
+std::string ToString(const Anime::SeriesStatus status) {
+	switch (status) {
+		case Anime::SeriesStatus::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesStatus::RELEASING: return Strings::ToUtf8String(QCoreApplication::tr("Currently airing"));
+		case Anime::SeriesStatus::FINISHED: return Strings::ToUtf8String(QCoreApplication::tr("Finished airing"));
+		case Anime::SeriesStatus::NOT_YET_RELEASED: return Strings::ToUtf8String(QCoreApplication::tr("Not yet aired"));
+		case Anime::SeriesStatus::CANCELLED: return Strings::ToUtf8String(QCoreApplication::tr("Cancelled"));
+		case Anime::SeriesStatus::HIATUS: return Strings::ToUtf8String(QCoreApplication::tr("On hiatus"));
+		default: return "";
+	}
+}
+
+} // namespace Translate
--- a/src/gui/translate/anime.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-#include "core/anime.h"
-#include "core/strings.h"
-#include "gui/translate/anime.h"
-#include <QCoreApplication>
-
-namespace Translate {
-
-std::string ToString(const Anime::ListStatus status) {
-	switch (status) {
-		case Anime::ListStatus::NOT_IN_LIST: return Strings::ToUtf8String(QCoreApplication::tr("Not in list"));
-		case Anime::ListStatus::CURRENT: return Strings::ToUtf8String(QCoreApplication::tr("Currently watching"));
-		case Anime::ListStatus::PLANNING: return Strings::ToUtf8String(QCoreApplication::tr("Plan to watch"));
-		case Anime::ListStatus::COMPLETED: return Strings::ToUtf8String(QCoreApplication::tr("Completed"));
-		case Anime::ListStatus::DROPPED: return Strings::ToUtf8String(QCoreApplication::tr("Dropped"));
-		case Anime::ListStatus::PAUSED: return Strings::ToUtf8String(QCoreApplication::tr("On hold"));
-		default: return "";
-	}
-}
-
-std::string ToString(const Anime::SeriesFormat format) {
-	switch (format) {
-		case Anime::SeriesFormat::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
-		case Anime::SeriesFormat::TV: return Strings::ToUtf8String(QCoreApplication::tr("TV"));
-		case Anime::SeriesFormat::TV_SHORT: return Strings::ToUtf8String(QCoreApplication::tr("TV short"));
-		case Anime::SeriesFormat::OVA: return Strings::ToUtf8String(QCoreApplication::tr("OVA"));
-		case Anime::SeriesFormat::MOVIE: return Strings::ToUtf8String(QCoreApplication::tr("Movie"));
-		case Anime::SeriesFormat::SPECIAL: return Strings::ToUtf8String(QCoreApplication::tr("Special"));
-		case Anime::SeriesFormat::ONA: return Strings::ToUtf8String(QCoreApplication::tr("ONA"));
-		case Anime::SeriesFormat::MUSIC: return Strings::ToUtf8String(QCoreApplication::tr("Music"));
-		default: return "";
-	}
-}
-
-std::string ToString(const Anime::SeriesSeason season) {
-	switch (season) {
-		case Anime::SeriesSeason::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
-		case Anime::SeriesSeason::WINTER: return Strings::ToUtf8String(QCoreApplication::tr("Winter"));
-		case Anime::SeriesSeason::SUMMER: return Strings::ToUtf8String(QCoreApplication::tr("Summer"));
-		case Anime::SeriesSeason::FALL: return Strings::ToUtf8String(QCoreApplication::tr("Fall"));
-		case Anime::SeriesSeason::SPRING: return Strings::ToUtf8String(QCoreApplication::tr("Spring"));
-		default: return "";
-	}
-}
-
-std::string ToString(const Anime::SeriesStatus status) {
-	switch (status) {
-		case Anime::SeriesStatus::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
-		case Anime::SeriesStatus::RELEASING: return Strings::ToUtf8String(QCoreApplication::tr("Currently airing"));
-		case Anime::SeriesStatus::FINISHED: return Strings::ToUtf8String(QCoreApplication::tr("Finished airing"));
-		case Anime::SeriesStatus::NOT_YET_RELEASED: return Strings::ToUtf8String(QCoreApplication::tr("Not yet aired"));
-		case Anime::SeriesStatus::CANCELLED: return Strings::ToUtf8String(QCoreApplication::tr("Cancelled"));
-		case Anime::SeriesStatus::HIATUS: return Strings::ToUtf8String(QCoreApplication::tr("On hiatus"));
-		default: return "";
-	}
-}
-
-} // namespace Translate
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/anime_info.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,48 @@
+#include "gui/widgets/anime_info.h"
+#include "core/anime.h"
+#include "core/strings.h"
+#include "gui/translate/anime.h"
+#include "gui/widgets/text.h"
+#include <QHBoxLayout>
+#include <QTextStream>
+
+AnimeInfoWidget::AnimeInfoWidget(QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	_title.reset(new TextWidgets::OneLineSection(tr("Alternative titles"), "", this));
+	layout->addWidget(_title.get());
+
+	_details.reset(new TextWidgets::LabelledSection(tr("Details"), new TextWidgets::LabelledSection(
+	    tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), "", this), "", this));
+	layout->addWidget(_details.get());
+
+	_synopsis.reset(new TextWidgets::LabelledSection(tr("Synopsis"), "", this));
+	_synopsis->GetParagraph()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	layout->addWidget(_synopsis.get());
+}
+
+AnimeInfoWidget::AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent) : AnimeInfoWidget(parent) {
+	SetAnime(anime);
+}
+
+AnimeInfoWidget::SetAnime(const Anime::Anime& anime) {
+	/* alt titles */
+	_title->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+
+	/* details */
+	QString details_data;
+	QTextStream details_data_s(&details_data);
+	details_data_s << Translate::ToString(anime.GetFormat()).c_str() << "\n"
+	               << anime.GetEpisodes() << "\n"
+	               << Translate::ToString(anime.GetUserStatus()).c_str() << "\n"
+	               << Translate::ToString(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear() << "\n"
+	               << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
+	               << anime.GetAudienceScore() << "%";
+	_details->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	layout->addWidget();
+
+	_synopsis->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	layout->addWidget(synopsis);
+}
+
+#include "gui/widgets/moc_anime_info.cpp"
--- a/src/gui/widgets/anime_info.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-#include "gui/widgets/anime_info.h"
-#include "core/anime.h"
-#include "core/strings.h"
-#include "gui/translate/anime.h"
-#include "gui/widgets/text.h"
-#include <QHBoxLayout>
-#include <QTextStream>
-
-AnimeInfoWidget::AnimeInfoWidget(QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	_title.reset(new TextWidgets::OneLineSection(tr("Alternative titles"), "", this));
-	layout->addWidget(_title.get());
-
-	_details.reset(new TextWidgets::LabelledSection(tr("Details"), new TextWidgets::LabelledSection(
-	    tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), "", this), "", this));
-	layout->addWidget(_details.get());
-
-	_synopsis.reset(new TextWidgets::LabelledSection(tr("Synopsis"), "", this));
-	_synopsis->GetParagraph()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-	layout->addWidget(_synopsis.get());
-}
-
-AnimeInfoWidget::AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent) : AnimeInfoWidget(parent) {
-	SetAnime(anime);
-}
-
-AnimeInfoWidget::SetAnime(const Anime::Anime& anime) {
-	/* alt titles */
-	_title->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
-
-	/* details */
-	QString details_data;
-	QTextStream details_data_s(&details_data);
-	details_data_s << Translate::ToString(anime.GetFormat()).c_str() << "\n"
-	               << anime.GetEpisodes() << "\n"
-	               << Translate::ToString(anime.GetUserStatus()).c_str() << "\n"
-	               << Translate::ToString(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear() << "\n"
-	               << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
-	               << anime.GetAudienceScore() << "%";
-	_details->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
-	layout->addWidget();
-
-	_synopsis->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
-	layout->addWidget(synopsis);
-}
-
-#include "gui/widgets/moc_anime_info.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/clickable_label.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,16 @@
+#include "gui/widgets/clickable_label.h"
+/* NOTE: this can likely be moved to poster.cpp, as
+   it's really the only place this will ever be used */
+
+ClickableLabel::ClickableLabel(QWidget* parent) : QLabel(parent) {
+	setCursor(Qt::PointingHandCursor);
+}
+
+ClickableLabel::~ClickableLabel() {
+}
+
+void ClickableLabel::mousePressEvent(QMouseEvent*) {
+	emit clicked();
+}
+
+#include "gui/widgets/moc_clickable_label.cpp"
--- a/src/gui/widgets/clickable_label.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-#include "gui/widgets/clickable_label.h"
-/* NOTE: this can likely be moved to poster.cpp, as
-   it's really the only place this will ever be used */
-
-ClickableLabel::ClickableLabel(QWidget* parent) : QLabel(parent) {
-	setCursor(Qt::PointingHandCursor);
-}
-
-ClickableLabel::~ClickableLabel() {
-}
-
-void ClickableLabel::mousePressEvent(QMouseEvent*) {
-	emit clicked();
-}
-
-#include "gui/widgets/moc_clickable_label.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/optional_date.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,66 @@
+#include "gui/widgets/optional_date.h"
+#include "core/date.h"
+#include <QCheckBox>
+#include <QDateEdit>
+#include <QHBoxLayout>
+
+OptionalDate::OptionalDate(QWidget* parent) {
+	OptionalDate(false, parent);
+}
+
+OptionalDate::OptionalDate(bool enabled, QWidget* parent) : QWidget(parent) {
+	QHBoxLayout* layout = new QHBoxLayout(this);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	_checkbox = new QCheckBox(this);
+	_checkbox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
+
+	layout->addWidget(_checkbox, 0, Qt::AlignVCenter);
+
+	_dateedit = new QDateEdit(this);
+	_dateedit->setDisplayFormat("yyyy-MM-dd");
+	_dateedit->setCalendarPopup(true);
+	_dateedit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+
+	layout->addWidget(_dateedit);
+
+	SetEnabled(enabled);
+	connect(_checkbox, &QCheckBox::stateChanged, this, [this](int state) {
+		SetEnabled(state == Qt::Checked);
+		emit DataChanged(IsEnabled(), GetDate());
+	});
+	connect(_dateedit, &QDateEdit::dateChanged, this, [this](QDate) { emit DataChanged(IsEnabled(), GetDate()); });
+}
+
+void OptionalDate::SetEnabled(bool enabled) {
+	_checkbox->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
+	_dateedit->setEnabled(enabled);
+}
+
+bool OptionalDate::IsEnabled() {
+	return _dateedit->isEnabled();
+}
+
+void OptionalDate::SetDate(QDate date) {
+	_dateedit->setDate(date);
+}
+
+void OptionalDate::SetDate(Date date) {
+	if (!date.IsValid())
+		return;
+	SetDate(date.GetAsQDate());
+}
+
+Date OptionalDate::GetDate() {
+	return Date(_dateedit->date());
+}
+
+QDateEdit* OptionalDate::GetDateEdit() {
+	return _dateedit;
+}
+
+QCheckBox* OptionalDate::GetCheckBox() {
+	return _checkbox;
+}
+
+#include "gui/widgets/moc_optional_date.cpp"
--- a/src/gui/widgets/optional_date.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-#include "gui/widgets/optional_date.h"
-#include "core/date.h"
-#include <QCheckBox>
-#include <QDateEdit>
-#include <QHBoxLayout>
-
-OptionalDate::OptionalDate(QWidget* parent) {
-	OptionalDate(false, parent);
-}
-
-OptionalDate::OptionalDate(bool enabled, QWidget* parent) : QWidget(parent) {
-	QHBoxLayout* layout = new QHBoxLayout(this);
-	layout->setContentsMargins(0, 0, 0, 0);
-
-	_checkbox = new QCheckBox(this);
-	_checkbox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
-
-	layout->addWidget(_checkbox, 0, Qt::AlignVCenter);
-
-	_dateedit = new QDateEdit(this);
-	_dateedit->setDisplayFormat("yyyy-MM-dd");
-	_dateedit->setCalendarPopup(true);
-	_dateedit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
-
-	layout->addWidget(_dateedit);
-
-	SetEnabled(enabled);
-	connect(_checkbox, &QCheckBox::stateChanged, this, [this](int state) {
-		SetEnabled(state == Qt::Checked);
-		emit DataChanged(IsEnabled(), GetDate());
-	});
-	connect(_dateedit, &QDateEdit::dateChanged, this, [this](QDate) { emit DataChanged(IsEnabled(), GetDate()); });
-}
-
-void OptionalDate::SetEnabled(bool enabled) {
-	_checkbox->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
-	_dateedit->setEnabled(enabled);
-}
-
-bool OptionalDate::IsEnabled() {
-	return _dateedit->isEnabled();
-}
-
-void OptionalDate::SetDate(QDate date) {
-	_dateedit->setDate(date);
-}
-
-void OptionalDate::SetDate(Date date) {
-	if (!date.IsValid())
-		return;
-	SetDate(date.GetAsQDate());
-}
-
-Date OptionalDate::GetDate() {
-	return Date(_dateedit->date());
-}
-
-QDateEdit* OptionalDate::GetDateEdit() {
-	return _dateedit;
-}
-
-QCheckBox* OptionalDate::GetCheckBox() {
-	return _checkbox;
-}
-
-#include "gui/widgets/moc_optional_date.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/poster.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,60 @@
+#include "gui/widgets/poster.h"
+#include "core/anime_db.h"
+#include "core/http.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "gui/widgets/clickable_label.h"
+#include <QByteArray>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QFrame>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QMessageBox>
+#include <QPixmap>
+#include <QThreadPool>
+#include <QUrl>
+#include <curl/curl.h>
+
+Poster::Poster(int id, QWidget* parent) : QFrame(parent) {
+	QHBoxLayout* layout = new QHBoxLayout(this);
+	layout->setContentsMargins(1, 1, 1, 1);
+
+	setCursor(Qt::PointingHandCursor);
+	setFixedSize(150, 225);
+	setFrameShape(QFrame::Box);
+	setFrameShadow(QFrame::Plain);
+
+	const Anime::Anime& anime = Anime::db.items[id];
+
+	QThreadPool::globalInstance()->start([this, anime] {
+		QByteArray ba = HTTP::Get(anime.GetPosterUrl(), {});
+		ImageDownloadFinished(ba);
+	});
+
+	QPixmap pixmap = QPixmap::fromImage(img);
+
+	label = new ClickableLabel(this);
+	label->setAlignment(Qt::AlignCenter);
+	connect(label, &ClickableLabel::clicked, this,
+	        [anime] { QDesktopServices::openUrl(Strings::ToQString(anime.GetServiceUrl())); });
+	layout->addWidget(label);
+}
+
+void Poster::ImageDownloadFinished(QByteArray arr) {
+	img.loadFromData(arr);
+	RenderToLabel();
+}
+
+void Poster::RenderToLabel() {
+	QPixmap pixmap = QPixmap::fromImage(img);
+	if (pixmap.isNull())
+		return;
+	label->setPixmap(pixmap.scaled(label->size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
+}
+
+void Poster::resizeEvent(QResizeEvent*) {
+	RenderToLabel();
+}
+
+#include "gui/widgets/moc_poster.cpp"
--- a/src/gui/widgets/poster.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-#include "gui/widgets/poster.h"
-#include "core/anime_db.h"
-#include "core/http.h"
-#include "core/session.h"
-#include "core/strings.h"
-#include "gui/widgets/clickable_label.h"
-#include <QByteArray>
-#include <QDebug>
-#include <QDesktopServices>
-#include <QFrame>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QMessageBox>
-#include <QPixmap>
-#include <QThreadPool>
-#include <QUrl>
-#include <curl/curl.h>
-
-Poster::Poster(int id, QWidget* parent) : QFrame(parent) {
-	QHBoxLayout* layout = new QHBoxLayout(this);
-	layout->setContentsMargins(1, 1, 1, 1);
-
-	setCursor(Qt::PointingHandCursor);
-	setFixedSize(150, 225);
-	setFrameShape(QFrame::Box);
-	setFrameShadow(QFrame::Plain);
-
-	const Anime::Anime& anime = Anime::db.items[id];
-
-	QThreadPool::globalInstance()->start([this, anime] {
-		QByteArray ba = HTTP::Get(anime.GetPosterUrl(), {});
-		ImageDownloadFinished(ba);
-	});
-
-	QPixmap pixmap = QPixmap::fromImage(img);
-
-	label = new ClickableLabel(this);
-	label->setAlignment(Qt::AlignCenter);
-	connect(label, &ClickableLabel::clicked, this,
-	        [anime] { QDesktopServices::openUrl(Strings::ToQString(anime.GetServiceUrl())); });
-	layout->addWidget(label);
-}
-
-void Poster::ImageDownloadFinished(QByteArray arr) {
-	img.loadFromData(arr);
-	RenderToLabel();
-}
-
-void Poster::RenderToLabel() {
-	QPixmap pixmap = QPixmap::fromImage(img);
-	if (pixmap.isNull())
-		return;
-	label->setPixmap(pixmap.scaled(label->size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
-}
-
-void Poster::resizeEvent(QResizeEvent*) {
-	RenderToLabel();
-}
-
-#include "gui/widgets/moc_poster.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/sidebar.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,101 @@
+#include "gui/widgets/sidebar.h"
+#include <QFrame>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QMouseEvent>
+
+SideBar::SideBar(QWidget* parent) : QListWidget(parent) {
+	setFrameShape(QFrame::NoFrame);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setSelectionMode(QAbstractItemView::SingleSelection);
+	setSelectionBehavior(QAbstractItemView::SelectItems);
+	setMouseTracking(true);
+	/* FIXME: is there an easy way to do this with palettes? */
+	setStyleSheet("QListWidget::item:disabled { background: transparent }");
+
+	SetBackgroundColor(Qt::transparent);
+
+	connect(this, &QListWidget::currentRowChanged, this,
+	        [this](int index) { emit CurrentItemChanged(RemoveSeparatorsFromIndex(index)); });
+}
+
+void SideBar::SetCurrentItem(int index) {
+	setCurrentRow(AddSeparatorsToIndex(index));
+}
+
+void SideBar::SetBackgroundColor(QColor color) {
+	viewport()->setAutoFillBackground(color != Qt::transparent);
+	QPalette pal(palette());
+	pal.setColor(QPalette::Window, color);
+	setPalette(pal);
+}
+
+QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
+	QListWidgetItem* item = new QListWidgetItem(this);
+	item->setText(name);
+	if (!icon.isNull())
+		item->setIcon(icon);
+	return item;
+}
+
+QIcon SideBar::CreateIcon(const char* file) {
+	QPixmap pixmap(file, "PNG");
+	QIcon result;
+	result.addPixmap(pixmap, QIcon::Normal);
+	result.addPixmap(pixmap, QIcon::Selected);
+	return result;
+}
+
+QListWidgetItem* SideBar::AddSeparator() {
+	QListWidgetItem* item = new QListWidgetItem(this);
+	QFrame* line = new QFrame(this);
+	line->setFrameShape(QFrame::HLine);
+	line->setFrameShadow(QFrame::Sunken);
+	line->setMouseTracking(true);
+	line->setEnabled(false);
+
+	setItemWidget(item, line);
+	item->setFlags(Qt::NoItemFlags);
+	item->setBackground(QBrush(Qt::transparent));
+	return item;
+}
+
+int SideBar::AddSeparatorsToIndex(int index) {
+	int i, j;
+	for (i = 0, j = 0; i < index;) {
+		i++;
+		if (IndexIsSeparator(indexFromItem(item(i))))
+			j++;
+	}
+	return i + j;
+}
+
+int SideBar::RemoveSeparatorsFromIndex(int index) {
+	int i, j;
+	for (i = 0, j = 0; i < index; i++) {
+		if (!IndexIsSeparator(indexFromItem(item(i))))
+			j++;
+	}
+	return j;
+}
+
+bool SideBar::IndexIsSeparator(QModelIndex index) const {
+	return !(index.isValid() && index.flags() & Qt::ItemIsEnabled);
+}
+
+QItemSelectionModel::SelectionFlags SideBar::selectionCommand(const QModelIndex& index, const QEvent*) const {
+	if (IndexIsSeparator(index))
+		return QItemSelectionModel::NoUpdate;
+	return QItemSelectionModel::ClearAndSelect;
+}
+
+void SideBar::mouseMoveEvent(QMouseEvent* event) {
+	if (!IndexIsSeparator(indexAt(event->pos())))
+		setCursor(Qt::PointingHandCursor);
+	else
+		unsetCursor();
+	QListView::mouseMoveEvent(event);
+}
+
+#include "gui/widgets/moc_sidebar.cpp"
--- a/src/gui/widgets/sidebar.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-#include "gui/widgets/sidebar.h"
-#include <QFrame>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QMouseEvent>
-
-SideBar::SideBar(QWidget* parent) : QListWidget(parent) {
-	setFrameShape(QFrame::NoFrame);
-	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setSelectionMode(QAbstractItemView::SingleSelection);
-	setSelectionBehavior(QAbstractItemView::SelectItems);
-	setMouseTracking(true);
-	/* FIXME: is there an easy way to do this with palettes? */
-	setStyleSheet("QListWidget::item:disabled { background: transparent }");
-
-	SetBackgroundColor(Qt::transparent);
-
-	connect(this, &QListWidget::currentRowChanged, this,
-	        [this](int index) { emit CurrentItemChanged(RemoveSeparatorsFromIndex(index)); });
-}
-
-void SideBar::SetCurrentItem(int index) {
-	setCurrentRow(AddSeparatorsToIndex(index));
-}
-
-void SideBar::SetBackgroundColor(QColor color) {
-	viewport()->setAutoFillBackground(color != Qt::transparent);
-	QPalette pal(palette());
-	pal.setColor(QPalette::Window, color);
-	setPalette(pal);
-}
-
-QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
-	QListWidgetItem* item = new QListWidgetItem(this);
-	item->setText(name);
-	if (!icon.isNull())
-		item->setIcon(icon);
-	return item;
-}
-
-QIcon SideBar::CreateIcon(const char* file) {
-	QPixmap pixmap(file, "PNG");
-	QIcon result;
-	result.addPixmap(pixmap, QIcon::Normal);
-	result.addPixmap(pixmap, QIcon::Selected);
-	return result;
-}
-
-QListWidgetItem* SideBar::AddSeparator() {
-	QListWidgetItem* item = new QListWidgetItem(this);
-	QFrame* line = new QFrame(this);
-	line->setFrameShape(QFrame::HLine);
-	line->setFrameShadow(QFrame::Sunken);
-	line->setMouseTracking(true);
-	line->setEnabled(false);
-
-	setItemWidget(item, line);
-	item->setFlags(Qt::NoItemFlags);
-	item->setBackground(QBrush(Qt::transparent));
-	return item;
-}
-
-int SideBar::AddSeparatorsToIndex(int index) {
-	int i, j;
-	for (i = 0, j = 0; i < index;) {
-		i++;
-		if (IndexIsSeparator(indexFromItem(item(i))))
-			j++;
-	}
-	return i + j;
-}
-
-int SideBar::RemoveSeparatorsFromIndex(int index) {
-	int i, j;
-	for (i = 0, j = 0; i < index; i++) {
-		if (!IndexIsSeparator(indexFromItem(item(i))))
-			j++;
-	}
-	return j;
-}
-
-bool SideBar::IndexIsSeparator(QModelIndex index) const {
-	return !(index.isValid() && index.flags() & Qt::ItemIsEnabled);
-}
-
-QItemSelectionModel::SelectionFlags SideBar::selectionCommand(const QModelIndex& index, const QEvent*) const {
-	if (IndexIsSeparator(index))
-		return QItemSelectionModel::NoUpdate;
-	return QItemSelectionModel::ClearAndSelect;
-}
-
-void SideBar::mouseMoveEvent(QMouseEvent* event) {
-	if (!IndexIsSeparator(indexAt(event->pos())))
-		setCursor(Qt::PointingHandCursor);
-	else
-		unsetCursor();
-	QListView::mouseMoveEvent(event);
-}
-
-#include "gui/widgets/moc_sidebar.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/text.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,237 @@
+#include "gui/widgets/text.h"
+#include "core/session.h"
+#include <QDebug>
+#include <QFrame>
+#include <QLabel>
+#include <QTextBlock>
+#include <QVBoxLayout>
+
+namespace TextWidgets {
+
+Header::Header(QString title, QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
+	static_text_title = new QLabel(title, this);
+	static_text_title->setTextFormat(Qt::PlainText);
+	QFont font = static_text_title->font();
+	font.setWeight(QFont::Bold);
+	static_text_title->setFont(font);
+
+	static_text_line = new QFrame(this);
+	static_text_line->setFrameShape(QFrame::HLine);
+	static_text_line->setFrameShadow(QFrame::Sunken);
+	static_text_line->setFixedHeight(2);
+
+	layout->addWidget(static_text_title);
+	layout->addWidget(static_text_line);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+void Header::SetText(QString text) {
+	static_text_title->setText(text);
+}
+
+/* inherits QPlainTextEdit and gives a much more reasonable minimum size */
+Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
+	setTextInteractionFlags(Qt::TextBrowserInteraction);
+	setFrameShape(QFrame::NoFrame);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+	QPalette pal;
+	pal.setColor(QPalette::Window, Qt::transparent);
+	setPalette(pal);
+
+	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+}
+
+void Paragraph::SetText(QString text) {
+	QTextDocument* document = new QTextDocument(this);
+	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
+	document->setPlainText(text);
+	setDocument(document);
+}
+
+/* highly based upon... some stackoverflow answer for PyQt */
+QSize Paragraph::minimumSizeHint() const {
+	return QSize(0, 0);
+}
+
+QSize Paragraph::sizeHint() const {
+	QTextDocument* doc = document();
+	doc->adjustSize();
+	long h = 0;
+	for (QTextBlock line = doc->begin(); line != doc->end(); line = line.next()) {
+		h += doc->documentLayout()->blockBoundingRect(line).height();
+	}
+	return QSize(doc->size().width(), h);
+}
+
+/* Equivalent to Paragraph(), but is only capable of showing one line. Only
+   exists because sometimes with SelectableSection it will let you go
+   out of bounds */
+Line::Line(QString text, QWidget* parent) : QLineEdit(text, parent) {
+	setFrame(false);
+	setReadOnly(true);
+	setCursorPosition(0); /* displays left text first */
+
+	QPalette pal;
+	pal.setColor(QPalette::Window, Qt::transparent);
+	setPalette(pal);
+
+	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+}
+
+Title::Title(QString title, QWidget* parent) : Line(title, parent) {
+	QFont fnt(font());
+	fnt.setPixelSize(16);
+	setFont(fnt);
+
+	QPalette pal(palette());
+	pal.setColor(QPalette::Window, Qt::transparent);
+	pal.setColor(QPalette::Text, QColor(0x00, 0x33, 0x99));
+	setPalette(pal);
+}
+
+Section::Section(QString title, QString data, QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	header = new Header(title, this);
+
+	QWidget* content = new QWidget(this);
+	QHBoxLayout* content_layout = new QHBoxLayout(content);
+
+	paragraph = new Paragraph(data, this);
+	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	paragraph->setWordWrapMode(QTextOption::NoWrap);
+
+	content_layout->addWidget(paragraph);
+	content_layout->setSpacing(0);
+	content_layout->setContentsMargins(0, 0, 0, 0);
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout->addWidget(header);
+	layout->addWidget(paragraph);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+Header* Section::GetHeader() {
+	return header;
+}
+
+Paragraph* Section::GetParagraph() {
+	return paragraph;
+}
+
+LabelledSection::LabelledSection(QString title, QString label, QString data, QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	header = new Header(title, this);
+
+	// this is not accessible from the object because there's really
+	// no reason to make it accessible...
+	QWidget* content = new QWidget(this);
+	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+
+	labels = new Paragraph(label, this);
+	labels->setTextInteractionFlags(Qt::NoTextInteraction);
+	labels->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	labels->setWordWrapMode(QTextOption::NoWrap);
+	labels->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+
+	paragraph = new Paragraph(data, this);
+	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	paragraph->setWordWrapMode(QTextOption::NoWrap);
+
+	QHBoxLayout* content_layout = new QHBoxLayout(content);
+	content_layout->addWidget(labels, 0, Qt::AlignTop);
+	content_layout->addWidget(paragraph, 0, Qt::AlignTop);
+	content_layout->setSpacing(20);
+	content_layout->setContentsMargins(0, 0, 0, 0);
+
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout->addWidget(header);
+	layout->addWidget(content);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+Header* LabelledSection::GetHeader() {
+	return header;
+}
+
+Paragraph* LabelledSection::GetLabels() {
+	return labels;
+}
+
+Paragraph* LabelledSection::GetParagraph() {
+	return paragraph;
+}
+
+SelectableSection::SelectableSection(QString title, QString data, QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	header = new Header(title, this);
+
+	QWidget* content = new QWidget(this);
+	QHBoxLayout* content_layout = new QHBoxLayout(content);
+
+	paragraph = new Paragraph(data, content);
+
+	content_layout->addWidget(paragraph);
+	content_layout->setSpacing(0);
+	content_layout->setContentsMargins(0, 0, 0, 0);
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout->addWidget(header);
+	layout->addWidget(content);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+Header* SelectableSection::GetHeader() {
+	return header;
+}
+
+Paragraph* SelectableSection::GetParagraph() {
+	return paragraph;
+}
+
+OneLineSection::OneLineSection(QString title, QString text, QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+
+	header = new Header(title, this);
+
+	QWidget* content = new QWidget(this);
+	QHBoxLayout* content_layout = new QHBoxLayout(content);
+
+	line = new Line(text, content);
+
+	content_layout->addWidget(line);
+	content_layout->setSpacing(0);
+	content_layout->setContentsMargins(0, 0, 0, 0);
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout->addWidget(header);
+	layout->addWidget(content);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+}
+
+Header* OneLineSection::GetHeader() {
+	return header;
+}
+
+Line* OneLineSection::GetLine() {
+	return line;
+}
+
+} // namespace TextWidgets
+
+#include "gui/widgets/moc_text.cpp"
--- a/src/gui/widgets/text.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-#include "gui/widgets/text.h"
-#include "core/session.h"
-#include <QDebug>
-#include <QFrame>
-#include <QLabel>
-#include <QTextBlock>
-#include <QVBoxLayout>
-
-namespace TextWidgets {
-
-Header::Header(QString title, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-
-	static_text_title = new QLabel(title, this);
-	static_text_title->setTextFormat(Qt::PlainText);
-	QFont font = static_text_title->font();
-	font.setWeight(QFont::Bold);
-	static_text_title->setFont(font);
-
-	static_text_line = new QFrame(this);
-	static_text_line->setFrameShape(QFrame::HLine);
-	static_text_line->setFrameShadow(QFrame::Sunken);
-	static_text_line->setFixedHeight(2);
-
-	layout->addWidget(static_text_title);
-	layout->addWidget(static_text_line);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-void Header::SetText(QString text) {
-	static_text_title->setText(text);
-}
-
-/* inherits QPlainTextEdit and gives a much more reasonable minimum size */
-Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
-	setTextInteractionFlags(Qt::TextBrowserInteraction);
-	setFrameShape(QFrame::NoFrame);
-	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-	QPalette pal;
-	pal.setColor(QPalette::Window, Qt::transparent);
-	setPalette(pal);
-
-	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
-}
-
-void Paragraph::SetText(QString text) {
-	QTextDocument* document = new QTextDocument(this);
-	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
-	document->setPlainText(text);
-	setDocument(document);
-}
-
-/* highly based upon... some stackoverflow answer for PyQt */
-QSize Paragraph::minimumSizeHint() const {
-	return QSize(0, 0);
-}
-
-QSize Paragraph::sizeHint() const {
-	QTextDocument* doc = document();
-	doc->adjustSize();
-	long h = 0;
-	for (QTextBlock line = doc->begin(); line != doc->end(); line = line.next()) {
-		h += doc->documentLayout()->blockBoundingRect(line).height();
-	}
-	return QSize(doc->size().width(), h);
-}
-
-/* Equivalent to Paragraph(), but is only capable of showing one line. Only
-   exists because sometimes with SelectableSection it will let you go
-   out of bounds */
-Line::Line(QString text, QWidget* parent) : QLineEdit(text, parent) {
-	setFrame(false);
-	setReadOnly(true);
-	setCursorPosition(0); /* displays left text first */
-
-	QPalette pal;
-	pal.setColor(QPalette::Window, Qt::transparent);
-	setPalette(pal);
-
-	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
-}
-
-Title::Title(QString title, QWidget* parent) : Line(title, parent) {
-	QFont fnt(font());
-	fnt.setPixelSize(16);
-	setFont(fnt);
-
-	QPalette pal(palette());
-	pal.setColor(QPalette::Window, Qt::transparent);
-	pal.setColor(QPalette::Text, QColor(0x00, 0x33, 0x99));
-	setPalette(pal);
-}
-
-Section::Section(QString title, QString data, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrapMode(QTextOption::NoWrap);
-
-	content_layout->addWidget(paragraph);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(paragraph);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* Section::GetHeader() {
-	return header;
-}
-
-Paragraph* Section::GetParagraph() {
-	return paragraph;
-}
-
-LabelledSection::LabelledSection(QString title, QString label, QString data, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	// this is not accessible from the object because there's really
-	// no reason to make it accessible...
-	QWidget* content = new QWidget(this);
-	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
-
-	labels = new Paragraph(label, this);
-	labels->setTextInteractionFlags(Qt::NoTextInteraction);
-	labels->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	labels->setWordWrapMode(QTextOption::NoWrap);
-	labels->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
-
-	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrapMode(QTextOption::NoWrap);
-
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-	content_layout->addWidget(labels, 0, Qt::AlignTop);
-	content_layout->addWidget(paragraph, 0, Qt::AlignTop);
-	content_layout->setSpacing(20);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* LabelledSection::GetHeader() {
-	return header;
-}
-
-Paragraph* LabelledSection::GetLabels() {
-	return labels;
-}
-
-Paragraph* LabelledSection::GetParagraph() {
-	return paragraph;
-}
-
-SelectableSection::SelectableSection(QString title, QString data, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	paragraph = new Paragraph(data, content);
-
-	content_layout->addWidget(paragraph);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* SelectableSection::GetHeader() {
-	return header;
-}
-
-Paragraph* SelectableSection::GetParagraph() {
-	return paragraph;
-}
-
-OneLineSection::OneLineSection(QString title, QString text, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	line = new Line(text, content);
-
-	content_layout->addWidget(line);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* OneLineSection::GetHeader() {
-	return header;
-}
-
-Line* OneLineSection::GetLine() {
-	return line;
-}
-
-} // namespace TextWidgets
-
-#include "gui/widgets/moc_text.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/window.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,281 @@
+#include "gui/window.h"
+#include "core/anime_db.h"
+#include "core/config.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "gui/dark_theme.h"
+#include "gui/dialog/about.h"
+#include "gui/dialog/settings.h"
+#include "gui/pages/anime_list.h"
+#include "gui/pages/history.h"
+#include "gui/pages/now_playing.h"
+#include "gui/pages/search.h"
+#include "gui/pages/seasons.h"
+#include "gui/pages/statistics.h"
+#include "gui/pages/torrents.h"
+#include "gui/widgets/sidebar.h"
+#include "services/services.h"
+#include "track/media.h"
+#include <QActionGroup>
+#include <QApplication>
+#include <QDebug>
+#include <QFile>
+#include <QHBoxLayout>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <QStackedWidget>
+#include <QTextStream>
+#include <QThreadPool>
+#include <QTimer>
+#include <QToolBar>
+#include <QToolButton>
+#if MACOSX
+#	include "sys/osx/dark_theme.h"
+#elif defined(WIN32)
+#	include "sys/win32/dark_theme.h"
+#endif
+
+enum class Pages {
+	NOW_PLAYING,
+
+	ANIME_LIST,
+	HISTORY,
+	STATISTICS,
+
+	SEARCH,
+	SEASONS,
+	TORRENTS
+};
+
+static void AsyncSynchronize(QStackedWidget* stack) {
+	QThreadPool::globalInstance()->start([stack] {
+		Services::Synchronize();
+		reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)))->Refresh();
+	});
+}
+
+MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
+	main_widget = new QWidget(parent);
+
+	sidebar = new SideBar(main_widget);
+	sidebar->AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
+	sidebar->AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
+	sidebar->AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
+	sidebar->AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
+	sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
+	sidebar->setFixedWidth(128);
+	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+
+	stack = new QStackedWidget(main_widget);
+	stack->addWidget(new NowPlayingPage(main_widget));
+	stack->addWidget(new AnimeListPage(main_widget));
+	stack->addWidget(new HistoryPage(main_widget));
+	stack->addWidget(new StatisticsPage(main_widget));
+	stack->addWidget(new SearchPage(main_widget));
+	stack->addWidget(new SeasonsPage(main_widget));
+	stack->addWidget(new TorrentsPage(main_widget));
+
+	connect(sidebar, &SideBar::CurrentItemChanged, stack, &QStackedWidget::setCurrentIndex);
+	sidebar->SetCurrentItem(static_cast<int>(Pages::ANIME_LIST));
+
+	QHBoxLayout* layout = new QHBoxLayout(main_widget);
+	layout->addWidget(sidebar);
+	layout->addWidget(stack);
+	setCentralWidget(main_widget);
+
+	CreateBars();
+
+	QTimer* timer = new QTimer(this);
+	connect(timer, &QTimer::timeout, this, [this] {
+		NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack->widget(static_cast<int>(Pages::NOW_PLAYING)));
+
+		Filesystem::Path p = Track::Media::GetCurrentPlaying();
+		std::unordered_map<std::string, std::string> elements = Track::Media::GetFileElements(p);
+		int id = Anime::db.GetAnimeFromTitle(elements["title"]);
+		if (id == 0) {
+			page->SetDefault();
+			return;
+		}
+
+		page->SetPlaying(id, elements);
+	});
+	timer->start(5000);
+
+	DarkTheme::SetTheme(session.config.theme);
+}
+
+void MainWindow::CreateBars() {
+	/* Menu Bar */
+	QAction* action;
+	QMenuBar* menubar = new QMenuBar(this);
+	QMenu* menu = menubar->addMenu(tr("&File"));
+
+	QMenu* submenu = menu->addMenu(tr("&Library folders"));
+	action = submenu->addAction(tr("&Add new folder..."));
+
+	action = menu->addAction(tr("&Scan available episodes"));
+
+	menu->addSeparator();
+
+	action = menu->addAction(tr("Play &next episode"));
+	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N));
+	action = menu->addAction(tr("Play &random episode"));
+	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
+
+	menu->addSeparator();
+
+	action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit);
+
+	menu = menubar->addMenu(tr("&Services"));
+	action = menu->addAction(tr("Synchronize &list"), [this] { AsyncSynchronize(stack); });
+	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
+
+	menu->addSeparator();
+
+	submenu = menu->addMenu(tr("&AniList"));
+	action = submenu->addAction(tr("Go to my &profile"));
+	action = submenu->addAction(tr("Go to my &stats"));
+
+	submenu = menu->addMenu(tr("&Kitsu"));
+	action = submenu->addAction(tr("Go to my &feed"));
+	action = submenu->addAction(tr("Go to my &library"));
+	action = submenu->addAction(tr("Go to my &profile"));
+
+	submenu = menu->addMenu(tr("&MyAnimeList"));
+	action = submenu->addAction(tr("Go to my p&anel"));
+	action = submenu->addAction(tr("Go to my &profile"));
+	action = submenu->addAction(tr("Go to my &history"));
+
+	menu = menubar->addMenu(tr("&Tools"));
+	submenu = menu->addMenu(tr("&Export anime list"));
+	action = submenu->addAction(tr("Export as &Markdown..."));
+	action = submenu->addAction(tr("Export as MyAnimeList &XML..."));
+
+	menu->addSeparator();
+
+	action = menu->addAction(tr("Enable anime &recognition"));
+	action->setCheckable(true);
+	action = menu->addAction(tr("Enable auto &sharing"));
+	action->setCheckable(true);
+	action = menu->addAction(tr("Enable &auto synchronization"));
+	action->setCheckable(true);
+
+	menu->addSeparator();
+
+	action = menu->addAction(tr("&Settings"), [this] {
+		SettingsDialog dialog(this);
+		dialog.exec();
+	});
+	action->setMenuRole(QAction::PreferencesRole);
+
+	menu = menubar->addMenu(tr("&View"));
+
+	std::map<QAction*, int> page_to_index_map = {};
+
+	QActionGroup* pages_group = new QActionGroup(this);
+	pages_group->setExclusive(true);
+
+	action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 0;
+
+	action = pages_group->addAction(menu->addAction(tr("&Anime List")));
+	page_to_index_map[action] = 1;
+	action->setCheckable(true);
+	action->setChecked(true);
+
+	action = pages_group->addAction(menu->addAction(tr("&History")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 2;
+
+	action = pages_group->addAction(menu->addAction(tr("&Statistics")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 3;
+
+	action = pages_group->addAction(menu->addAction(tr("S&earch")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 4;
+
+	action = pages_group->addAction(menu->addAction(tr("Se&asons")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 5;
+
+	action = pages_group->addAction(menu->addAction(tr("&Torrents")));
+	action->setCheckable(true);
+	page_to_index_map[action] = 6;
+
+	connect(sidebar, &SideBar::CurrentItemChanged, this,
+	        [pages_group](int index) { pages_group->actions()[index]->setChecked(true); });
+
+	connect(pages_group, &QActionGroup::triggered, this,
+	        [this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); });
+
+	menu->addSeparator();
+	menu->addAction(tr("Show sidebar"));
+
+	menu = menubar->addMenu(tr("&Help"));
+	action = menu->addAction(tr("&About Minori"), this, [this] {
+		AboutWindow dialog(this);
+		dialog.exec();
+	});
+	action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
+	action->setMenuRole(QAction::AboutQtRole);
+
+	setMenuBar(menubar);
+
+	/* Toolbar */
+	/* remove old toolbar(s) */
+	QList<QToolBar*> toolbars = findChildren<QToolBar*>();
+	for (auto& t : toolbars)
+		removeToolBar(t);
+
+	QToolBar* toolbar = new QToolBar(this);
+	toolbar->addAction(QIcon(":/icons/24x24/arrow-circle-double-135.png"), tr("&Synchronize"),
+	                   [this] { AsyncSynchronize(stack); });
+	toolbar->addSeparator();
+
+	QToolButton* button = new QToolButton(toolbar);
+
+	menu = new QMenu(button);
+	action = menu->addAction(tr("Add new folder..."));
+
+	button->setMenu(menu);
+	button->setIcon(QIcon(":/icons/24x24/folder-open.png"));
+	button->setPopupMode(QToolButton::InstantPopup);
+	toolbar->addWidget(button);
+
+	button = new QToolButton(toolbar);
+
+	menu = new QMenu(button);
+	action = menu->addAction(tr("Placeholder"));
+
+	button->setMenu(menu);
+	button->setIcon(QIcon(":/icons/24x24/application-export.png"));
+	button->setPopupMode(QToolButton::InstantPopup);
+	toolbar->addWidget(button);
+
+	toolbar->addSeparator();
+	toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] {
+		SettingsDialog dialog(this);
+		dialog.exec();
+	});
+	addToolBar(toolbar);
+
+}
+
+void MainWindow::SetActivePage(QWidget* page) {
+	this->setCentralWidget(page);
+}
+
+void MainWindow::closeEvent(QCloseEvent* event) {
+	session.config.Save();
+	event->accept();
+}
+
+#include "gui/moc_window.cpp"
--- a/src/gui/window.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,281 +0,0 @@
-#include "gui/window.h"
-#include "core/anime_db.h"
-#include "core/config.h"
-#include "core/session.h"
-#include "core/strings.h"
-#include "gui/dark_theme.h"
-#include "gui/dialog/about.h"
-#include "gui/dialog/settings.h"
-#include "gui/pages/anime_list.h"
-#include "gui/pages/history.h"
-#include "gui/pages/now_playing.h"
-#include "gui/pages/search.h"
-#include "gui/pages/seasons.h"
-#include "gui/pages/statistics.h"
-#include "gui/pages/torrents.h"
-#include "gui/widgets/sidebar.h"
-#include "services/services.h"
-#include "track/media.h"
-#include <QActionGroup>
-#include <QApplication>
-#include <QDebug>
-#include <QFile>
-#include <QHBoxLayout>
-#include <QMainWindow>
-#include <QMenuBar>
-#include <QMessageBox>
-#include <QPlainTextEdit>
-#include <QStackedWidget>
-#include <QTextStream>
-#include <QThreadPool>
-#include <QTimer>
-#include <QToolBar>
-#include <QToolButton>
-#if MACOSX
-#	include "sys/osx/dark_theme.h"
-#elif defined(WIN32)
-#	include "sys/win32/dark_theme.h"
-#endif
-
-enum class Pages {
-	NOW_PLAYING,
-
-	ANIME_LIST,
-	HISTORY,
-	STATISTICS,
-
-	SEARCH,
-	SEASONS,
-	TORRENTS
-};
-
-static void AsyncSynchronize(QStackedWidget* stack) {
-	QThreadPool::globalInstance()->start([stack] {
-		Services::Synchronize();
-		reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)))->Refresh();
-	});
-}
-
-MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
-	main_widget = new QWidget(parent);
-
-	sidebar = new SideBar(main_widget);
-	sidebar->AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
-	sidebar->AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
-	sidebar->AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
-	sidebar->AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
-	sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
-	sidebar->setFixedWidth(128);
-	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
-
-	stack = new QStackedWidget(main_widget);
-	stack->addWidget(new NowPlayingPage(main_widget));
-	stack->addWidget(new AnimeListPage(main_widget));
-	stack->addWidget(new HistoryPage(main_widget));
-	stack->addWidget(new StatisticsPage(main_widget));
-	stack->addWidget(new SearchPage(main_widget));
-	stack->addWidget(new SeasonsPage(main_widget));
-	stack->addWidget(new TorrentsPage(main_widget));
-
-	connect(sidebar, &SideBar::CurrentItemChanged, stack, &QStackedWidget::setCurrentIndex);
-	sidebar->SetCurrentItem(static_cast<int>(Pages::ANIME_LIST));
-
-	QHBoxLayout* layout = new QHBoxLayout(main_widget);
-	layout->addWidget(sidebar);
-	layout->addWidget(stack);
-	setCentralWidget(main_widget);
-
-	CreateBars();
-
-	QTimer* timer = new QTimer(this);
-	connect(timer, &QTimer::timeout, this, [this] {
-		NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack->widget(static_cast<int>(Pages::NOW_PLAYING)));
-
-		Filesystem::Path p = Track::Media::GetCurrentPlaying();
-		std::unordered_map<std::string, std::string> elements = Track::Media::GetFileElements(p);
-		int id = Anime::db.GetAnimeFromTitle(elements["title"]);
-		if (id == 0) {
-			page->SetDefault();
-			return;
-		}
-
-		page->SetPlaying(id, elements);
-	});
-	timer->start(5000);
-
-	DarkTheme::SetTheme(session.config.theme);
-}
-
-void MainWindow::CreateBars() {
-	/* Menu Bar */
-	QAction* action;
-	QMenuBar* menubar = new QMenuBar(this);
-	QMenu* menu = menubar->addMenu(tr("&File"));
-
-	QMenu* submenu = menu->addMenu(tr("&Library folders"));
-	action = submenu->addAction(tr("&Add new folder..."));
-
-	action = menu->addAction(tr("&Scan available episodes"));
-
-	menu->addSeparator();
-
-	action = menu->addAction(tr("Play &next episode"));
-	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N));
-	action = menu->addAction(tr("Play &random episode"));
-	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
-
-	menu->addSeparator();
-
-	action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit);
-
-	menu = menubar->addMenu(tr("&Services"));
-	action = menu->addAction(tr("Synchronize &list"), [this] { AsyncSynchronize(stack); });
-	action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
-
-	menu->addSeparator();
-
-	submenu = menu->addMenu(tr("&AniList"));
-	action = submenu->addAction(tr("Go to my &profile"));
-	action = submenu->addAction(tr("Go to my &stats"));
-
-	submenu = menu->addMenu(tr("&Kitsu"));
-	action = submenu->addAction(tr("Go to my &feed"));
-	action = submenu->addAction(tr("Go to my &library"));
-	action = submenu->addAction(tr("Go to my &profile"));
-
-	submenu = menu->addMenu(tr("&MyAnimeList"));
-	action = submenu->addAction(tr("Go to my p&anel"));
-	action = submenu->addAction(tr("Go to my &profile"));
-	action = submenu->addAction(tr("Go to my &history"));
-
-	menu = menubar->addMenu(tr("&Tools"));
-	submenu = menu->addMenu(tr("&Export anime list"));
-	action = submenu->addAction(tr("Export as &Markdown..."));
-	action = submenu->addAction(tr("Export as MyAnimeList &XML..."));
-
-	menu->addSeparator();
-
-	action = menu->addAction(tr("Enable anime &recognition"));
-	action->setCheckable(true);
-	action = menu->addAction(tr("Enable auto &sharing"));
-	action->setCheckable(true);
-	action = menu->addAction(tr("Enable &auto synchronization"));
-	action->setCheckable(true);
-
-	menu->addSeparator();
-
-	action = menu->addAction(tr("&Settings"), [this] {
-		SettingsDialog dialog(this);
-		dialog.exec();
-	});
-	action->setMenuRole(QAction::PreferencesRole);
-
-	menu = menubar->addMenu(tr("&View"));
-
-	std::map<QAction*, int> page_to_index_map = {};
-
-	QActionGroup* pages_group = new QActionGroup(this);
-	pages_group->setExclusive(true);
-
-	action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 0;
-
-	action = pages_group->addAction(menu->addAction(tr("&Anime List")));
-	page_to_index_map[action] = 1;
-	action->setCheckable(true);
-	action->setChecked(true);
-
-	action = pages_group->addAction(menu->addAction(tr("&History")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 2;
-
-	action = pages_group->addAction(menu->addAction(tr("&Statistics")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 3;
-
-	action = pages_group->addAction(menu->addAction(tr("S&earch")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 4;
-
-	action = pages_group->addAction(menu->addAction(tr("Se&asons")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 5;
-
-	action = pages_group->addAction(menu->addAction(tr("&Torrents")));
-	action->setCheckable(true);
-	page_to_index_map[action] = 6;
-
-	connect(sidebar, &SideBar::CurrentItemChanged, this,
-	        [pages_group](int index) { pages_group->actions()[index]->setChecked(true); });
-
-	connect(pages_group, &QActionGroup::triggered, this,
-	        [this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); });
-
-	menu->addSeparator();
-	menu->addAction(tr("Show sidebar"));
-
-	menu = menubar->addMenu(tr("&Help"));
-	action = menu->addAction(tr("&About Minori"), this, [this] {
-		AboutWindow dialog(this);
-		dialog.exec();
-	});
-	action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
-	action->setMenuRole(QAction::AboutQtRole);
-
-	setMenuBar(menubar);
-
-	/* Toolbar */
-	/* remove old toolbar(s) */
-	QList<QToolBar*> toolbars = findChildren<QToolBar*>();
-	for (auto& t : toolbars)
-		removeToolBar(t);
-
-	QToolBar* toolbar = new QToolBar(this);
-	toolbar->addAction(QIcon(":/icons/24x24/arrow-circle-double-135.png"), tr("&Synchronize"),
-	                   [this] { AsyncSynchronize(stack); });
-	toolbar->addSeparator();
-
-	QToolButton* button = new QToolButton(toolbar);
-
-	menu = new QMenu(button);
-	action = menu->addAction(tr("Add new folder..."));
-
-	button->setMenu(menu);
-	button->setIcon(QIcon(":/icons/24x24/folder-open.png"));
-	button->setPopupMode(QToolButton::InstantPopup);
-	toolbar->addWidget(button);
-
-	button = new QToolButton(toolbar);
-
-	menu = new QMenu(button);
-	action = menu->addAction(tr("Placeholder"));
-
-	button->setMenu(menu);
-	button->setIcon(QIcon(":/icons/24x24/application-export.png"));
-	button->setPopupMode(QToolButton::InstantPopup);
-	toolbar->addWidget(button);
-
-	toolbar->addSeparator();
-	toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] {
-		SettingsDialog dialog(this);
-		dialog.exec();
-	});
-	addToolBar(toolbar);
-
-}
-
-void MainWindow::SetActivePage(QWidget* page) {
-	this->setCentralWidget(page);
-}
-
-void MainWindow::closeEvent(QCloseEvent* event) {
-	session.config.Save();
-	event->accept();
-}
-
-#include "gui/moc_window.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,20 @@
+#include "core/session.h"
+#include "gui/window.h"
+#include <QApplication>
+#include <QStyleFactory>
+
+Session session;
+
+int main(int argc, char** argv) {
+	QApplication app(argc, argv);
+
+	session.config.Load();
+
+	MainWindow window;
+
+	window.resize(941, 750);
+	window.setWindowTitle("Minori");
+	window.show();
+
+	return app.exec();
+}
--- a/src/main.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-#include "core/session.h"
-#include "gui/window.h"
-#include <QApplication>
-#include <QStyleFactory>
-
-Session session;
-
-int main(int argc, char** argv) {
-	QApplication app(argc, argv);
-
-	session.config.Load();
-
-	MainWindow window;
-
-	window.resize(941, 750);
-	window.setWindowTitle("Minori");
-	window.show();
-
-	return app.exec();
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/services/anilist.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,320 @@
+#include "services/anilist.h"
+#include "core/anime.h"
+#include "core/anime_db.h"
+#include "core/config.h"
+#include "core/http.h"
+#include "core/json.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "gui/translate/anilist.h"
+#include <QByteArray>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QLineEdit>
+#include <QMessageBox>
+#include <QUrl>
+#include <chrono>
+#include <exception>
+#define CLIENT_ID "13706"
+
+using namespace nlohmann::literals::json_literals;
+
+namespace Services {
+namespace AniList {
+
+class Account {
+	public:
+		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.config.anilist.user_id; }
+		void SetUserId(const int id) { session.config.anilist.user_id = id; }
+
+		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;
+
+std::string SendRequest(std::string data) {
+	std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json",
+	                                    "Content-Type: application/json"};
+	return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
+}
+
+void ParseListStatus(std::string status, Anime::Anime& anime) {
+	std::unordered_map<std::string, Anime::ListStatus> map = {
+	    {"CURRENT",   Anime::ListStatus::CURRENT  },
+	    {"PLANNING",  Anime::ListStatus::PLANNING },
+	    {"COMPLETED", Anime::ListStatus::COMPLETED},
+	    {"DROPPED",   Anime::ListStatus::DROPPED  },
+	    {"PAUSED",    Anime::ListStatus::PAUSED   }
+    };
+
+	if (status == "REPEATING") {
+		anime.SetUserIsRewatching(true);
+		anime.SetUserStatus(Anime::ListStatus::CURRENT);
+		return;
+	}
+
+	if (map.find(status) == map.end()) {
+		anime.SetUserStatus(Anime::ListStatus::NOT_IN_LIST);
+		return;
+	}
+
+	anime.SetUserStatus(map[status]);
+}
+
+std::string ListStatusToString(const Anime::Anime& anime) {
+	if (anime.GetUserIsRewatching())
+		return "REWATCHING";
+
+	switch (anime.GetUserStatus()) {
+		case Anime::ListStatus::PLANNING: return "PLANNING";
+		case Anime::ListStatus::COMPLETED: return "COMPLETED";
+		case Anime::ListStatus::DROPPED: return "DROPPED";
+		case Anime::ListStatus::PAUSED: return "PAUSED";
+		default: break;
+	}
+	return "CURRENT";
+}
+
+Date ParseDate(const nlohmann::json& json) {
+	Date date;
+	/* JSON for Modern C++ warns here. I'm not too sure why, this code works when I set the
+	   standard to C++17 :/ */
+	if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number())
+		date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
+	else
+		date.VoidYear();
+
+	if (json.contains("/month"_json_pointer) && json.at("/month"_json_pointer).is_number())
+		date.SetMonth(JSON::GetInt(json, "/month"_json_pointer));
+	else
+		date.VoidMonth();
+
+	if (json.contains("/day"_json_pointer) && json.at("/day"_json_pointer).is_number())
+		date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
+	else
+		date.VoidDay();
+	return date;
+}
+
+void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
+	anime.SetNativeTitle(JSON::GetString(json, "/native"_json_pointer));
+	anime.SetEnglishTitle(JSON::GetString(json, "/english"_json_pointer));
+	anime.SetRomajiTitle(JSON::GetString(json, "/romaji"_json_pointer));
+}
+
+int ParseMediaJson(const nlohmann::json& json) {
+	int id = JSON::GetInt(json, "/id"_json_pointer);
+	if (!id)
+		return 0;
+	Anime::Anime& anime = Anime::db.items[id];
+	anime.SetId(id);
+
+	ParseTitle(json.at("/title"_json_pointer), anime);
+
+	anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
+	anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString(json, "/format"_json_pointer)));
+
+	anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString(json, "/status"_json_pointer)));
+
+	anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
+
+	anime.SetPosterUrl(JSON::GetString(json, "/coverImage/large"_json_pointer));
+
+	anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
+	anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString(json, "/season"_json_pointer)));
+	anime.SetDuration(JSON::GetInt(json, "/duration"_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.SetTitleSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
+	return id;
+}
+
+int ParseListItem(const nlohmann::json& json) {
+	int id = ParseMediaJson(json["media"]);
+
+	Anime::Anime& anime = Anime::db.items[id];
+
+	anime.AddToUserList();
+
+	anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer));
+	anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer));
+	ParseListStatus(JSON::GetString(json, "/status"_json_pointer), anime);
+	anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer));
+
+	anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
+	anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
+
+	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() {
+	/* NOTE: these should be in the qrc file */
+	const std::string query = "query ($id: Int) {\n"
+	                          "  MediaListCollection (userId: $id, type: ANIME) {\n"
+	                          "    lists {\n"
+	                          "      name\n"
+	                          "      entries {\n"
+	                          "        score\n"
+	                          "        notes\n"
+	                          "        status\n"
+	                          "        progress\n"
+	                          "        startedAt {\n"
+	                          "          year\n"
+	                          "          month\n"
+	                          "          day\n"
+	                          "        }\n"
+	                          "        completedAt {\n"
+	                          "          year\n"
+	                          "          month\n"
+	                          "          day\n"
+	                          "        }\n"
+	                          "        updatedAt\n"
+	                          "        media {\n"
+	                          "          coverImage {\n"
+	                          "            large\n"
+	                          "          }\n"
+	                          "          id\n"
+	                          "          title {\n"
+	                          "            romaji\n"
+	                          "            english\n"
+	                          "            native\n"
+	                          "          }\n"
+	                          "          format\n"
+	                          "          status\n"
+	                          "          averageScore\n"
+	                          "          season\n"
+	                          "          startDate {\n"
+	                          "            year\n"
+	                          "            month\n"
+	                          "            day\n"
+	                          "          }\n"
+	                          "          genres\n"
+	                          "          episodes\n"
+	                          "          duration\n"
+	                          "          synonyms\n"
+	                          "          description(asHtml: false)\n"
+	                          "        }\n"
+	                          "      }\n"
+	                          "    }\n"
+	                          "  }\n"
+	                          "}\n";
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"id", account.UserId()}
+		}}
+	};
+	// clang-format on
+	/* TODO: do a try catch here, catch any json errors and then call
+	   Authorize() if needed */
+	auto res = nlohmann::json::parse(SendRequest(json.dump()));
+	/* 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.value());
+	}
+	return 1;
+}
+
+int UpdateAnimeEntry(int id) {
+	/**
+	 * possible values:
+	 *
+	 * int mediaId,
+	 * MediaListStatus status,
+	 * float score,
+	 * int scoreRaw,
+	 * int progress,
+	 * int progressVolumes,
+	 * int repeat,
+	 * int priority,
+	 * bool private,
+	 * string notes,
+	 * bool hiddenFromStatusLists,
+	 * string[] customLists,
+	 * float[] advancedScores,
+	 * Date startedAt,
+	 * Date completedAt
+	 **/
+	Anime::Anime& anime = Anime::db.items[id];
+	const std::string query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, "
+	                          "$notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput) {\n"
+	                          "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, "
+	                          "scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp) {\n"
+	                          "    id\n"
+	                          "  }\n"
+	                          "}\n";
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"media_id", anime.GetId()},
+			{"progress", anime.GetUserProgress()},
+			{"status",   ListStatusToString(anime)},
+			{"score",    anime.GetUserScore()},
+			{"notes",    anime.GetUserNotes()},
+			{"start",    anime.GetUserDateStarted().GetAsAniListJson()},
+			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()}
+		}}
+	};
+	// clang-format on
+	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+	return JSON::GetInt(ret, "/data/SaveMediaListEntry/id"_json_pointer);
+}
+
+int ParseUser(const nlohmann::json& json) {
+	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
+	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
+	return account.UserId();
+}
+
+bool AuthorizeUser() {
+	/* Prompt for PIN */
+	QDesktopServices::openUrl(
+	    QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
+	bool ok;
+	QString token = QInputDialog::getText(
+	    0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
+	    "", &ok);
+	if (ok && !token.isEmpty())
+		account.SetAuthToken(Strings::ToUtf8String(token));
+	else // fail
+		return false;
+	const std::string query = "query {\n"
+	                          "  Viewer {\n"
+	                          "    id\n"
+	                          "    name\n"
+	                          "    mediaListOptions {\n"
+	                          "      scoreFormat\n"
+	                          "    }\n"
+	                          "  }\n"
+	                          "}\n";
+	nlohmann::json json = {
+	    {"query", query}
+    };
+	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+	ParseUser(ret["data"]["Viewer"]);
+	return true;
+}
+
+} // namespace AniList
+} // namespace Services
--- a/src/services/anilist.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,320 +0,0 @@
-#include "services/anilist.h"
-#include "core/anime.h"
-#include "core/anime_db.h"
-#include "core/config.h"
-#include "core/http.h"
-#include "core/json.h"
-#include "core/session.h"
-#include "core/strings.h"
-#include "gui/translate/anilist.h"
-#include <QByteArray>
-#include <QDesktopServices>
-#include <QInputDialog>
-#include <QLineEdit>
-#include <QMessageBox>
-#include <QUrl>
-#include <chrono>
-#include <exception>
-#define CLIENT_ID "13706"
-
-using namespace nlohmann::literals::json_literals;
-
-namespace Services {
-namespace AniList {
-
-class Account {
-	public:
-		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.config.anilist.user_id; }
-		void SetUserId(const int id) { session.config.anilist.user_id = id; }
-
-		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;
-
-std::string SendRequest(std::string data) {
-	std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json",
-	                                    "Content-Type: application/json"};
-	return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
-}
-
-void ParseListStatus(std::string status, Anime::Anime& anime) {
-	std::unordered_map<std::string, Anime::ListStatus> map = {
-	    {"CURRENT",   Anime::ListStatus::CURRENT  },
-	    {"PLANNING",  Anime::ListStatus::PLANNING },
-	    {"COMPLETED", Anime::ListStatus::COMPLETED},
-	    {"DROPPED",   Anime::ListStatus::DROPPED  },
-	    {"PAUSED",    Anime::ListStatus::PAUSED   }
-    };
-
-	if (status == "REPEATING") {
-		anime.SetUserIsRewatching(true);
-		anime.SetUserStatus(Anime::ListStatus::CURRENT);
-		return;
-	}
-
-	if (map.find(status) == map.end()) {
-		anime.SetUserStatus(Anime::ListStatus::NOT_IN_LIST);
-		return;
-	}
-
-	anime.SetUserStatus(map[status]);
-}
-
-std::string ListStatusToString(const Anime::Anime& anime) {
-	if (anime.GetUserIsRewatching())
-		return "REWATCHING";
-
-	switch (anime.GetUserStatus()) {
-		case Anime::ListStatus::PLANNING: return "PLANNING";
-		case Anime::ListStatus::COMPLETED: return "COMPLETED";
-		case Anime::ListStatus::DROPPED: return "DROPPED";
-		case Anime::ListStatus::PAUSED: return "PAUSED";
-		default: break;
-	}
-	return "CURRENT";
-}
-
-Date ParseDate(const nlohmann::json& json) {
-	Date date;
-	/* JSON for Modern C++ warns here. I'm not too sure why, this code works when I set the
-	   standard to C++17 :/ */
-	if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number())
-		date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
-	else
-		date.VoidYear();
-
-	if (json.contains("/month"_json_pointer) && json.at("/month"_json_pointer).is_number())
-		date.SetMonth(JSON::GetInt(json, "/month"_json_pointer));
-	else
-		date.VoidMonth();
-
-	if (json.contains("/day"_json_pointer) && json.at("/day"_json_pointer).is_number())
-		date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
-	else
-		date.VoidDay();
-	return date;
-}
-
-void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
-	anime.SetNativeTitle(JSON::GetString(json, "/native"_json_pointer));
-	anime.SetEnglishTitle(JSON::GetString(json, "/english"_json_pointer));
-	anime.SetRomajiTitle(JSON::GetString(json, "/romaji"_json_pointer));
-}
-
-int ParseMediaJson(const nlohmann::json& json) {
-	int id = JSON::GetInt(json, "/id"_json_pointer);
-	if (!id)
-		return 0;
-	Anime::Anime& anime = Anime::db.items[id];
-	anime.SetId(id);
-
-	ParseTitle(json.at("/title"_json_pointer), anime);
-
-	anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
-	anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString(json, "/format"_json_pointer)));
-
-	anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString(json, "/status"_json_pointer)));
-
-	anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
-
-	anime.SetPosterUrl(JSON::GetString(json, "/coverImage/large"_json_pointer));
-
-	anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
-	anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString(json, "/season"_json_pointer)));
-	anime.SetDuration(JSON::GetInt(json, "/duration"_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.SetTitleSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
-	return id;
-}
-
-int ParseListItem(const nlohmann::json& json) {
-	int id = ParseMediaJson(json["media"]);
-
-	Anime::Anime& anime = Anime::db.items[id];
-
-	anime.AddToUserList();
-
-	anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer));
-	anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer));
-	ParseListStatus(JSON::GetString(json, "/status"_json_pointer), anime);
-	anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer));
-
-	anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
-	anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
-
-	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() {
-	/* NOTE: these should be in the qrc file */
-	const std::string query = "query ($id: Int) {\n"
-	                          "  MediaListCollection (userId: $id, type: ANIME) {\n"
-	                          "    lists {\n"
-	                          "      name\n"
-	                          "      entries {\n"
-	                          "        score\n"
-	                          "        notes\n"
-	                          "        status\n"
-	                          "        progress\n"
-	                          "        startedAt {\n"
-	                          "          year\n"
-	                          "          month\n"
-	                          "          day\n"
-	                          "        }\n"
-	                          "        completedAt {\n"
-	                          "          year\n"
-	                          "          month\n"
-	                          "          day\n"
-	                          "        }\n"
-	                          "        updatedAt\n"
-	                          "        media {\n"
-	                          "          coverImage {\n"
-	                          "            large\n"
-	                          "          }\n"
-	                          "          id\n"
-	                          "          title {\n"
-	                          "            romaji\n"
-	                          "            english\n"
-	                          "            native\n"
-	                          "          }\n"
-	                          "          format\n"
-	                          "          status\n"
-	                          "          averageScore\n"
-	                          "          season\n"
-	                          "          startDate {\n"
-	                          "            year\n"
-	                          "            month\n"
-	                          "            day\n"
-	                          "          }\n"
-	                          "          genres\n"
-	                          "          episodes\n"
-	                          "          duration\n"
-	                          "          synonyms\n"
-	                          "          description(asHtml: false)\n"
-	                          "        }\n"
-	                          "      }\n"
-	                          "    }\n"
-	                          "  }\n"
-	                          "}\n";
-	// clang-format off
-	nlohmann::json json = {
-		{"query", query},
-		{"variables", {
-			{"id", account.UserId()}
-		}}
-	};
-	// clang-format on
-	/* TODO: do a try catch here, catch any json errors and then call
-	   Authorize() if needed */
-	auto res = nlohmann::json::parse(SendRequest(json.dump()));
-	/* 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.value());
-	}
-	return 1;
-}
-
-int UpdateAnimeEntry(int id) {
-	/**
-	 * possible values:
-	 *
-	 * int mediaId,
-	 * MediaListStatus status,
-	 * float score,
-	 * int scoreRaw,
-	 * int progress,
-	 * int progressVolumes,
-	 * int repeat,
-	 * int priority,
-	 * bool private,
-	 * string notes,
-	 * bool hiddenFromStatusLists,
-	 * string[] customLists,
-	 * float[] advancedScores,
-	 * Date startedAt,
-	 * Date completedAt
-	 **/
-	Anime::Anime& anime = Anime::db.items[id];
-	const std::string query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, "
-	                          "$notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput) {\n"
-	                          "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, "
-	                          "scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp) {\n"
-	                          "    id\n"
-	                          "  }\n"
-	                          "}\n";
-	// clang-format off
-	nlohmann::json json = {
-		{"query", query},
-		{"variables", {
-			{"media_id", anime.GetId()},
-			{"progress", anime.GetUserProgress()},
-			{"status",   ListStatusToString(anime)},
-			{"score",    anime.GetUserScore()},
-			{"notes",    anime.GetUserNotes()},
-			{"start",    anime.GetUserDateStarted().GetAsAniListJson()},
-			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()}
-		}}
-	};
-	// clang-format on
-	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
-	return JSON::GetInt(ret, "/data/SaveMediaListEntry/id"_json_pointer);
-}
-
-int ParseUser(const nlohmann::json& json) {
-	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
-	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
-	return account.UserId();
-}
-
-bool AuthorizeUser() {
-	/* Prompt for PIN */
-	QDesktopServices::openUrl(
-	    QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
-	bool ok;
-	QString token = QInputDialog::getText(
-	    0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
-	    "", &ok);
-	if (ok && !token.isEmpty())
-		account.SetAuthToken(Strings::ToUtf8String(token));
-	else // fail
-		return false;
-	const std::string query = "query {\n"
-	                          "  Viewer {\n"
-	                          "    id\n"
-	                          "    name\n"
-	                          "    mediaListOptions {\n"
-	                          "      scoreFormat\n"
-	                          "    }\n"
-	                          "  }\n"
-	                          "}\n";
-	nlohmann::json json = {
-	    {"query", query}
-    };
-	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
-	ParseUser(ret["data"]["Viewer"]);
-	return true;
-}
-
-} // namespace AniList
-} // namespace Services
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/services/services.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,42 @@
+#include "services/services.h"
+#include "core/session.h"
+#include "gui/dialog/settings.h"
+#include "services/anilist.h"
+#include <QMessageBox>
+
+namespace Services {
+
+void Synchronize() {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: AniList::GetAnimeList(); break;
+		default: {
+			QMessageBox msg;
+			msg.setInformativeText("It seems you haven't yet selected a service to use.");
+			msg.setText("Would you like to select one now?");
+			msg.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+			msg.setDefaultButton(QMessageBox::Yes);
+			int ret = msg.exec();
+			if (ret == QMessageBox::Yes) {
+				SettingsDialog dialog;
+				dialog.exec();
+			}
+			break;
+		}
+	}
+}
+
+void UpdateAnimeEntry(int id) {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: AniList::UpdateAnimeEntry(id); break;
+		default: break;
+	}
+}
+
+bool Authorize() {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: return AniList::AuthorizeUser();
+		default: return true;
+	}
+}
+
+}; // namespace Services
--- a/src/services/services.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-#include "services/services.h"
-#include "core/session.h"
-#include "gui/dialog/settings.h"
-#include "services/anilist.h"
-#include <QMessageBox>
-
-namespace Services {
-
-void Synchronize() {
-	switch (session.config.service) {
-		case Anime::Services::ANILIST: AniList::GetAnimeList(); break;
-		default: {
-			QMessageBox msg;
-			msg.setInformativeText("It seems you haven't yet selected a service to use.");
-			msg.setText("Would you like to select one now?");
-			msg.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
-			msg.setDefaultButton(QMessageBox::Yes);
-			int ret = msg.exec();
-			if (ret == QMessageBox::Yes) {
-				SettingsDialog dialog;
-				dialog.exec();
-			}
-			break;
-		}
-	}
-}
-
-void UpdateAnimeEntry(int id) {
-	switch (session.config.service) {
-		case Anime::Services::ANILIST: AniList::UpdateAnimeEntry(id); break;
-		default: break;
-	}
-}
-
-bool Authorize() {
-	switch (session.config.service) {
-		case Anime::Services::ANILIST: return AniList::AuthorizeUser();
-		default: return true;
-	}
-}
-
-}; // namespace Services
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/sys/win32/dark_theme.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,20 @@
+#include "sys/win32/dark_theme.h"
+#include <QOperatingSystemVersion>
+#include <QSettings>
+
+namespace win32 {
+
+bool DarkThemeAvailable() {
+	// dark mode supported Windows 10 1809 10.0.17763 onward
+	// https://stackoverflow.com/questions/53501268/win10-dark-theme-how-to-use-in-winapi
+	const auto& ver = QOperatingSystemVersion::current();
+	return (ver.majorVersion() > 10) ? true : (ver.majorVersion() == 10 && ver.microVersion() >= 17763);
+}
+
+bool IsInDarkTheme() {
+	QSettings settings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+	                   QSettings::NativeFormat);
+	return settings.value("AppsUseLightTheme", 1).toInt() == 0;
+}
+
+} // namespace win32
--- a/src/sys/win32/dark_theme.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-#include "sys/win32/dark_theme.h"
-#include <QOperatingSystemVersion>
-#include <QSettings>
-
-namespace win32 {
-
-bool DarkThemeAvailable() {
-	// dark mode supported Windows 10 1809 10.0.17763 onward
-	// https://stackoverflow.com/questions/53501268/win10-dark-theme-how-to-use-in-winapi
-	const auto& ver = QOperatingSystemVersion::current();
-	return (ver.majorVersion() > 10) ? true : (ver.majorVersion() == 10 && ver.microVersion() >= 17763);
-}
-
-bool IsInDarkTheme() {
-	QSettings settings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
-	                   QSettings::NativeFormat);
-	return settings.value("AppsUseLightTheme", 1).toInt() == 0;
-}
-
-} // namespace win32
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/track/media.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,53 @@
+#include "track/media.h"
+#include "animia.h"
+#include "anitomy/anitomy.h"
+#include "core/filesystem.h"
+#include "core/strings.h"
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <QDebug>
+
+namespace Track {
+namespace Media {
+
+Filesystem::Path GetCurrentPlaying() {
+	/* getting all open files */
+	std::vector<int> pids = Animia::get_all_pids();
+	for (int i : pids) {
+		if (Animia::get_process_name(i) == "vlc") {
+			std::vector<std::string> files = Animia::filter_system_files(Animia::get_open_files(i));
+			for (std::string s : files) {
+				qDebug() << Strings::ToQString(s);
+				Filesystem::Path p(s);
+				if (p.Extension() == "mp4")
+					return p;
+			}
+		}
+	}
+	return Filesystem::Path();
+}
+
+std::unordered_map<std::string, std::string> GetMapFromElements(const anitomy::Elements& elements) {
+	/* there are way more than this in anitomy, but we only need basic information 
+	   I also just prefer using maps than using the ".get()" stuff which is why I'm doing this */
+	std::unordered_map<std::string, std::string> ret;
+
+	ret["title"] = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle));
+	ret["filename"] = Strings::ToUtf8String(elements.get(anitomy::kElementFileName));
+	ret["language"] = Strings::ToUtf8String(elements.get(anitomy::kElementLanguage));
+	ret["group"] = Strings::ToUtf8String(elements.get(anitomy::kElementReleaseGroup));
+	ret["episode"] = Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber));
+
+	return ret;
+}
+
+std::unordered_map<std::string, std::string> GetFileElements(Filesystem::Path path) {
+	anitomy::Anitomy anitomy;
+	anitomy.Parse(Strings::ToWstring(path.Basename()));
+
+	return GetMapFromElements(anitomy.elements());
+}
+
+} // namespace Media
+} // namespace Track
--- a/src/track/media.cpp	Fri Oct 13 13:15:19 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-#include "track/media.h"
-#include "animia.h"
-#include "anitomy/anitomy.h"
-#include "core/filesystem.h"
-#include "core/strings.h"
-#include <string>
-#include <vector>
-#include <unordered_map>
-#include <QDebug>
-
-namespace Track {
-namespace Media {
-
-Filesystem::Path GetCurrentPlaying() {
-	/* getting all open files */
-	std::vector<int> pids = Animia::get_all_pids();
-	for (int i : pids) {
-		if (Animia::get_process_name(i) == "vlc") {
-			std::vector<std::string> files = Animia::filter_system_files(Animia::get_open_files(i));
-			for (std::string s : files) {
-				qDebug() << Strings::ToQString(s);
-				Filesystem::Path p(s);
-				if (p.Extension() == "mp4")
-					return p;
-			}
-		}
-	}
-	return Filesystem::Path();
-}
-
-std::unordered_map<std::string, std::string> GetMapFromElements(const anitomy::Elements& elements) {
-	/* there are way more than this in anitomy, but we only need basic information 
-	   I also just prefer using maps than using the ".get()" stuff which is why I'm doing this */
-	std::unordered_map<std::string, std::string> ret;
-
-	ret["title"] = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle));
-	ret["filename"] = Strings::ToUtf8String(elements.get(anitomy::kElementFileName));
-	ret["language"] = Strings::ToUtf8String(elements.get(anitomy::kElementLanguage));
-	ret["group"] = Strings::ToUtf8String(elements.get(anitomy::kElementReleaseGroup));
-	ret["episode"] = Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber));
-
-	return ret;
-}
-
-std::unordered_map<std::string, std::string> GetFileElements(Filesystem::Path path) {
-	anitomy::Anitomy anitomy;
-	anitomy.Parse(Strings::ToWstring(path.Basename()));
-
-	return GetMapFromElements(anitomy.elements());
-}
-
-} // namespace Media
-} // namespace Track