changeset 6:1d82f6e04d7d

Update: add first parts to the settings dialog
author Paper <mrpapersonic@gmail.com>
date Wed, 16 Aug 2023 00:49:17 -0400
parents 51ae25154b70
children 07a9095eaeed
files CMakeLists.txt rc/icons.qrc rc/icons/24x24/application-sidebar-list.png rc/icons/24x24/feed.png rc/icons/24x24/globe.png rc/icons/24x24/inbox-film.png rc/icons/24x24/megaphone.png rc/icons/24x24/question.png src/anilist.cpp src/anime.cpp src/config.cpp src/date.cpp src/dialog/information.cpp src/dialog/settings.cpp src/dialog/settings/application.cpp src/dialog/settings/services.cpp src/include/anilist.h src/include/anime.h src/include/config.h src/include/date.h src/include/json.h src/include/progress.h src/include/settings.h src/include/sidebar.h src/include/time_utils.h src/include/ui_utils.h src/include/window.h src/json.cpp src/main.cpp src/progress.cpp src/sidebar.cpp src/time.cpp src/ui_utils.cpp src/window.cpp
diffstat 34 files changed, 998 insertions(+), 500 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sat Aug 12 13:10:34 2023 -0400
+++ b/CMakeLists.txt	Wed Aug 16 00:49:17 2023 -0400
@@ -10,7 +10,12 @@
 	src/json.cpp
 	src/date.cpp
 	src/time.cpp
+	src/sidebar.cpp
+	src/progress.cpp
+	src/dialog/settings.cpp
 	src/dialog/information.cpp
+	src/dialog/settings/services.cpp
+	src/dialog/settings/application.cpp
 	src/ui_utils.cpp
 	src/string_utils.cpp
 	rc/icons.qrc
--- a/rc/icons.qrc	Sat Aug 12 13:10:34 2023 -0400
+++ b/rc/icons.qrc	Wed Aug 16 00:49:17 2023 -0400
@@ -7,8 +7,14 @@
 		<file>icons/16x16/feed.png</file>
 		<file>icons/16x16/film.png</file>
 		<file>icons/16x16/magnifier.png</file>
+		<file>icons/24x24/application-sidebar-list.png</file>
 		<file>icons/24x24/arrow-circle-double-135.png</file>
+		<file>icons/24x24/feed.png</file>
 		<file>icons/24x24/folder-open.png</file>
 		<file>icons/24x24/gear.png</file>
+		<file>icons/24x24/globe.png</file>
+		<file>icons/24x24/inbox-film.png</file>
+		<file>icons/24x24/megaphone.png</file>
+		<file>icons/24x24/question.png</file>
 	</qresource>
 </RCC>
\ No newline at end of file
Binary file rc/icons/24x24/application-sidebar-list.png has changed
Binary file rc/icons/24x24/feed.png has changed
Binary file rc/icons/24x24/globe.png has changed
Binary file rc/icons/24x24/inbox-film.png has changed
Binary file rc/icons/24x24/megaphone.png has changed
Binary file rc/icons/24x24/question.png has changed
--- a/src/anilist.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/anilist.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -65,7 +65,7 @@
 		}}
 	};
 	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
-	return ret["data"]["User"]["id"].get<int>();
+	return JSON::GetInt(ret, "/data/User/id"_json_pointer);
 #undef QUERY
 }
 
@@ -169,42 +169,46 @@
 	for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
 		/* why are the .key() values strings?? */
 		AnimeList anime_list;
-		anime_list.name = StringUtils::Utf8ToWstr(JSON::GetString(list.value(), "name"));
+		anime_list.name = JSON::GetString(list.value(), "/name"_json_pointer);
 		for (const auto& entry : list.value()["entries"].items()) {
 			Anime anime;
-			anime.score = JSON::GetInt(entry.value(), "score");
-			anime.progress = JSON::GetInt(entry.value(), "progress");
-			anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")];
-			anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes"));
+			anime.score = JSON::GetInt(entry.value(), "/score"_json_pointer);
+			anime.progress = JSON::GetInt(entry.value(), "/progress"_json_pointer);
+			anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "/status"_json_pointer)];
+			anime.notes = JSON::GetString(entry.value(), "/notes"_json_pointer);
 
-			anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year"));
-			anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month"));
-			anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day"));
+			anime.started.SetYear(JSON::GetInt(entry.value(), "/startedAt/year"_json_pointer));
+			anime.started.SetMonth(JSON::GetInt(entry.value(), "/startedAt/month"_json_pointer));
+			anime.started.SetDay(JSON::GetInt(entry.value(), "/startedAt/day"_json_pointer));
 
-			anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year"));
-			anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month"));
-			anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day"));
+			anime.completed.SetYear(JSON::GetInt(entry.value(), "/completedAt/year"_json_pointer));
+			anime.completed.SetMonth(JSON::GetInt(entry.value(), "/completedAt/month"_json_pointer));
+			anime.completed.SetDay(JSON::GetInt(entry.value(), "/completedAt/day"_json_pointer));
 
-			anime.updated = JSON::GetInt(entry.value(), "updatedAt");
+			anime.updated = JSON::GetInt(entry.value(), "/updatedAt"_json_pointer);
 
-			anime.title.native  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native"));
-			anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english"));
-			anime.title.romaji  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji"));
-
-			anime.id = JSON::GetInt(entry.value()["media"], "id");
-			anime.episodes = JSON::GetInt(entry.value()["media"], "episodes");
-			anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")];
+			anime.title.native  = JSON::GetString(entry.value(), "/media/title/native"_json_pointer);
+			anime.title.english = JSON::GetString(entry.value(), "/media/title/english"_json_pointer);
+			anime.title.romaji  = JSON::GetString(entry.value(), "/media/title/romaji"_json_pointer);
+			/* fallback to romaji if english is not available
+			   note that this takes up more space in memory and is stinky */
+			if (anime.title.english.empty())
+				anime.title.english = anime.title.romaji;
 
-			anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")];
+			anime.id = JSON::GetInt(entry.value(), "/media/id"_json_pointer);
+			anime.episodes = JSON::GetInt(entry.value(), "/media/episodes"_json_pointer);
+			anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "/media/format"_json_pointer)];
+
+			anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "/media/status"_json_pointer)];
 
-			anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year"));
-			anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month"));
-			anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day"));
+			anime.air_date.SetYear(JSON::GetInt(entry.value(), "/media/startDate/year"_json_pointer));
+			anime.air_date.SetMonth(JSON::GetInt(entry.value(), "/media/startDate/month"_json_pointer));
+			anime.air_date.SetDay(JSON::GetInt(entry.value(), "/media/startDate/day"_json_pointer));
 
-			anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore");
-			anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")];
-			anime.duration = JSON::GetInt(entry.value()["media"], "duration");
-			anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration")));
+			anime.audience_score = JSON::GetInt(entry.value(), "/media/averageScore"_json_pointer);
+			anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value(), "/media/season"_json_pointer)];
+			anime.duration = JSON::GetInt(entry.value(), "/media/duration"_json_pointer);
+			anime.synopsis = StringUtils::TextifySynopsis(JSON::GetString(entry.value(), "/media/description"_json_pointer));
 
 			if (entry.value()["media"]["genres"].is_array())
 				anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
@@ -217,16 +221,14 @@
 }
 
 int AniList::Authorize() {
-	if (session.config.anilist.auth_token.empty()) {
-		/* 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()) {
-			session.config.anilist.auth_token = token.toStdString();
-		} else { // fail
-			return 0;
-		}
+	/* 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()) {
+		session.config.anilist.auth_token = token.toStdString();
+	} else { // fail
+		return 0;
 	}
 	return 1;
 }
--- a/src/anime.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/anime.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -10,6 +10,7 @@
 #include "time_utils.h"
 #include "information.h"
 #include "ui_utils.h"
+#include <iostream>
 
 std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
 	{CURRENT,   "Watching"},
@@ -74,6 +75,12 @@
 	duration = a.duration;
 }
 
+std::string Anime::GetUserPreferredTitle() {
+	if (title.english.empty())
+		return title.romaji;
+	return title.english;
+}
+
 void AnimeList::Add(Anime& anime) {
 	if (anime_id_to_anime.contains(anime.id))
 		return;
@@ -156,6 +163,58 @@
 
 /* ------------------------------------------------------------------------- */
 
+AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent)
+	: QStyledItemDelegate (parent) {
+}
+
+QWidget *AnimeListWidgetDelegate::createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const
+{
+    // LOL
+    return nullptr;
+}
+
+void AnimeListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+    switch (index.column()) {
+		case AnimeListWidgetModel::AL_PROGRESS: {
+			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
+			const int episodes = static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
+
+			QStyleOptionViewItem customOption (option);
+			customOption.state.setFlag(QStyle::State_Enabled, true);
+
+			progress_bar.paint(painter, customOption, index.data().toString(), progress, episodes);
+			break;
+		}
+		default:
+			QStyledItemDelegate::paint(painter, option, index);
+			break;
+    }
+}
+
+AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject *parent)
+    : QSortFilterProxyModel(parent) {
+}
+
+bool AnimeListWidgetSortFilter::lessThan(const QModelIndex &l,
+                                         const QModelIndex &r) const {
+    QVariant left  = sourceModel()->data(l, sortRole());
+    QVariant right = sourceModel()->data(r, sortRole());
+
+	switch (left.userType()) {
+		case QMetaType::Int:
+		case QMetaType::UInt:
+		case QMetaType::LongLong:
+		case QMetaType::ULongLong:
+			return left.toInt() < right.toInt();
+		case QMetaType::QDate:
+			return left.toDate() < right.toDate();
+		case QMetaType::QString:
+		default:
+			return left.toString() < right.toString();
+	}
+}
+
 /* Thank you qBittorrent for having a great example of a
    widget model. */
 AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
@@ -181,6 +240,8 @@
 				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:
@@ -206,6 +267,7 @@
 			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:
@@ -229,53 +291,75 @@
 QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
 	if (!index.isValid())
 		return QVariant();
-	if (role == Qt::DisplayRole) {
-		switch (index.column()) {
-			case AL_TITLE:
-				return QString::fromWCharArray(list[index.row()].title.english.c_str());
-			case AL_PROGRESS:
-				return list[index.row()].progress;
-			case AL_SCORE:
-				return list[index.row()].score;
-			case AL_TYPE:
-				return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
-			case AL_SEASON:
-				return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number(list[index.row()].air_date.GetYear());
-			case AL_AVG_SCORE:
-				return list[index.row()].audience_score;
-			case AL_STARTED:
-				return list[index.row()].started.GetAsQDate();
-			case AL_COMPLETED:
-				return list[index.row()].completed.GetAsQDate();
-			case AL_UPDATED: {
-				if (list[index.row()].updated == 0)
-					return QString("-");
-				Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
-				return QString::fromStdString(duration.AsRelativeString());
+	switch (role) {
+		case Qt::DisplayRole:
+			switch (index.column()) {
+				case AL_TITLE:
+					return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
+				case AL_PROGRESS:
+					return QString::number(list[index.row()].progress) + "/" + QString::number(list[index.row()].episodes);
+				case AL_EPISODES:
+					return list[index.row()].episodes;
+				case AL_SCORE:
+					return list[index.row()].score;
+				case AL_TYPE:
+					return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
+				case AL_SEASON:
+					return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number(list[index.row()].air_date.GetYear());
+				case AL_AVG_SCORE:
+					return QString::number(list[index.row()].audience_score) + "%";
+				case AL_STARTED:
+					return list[index.row()].started.GetAsQDate();
+				case AL_COMPLETED:
+					return list[index.row()].completed.GetAsQDate();
+				case AL_UPDATED: {
+					if (list[index.row()].updated == 0)
+						return QString("-");
+					Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
+					return QString::fromUtf8(duration.AsRelativeString().c_str());
+				}
+				case AL_NOTES:
+					return QString::fromUtf8(list[index.row()].notes.c_str());
+				default:
+					return "";
 			}
-			case AL_NOTES:
-				return QString::fromWCharArray(list[index.row()].notes.c_str());
-			default:
-				return "";
-		}
-	} else if (role == Qt::TextAlignmentRole) {
-		switch (index.column()) {
-			case AL_TITLE:
-			case AL_NOTES:
-				return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
-			case AL_PROGRESS:
-			case AL_TYPE:
-			case AL_SCORE:
-			case AL_AVG_SCORE:
-				return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
-			case AL_SEASON:
-			case AL_STARTED:
-			case AL_COMPLETED:
-			case AL_UPDATED:
-				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
-			default:
-				break;
-		}
+			break;
+		case Qt::UserRole:
+			switch (index.column()) {
+				case AL_PROGRESS:
+					return list[index.row()].progress;
+				case AL_TYPE:
+					return list[index.row()].type;
+				case AL_SEASON:
+					return list[index.row()].air_date.GetAsQDate();
+				case AL_AVG_SCORE:
+					return list[index.row()].audience_score;
+				case AL_UPDATED:
+					return list[index.row()].updated;
+				default:
+					return data(index, Qt::DisplayRole);
+			}
+			break;
+		case Qt::TextAlignmentRole:
+			switch (index.column()) {
+				case AL_TITLE:
+				case AL_NOTES:
+					return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+				case AL_PROGRESS:
+				case AL_EPISODES:
+				case AL_TYPE:
+				case AL_SCORE:
+				case AL_AVG_SCORE:
+					return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+				case AL_SEASON:
+				case AL_STARTED:
+				case AL_COMPLETED:
+				case AL_UPDATED:
+					return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+				default:
+					break;
+			}
+			break;
 	}
 	return QVariant();
 }
@@ -285,27 +369,6 @@
 	emit dataChanged(index(i), index(i));
 }
 
-/* Most of this stuff is const and/or should be edited in the Information dialog
-
-bool AnimeListWidgetModel::setData(const QModelIndex &index, const QVariant &value, int role) {
-	if (!index.isValid() || role != Qt::DisplayRole)
-		return false;
-
-	Anime* const anime = &list[index.row()];
-
-	switch (index.column()) {
-		case AL_TITLE:
-			break;
-		case AL_CATEGORY:
-			break;
-		default:
-			return false;
-	}
-
-	return true;
-}
-*/
-
 int AnimeListWidget::VisibleColumnsCount() const {
     int count = 0;
 
@@ -325,6 +388,7 @@
 	setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
 	setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
 	setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
+	setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
 	setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
 	setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
 	setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
@@ -370,29 +434,44 @@
 }
 
 void AnimeListWidget::DisplayListMenu() {
-	/* throw out any other garbage */
-    const QModelIndexList selected_items = selectionModel()->selectedRows();
-    if (selected_items.size() != 1 || !selected_items.first().isValid()) {
+    QMenu *menu = new QMenu(this);
+    menu->setAttribute(Qt::WA_DeleteOnClose);
+    menu->setTitle(tr("Column visibility"));
+    menu->setToolTipsVisible(true);
+
+    const QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
+    if (!selection.indexes().first().isValid()) {
         return;
 	}
 
-	const QModelIndex index = model->index(selected_items.first().row());
-	Anime* anime = model->GetAnimeFromIndex(index);
-	if (!anime) {
-		return;
-	}
+	QAction* action = menu->addAction("Information", [this, selection]{
+		const QModelIndex index = model->index(selection.indexes().first().row());
+		Anime* anime = model->GetAnimeFromIndex(index);
+		if (!anime) {
+			return;
+		}
 
+		InformationDialog* dialog = new InformationDialog(*anime, model, this);
+
+		dialog->show();
+		dialog->raise();
+		dialog->activateWindow();
+	});
+	menu->popup(QCursor::pos());
 }
 
 void AnimeListWidget::ItemDoubleClicked() {
 	/* throw out any other garbage */
-    const QModelIndexList selected_items = selectionModel()->selectedRows();
-    if (selected_items.size() != 1 || !selected_items.first().isValid()) {
+    const QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
+    if (!selection.indexes().first().isValid()) {
         return;
 	}
 
-	/* TODO: after we implement our sort model, we have to use mapToSource here... */
-	const QModelIndex index = model->index(selected_items.first().row());
+	const QModelIndex index = model->index(selection.indexes().first().row());
+	const QString title = index.siblingAtColumn(AnimeListWidgetModel::AL_TITLE).data(Qt::UserRole).toString();
+	QMessageBox box;
+	box.setText(QString::number(title.size()));
+	box.exec();
 	Anime* anime = model->GetAnimeFromIndex(index);
 	if (!anime) {
 		return;
@@ -407,10 +486,14 @@
 
 AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
                                : QTreeView(parent) {
+	setItemDelegate(new AnimeListWidgetDelegate(this));
 	model = new AnimeListWidgetModel(parent, alist);
-	setModel(model);
+	sort_model = new AnimeListWidgetSortFilter(this);
+	sort_model->setSourceModel(model);
+	sort_model->setSortRole(Qt::UserRole);
+	setModel(sort_model);
 	setObjectName("listwidget");
-	setStyleSheet("QTreeView#listwidget{border-top:0px;}");
+	setStyleSheet("QTreeView#listwidget{border:0px;}");
 	setUniformRowHeights(true);
 	setAllColumnsShowFocus(false);
 	setSortingEnabled(true);
@@ -438,9 +521,11 @@
 
 AnimeListPage::AnimeListPage(QWidget* parent) : QTabWidget (parent) {
 	setDocumentMode(false);
+	setObjectName("animepage");
+	//setStyleSheet("QTabWidget#animepage{border-bottom:0px;border-left:0px;border-right:0px;}");
 	SyncAnimeList();
 	for (AnimeList& list : anime_lists) {
-		addTab(new AnimeListWidget(this, &list), QString::fromWCharArray(list.name.c_str()));
+		addTab(new AnimeListWidget(this, &list), QString::fromUtf8(list.name.c_str()));
 	}
 }
 
@@ -448,7 +533,6 @@
 	switch (session.config.service) {
 		case ANILIST: {
 			AniList anilist = AniList();
-			anilist.Authorize();
 			session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username);
 			FreeAnimeList();
 			anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
@@ -460,14 +544,10 @@
 }
 
 void AnimeListPage::FreeAnimeList() {
-	if (anime_lists.size() > 0) {
-		/* FIXME: we may not need this, but to prevent memleaks
-		   we should keep it until we're sure we don't */
-		for (auto& list : anime_lists) {
-			list.Clear();
-		}
-		anime_lists.clear();
+	for (auto& list : anime_lists) {
+		list.Clear();
 	}
+	anime_lists.clear();
 }
 
 int AnimeListPage::GetTotalAnimeAmount() {
--- a/src/config.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/config.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -31,21 +31,27 @@
 	{DARK, "Dark"}
 };
 
+std::map<enum AnimeListServices, std::string> ServiceToString {
+	{NONE, "None"},
+	{ANILIST, "AniList"}
+};
+
+std::map<std::string, enum AnimeListServices> StringToService {
+	{"None", NONE},
+	{"AniList", ANILIST}
+};
+
 int Config::Load() {
 	std::filesystem::path cfg_path = get_config_path();
 	if (!std::filesystem::exists(cfg_path))
 		return 0;
 	std::ifstream config_in(cfg_path.string().c_str(), std::ifstream::in);
 	auto config_js = nlohmann::json::parse(config_in);
-/* this macro will make it easier to edit these in the future, if needed */
-#define GET_CONFIG_VALUE(pointer, location, struct, default) \
-	struct = (config_js.contains(pointer)) ? (location) : (default)
-	GET_CONFIG_VALUE("/General/Service"_json_pointer, (enum AnimeListServices)config_js["General"]["Service"].get<int>(), service, NONE);
-	GET_CONFIG_VALUE("/Authorization/AniList/Auth Token"_json_pointer, config_js["Authorization"]["AniList"]["Auth Token"].get<std::string>(), anilist.auth_token, "");
-	GET_CONFIG_VALUE("/Authorization/AniList/Username"_json_pointer, config_js["Authorization"]["AniList"]["Username"].get<std::string>(), anilist.username, "");
-	GET_CONFIG_VALUE("/Authorization/AniList/User ID"_json_pointer, config_js["Authorization"]["AniList"]["User ID"].get<int>(), anilist.user_id, 0);
-	GET_CONFIG_VALUE("/Appearance/Theme"_json_pointer, StringToTheme[config_js["Appearance"]["Theme"].get<std::string>()], theme, OS);
-#undef GET_CONFIG_VALUE
+	service = StringToService[JSON::GetString(config_js, "/General/Service"_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_in.close();
 	return 0;
 }
@@ -57,7 +63,7 @@
 	std::ofstream config_out(cfg_path.string().c_str(), std::ofstream::out | std::ofstream::trunc);
 	nlohmann::json config_js = {
 		{"General", {
-			{"Service", service}
+			{"Service", ServiceToString[service]}
 		}},
 		{"Authorization", {
 			{"AniList", {
--- a/src/date.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/date.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -1,54 +1,73 @@
-#include "date.h"
-#include <QDate>
-#include <cstdint>
-
-#define MIN(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
-#define MAX(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; })
-
-#define CLAMP(x, low, high) ({\
-  __typeof__(x) __x = (x); \
-  __typeof__(low) __low = (low);\
-  __typeof__(high) __high = (high);\
-  __x > __high ? __high : (__x < __low ? __low : __x);\
-  })
-
-Date::Date() {
-}
-
-Date::Date(int32_t y) {
-	year = MAX(0, y);
-}
-
-Date::Date(int32_t y, int8_t m, int8_t d) {
-	year = MAX(0, y);
-	month = CLAMP(m, 1, 12);
-	day = CLAMP(d, 1, 31);
-}
-
-void Date::SetYear(int32_t y) {
-	year = MAX(0, y);
-}
-
-void Date::SetMonth(int8_t m) {
-	month = CLAMP(m, 1, 12);
-}
-
-void Date::SetDay(int8_t d) {
-	day = CLAMP(d, 1, 31);
-}
-
-int32_t Date::GetYear() {
-	return year;
-}
-
-int8_t Date::GetMonth() {
-	return month;
-}
-
-int8_t Date::GetDay() {
-	return day;
-}
-
-QDate Date::GetAsQDate() {
-	return QDate(year, month, day);
-}
+#include "date.h"
+#include <QDate>
+#include <cstdint>
+#include <tuple>
+
+#define MIN(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
+#define MAX(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; })
+
+#define CLAMP(x, low, high) ({\
+  __typeof__(x) __x = (x); \
+  __typeof__(low) __low = (low);\
+  __typeof__(high) __high = (high);\
+  __x > __high ? __high : (__x < __low ? __low : __x);\
+  })
+
+Date::Date() {
+}
+
+Date::Date(int32_t y) {
+	year = MAX(0, y);
+}
+
+Date::Date(int32_t y, int8_t m, int8_t d) {
+	year = MAX(0, y);
+	month = CLAMP(m, 1, 12);
+	day = CLAMP(d, 1, 31);
+}
+
+void Date::SetYear(int32_t y) {
+	year = MAX(0, y);
+}
+
+void Date::SetMonth(int8_t m) {
+	month = CLAMP(m, 1, 12);
+}
+
+void Date::SetDay(int8_t d) {
+	day = CLAMP(d, 1, 31);
+}
+
+int32_t Date::GetYear() const {
+	return year;
+}
+
+int8_t Date::GetMonth() const {
+	return month;
+}
+
+int8_t Date::GetDay() const {
+	return day;
+}
+
+bool Date::operator< (const Date& other) const {
+	int o_y = other.GetYear(), o_m = other.GetMonth(), o_d = other.GetDay();
+	return std::tie(year,       month,       day)
+		 < 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() {
+	return QDate(year, month, day);
+}
--- a/src/dialog/information.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/dialog/information.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -25,15 +25,17 @@
 	widget->move(175, 0);
 	widget->setStyleSheet(UiUtils::IsInDarkMode() ? "" : "background-color: white");
 	widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-	QPlainTextEdit* anime_title = new QPlainTextEdit(QString::fromWCharArray(anime->title.english.c_str()), widget);
+	QPlainTextEdit* anime_title = new QPlainTextEdit(QString::fromUtf8(anime->title.english.c_str()), widget);
 	anime_title->setReadOnly(true);
 	anime_title->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 	anime_title->setWordWrapMode(QTextOption::NoWrap);
 	anime_title->setFrameShape(QFrame::NoFrame);
+	anime_title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+	anime_title->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	anime_title->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	anime_title->setStyleSheet("font-size: 16px; color: blue; background: transparent;");
 	anime_title->resize(636, 28);
 	anime_title->move(0, 12);
-	anime_title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-	anime_title->setStyleSheet("font-size: 16px; color: blue");
 	QTabWidget* tabbed_widget = new QTabWidget(widget);
 	tabbed_widget->resize(636, 485);
 	tabbed_widget->move(0, 45);
@@ -49,15 +51,15 @@
 	               << StringUtils::Implode(anime->genres, ", ").c_str() << "\n"
 	               << anime->audience_score << "%\n";
 	UiUtils::CreateTextParagraphWithLabels(main_information_widget, "Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data, QPoint(6, 62), QSize(636-18, 142));
-	UiUtils::CreateSelectableTextParagraph(main_information_widget, "Synopsis", QString::fromWCharArray(anime->synopsis.c_str()), QPoint(6, 202), QSize(636-18, 253));
+	UiUtils::CreateSelectableTextParagraph(main_information_widget, "Synopsis", QString::fromUtf8(anime->synopsis.c_str()), QPoint(6, 202), QSize(636-18, 253));
 	tabbed_widget->addTab(main_information_widget, "Main information");
 	QWidget* settings_widget = new QWidget(tabbed_widget);
 	tabbed_widget->addTab(settings_widget, "My list and settings");
 	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
 	connect(button_box, &QDialogButtonBox::accepted, this, &InformationDialog::OnOK);
 	connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
-	QVBoxLayout* buttons_layout = new QVBoxLayout(widget);
-	buttons_layout->addWidget(widget, 0, Qt::AlignTop);
+	QVBoxLayout* buttons_layout = new QVBoxLayout(this);
+	//buttons_layout->addWidget(widget, 0, Qt::AlignTop);
 	buttons_layout->addWidget(button_box, 0, Qt::AlignBottom);
 	// this should probably be win32-only
 	setStyleSheet(UiUtils::IsInDarkMode() ? "" : "QDialog#infodiag{background-color: white;}");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialog/settings.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,104 @@
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+#include <QPlainTextEdit>
+#include <QPlainTextDocumentLayout>
+#include <QComboBox>
+#include <QGroupBox>
+#include <QWidget>
+#include <QTextDocument>
+#include "settings.h"
+#include "sidebar.h"
+#include "ui_utils.h"
+
+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);
+	page_title->setStyleSheet("QLabel { font-size: 10pt; font-weight: bold; background-color: #ABABAB; color: white; }");
+	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;
+	layout->setMargin(0);
+	layout->addWidget(page_title);
+	layout->addWidget(tab_widget);
+	setLayout(layout);
+}
+
+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::OnSidebar(QListWidgetItem* item) {
+	layout->itemAt(1)->widget()->setVisible(false); // old widget
+	layout->replaceWidget(layout->itemAt(1)->widget(), pages[item->listWidget()->row(item)], Qt::FindDirectChildrenOnly);
+	pages[item->listWidget()->row(item)]->setVisible(true); // new widget
+}
+
+void SettingsDialog::OnOK() {
+	for (const auto& page : pages) {
+		page->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"), UiUtils::CreateSideBarIcon(":/icons/24x24/globe.png")));
+	//sidebar->AddItem(tr("Library"), UiUtils::CreateSideBarIcon(":/icons/24x24/inbox-film.png"));
+	sidebar->AddItem(tr("Application"), UiUtils::CreateSideBarIcon(":/icons/24x24/application-sidebar-list.png"));
+	//sidebar->AddItem(tr("Recognition"), UiUtils::CreateSideBarIcon(":/icons/24x24/question.png"));
+	//sidebar->AddItem(tr("Sharing"), UiUtils::CreateSideBarIcon(":/icons/24x24/megaphone.png"));
+	//sidebar->AddItem(tr("Torrents"), UiUtils::CreateSideBarIcon(":/icons/24x24/feed.png"));
+	//sidebar->AddItem(tr("Advanced"), UiUtils::CreateSideBarIcon(":/icons/24x24/gear.png"));
+	sidebar->setIconSize(QSize(24, 24));
+	sidebar->setFrameShape(QFrame::Box);
+	sidebar->setStyleSheet("QListWidget { background-color: white; font-size: 12px; }");
+	sidebar->setFixedWidth(158);
+	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+	connect(sidebar, &QListWidget::itemActivated, this, &SettingsDialog::OnSidebar);
+	
+	SettingsPageServices* services_page = new SettingsPageServices(this);
+	pages.push_back(services_page);
+	SettingsPageApplication* application_page = new SettingsPageApplication(this);
+	application_page->setVisible(false);
+	pages.push_back(application_page);
+
+	layout = new QHBoxLayout;
+	layout->addWidget(sidebar);
+	layout->addWidget(services_page);
+	layout->setMargin(0);
+	widget->setLayout(layout);
+	
+	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);
+	setLayout(buttons_layout);
+}
+
+#include "moc_settings.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialog/settings/application.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,95 @@
+#include "settings.h"
+#include "anilist.h"
+#include "window.h"
+#include <QGroupBox>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QPushButton>
+#include <QSizePolicy>
+
+QWidget* SettingsPageApplication::CreateAnimeListPage() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QGroupBox* actions_group_box = new QGroupBox(tr("Actions"), result);
+	actions_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	/* Actions/Double click */
+	QWidget* double_click_widget = new QWidget(actions_group_box);
+	QLabel* dc_combo_box_label = new QLabel(tr("Double click:"), double_click_widget);
+	QComboBox* dc_combo_box = new QComboBox(double_click_widget);
+	dc_combo_box->addItem(tr("View anime info"));
+
+	QVBoxLayout* double_click_layout = new QVBoxLayout;
+	double_click_layout->addWidget(dc_combo_box_label);
+	double_click_layout->addWidget(dc_combo_box);
+	double_click_widget->setLayout(double_click_layout);
+
+	/* Actions/Middle click */
+	QWidget* middle_click_widget = new QWidget(actions_group_box);
+	QLabel* mc_combo_box_label = new QLabel(tr("Middle click:"), middle_click_widget);
+	QComboBox* mc_combo_box = new QComboBox(middle_click_widget);
+	mc_combo_box->addItem(tr("Play next episode"));
+
+	QVBoxLayout* middle_click_layout = new QVBoxLayout;
+	middle_click_layout->addWidget(mc_combo_box_label);
+	middle_click_layout->addWidget(mc_combo_box);
+	middle_click_widget->setLayout(middle_click_layout);
+
+	/* Actions */
+	QHBoxLayout* actions_layout = new QHBoxLayout;
+	actions_layout->addWidget(double_click_widget);
+	actions_layout->addWidget(middle_click_widget);
+	actions_group_box->setLayout(actions_layout);
+
+	QGroupBox* appearance_group_box = new QGroupBox(tr("Appearance"), result);
+	appearance_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* lang_combo_box_label = new QLabel(tr("Title language preference:"), appearance_group_box);
+	QComboBox* lang_combo_box = new QComboBox(appearance_group_box);
+	lang_combo_box->addItem(tr("English"));
+	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);
+	hl_above_anime_box->setEnabled((hl_anime_box->checkState() == Qt::Unchecked) ? 0 : 1);
+	hl_above_anime_box->setStyleSheet("margin-left: 10px;");
+
+	connect(hl_anime_box, &QCheckBox::stateChanged, this, [hl_above_anime_box](int state){
+		hl_above_anime_box->setEnabled(state);
+	});
+
+	/* Appearance */
+	QVBoxLayout* appearance_layout = new QVBoxLayout;
+	appearance_layout->addWidget(lang_combo_box_label);
+	appearance_layout->addWidget(lang_combo_box);
+	appearance_layout->addWidget(hl_anime_box);
+	appearance_layout->addWidget(hl_above_anime_box);
+	appearance_group_box->setLayout(appearance_layout);
+
+	QGroupBox* progress_group_box = new QGroupBox(tr("Progress"), result);
+	progress_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QCheckBox* display_aired_episodes = new QCheckBox(tr("Display aired episodes (estimated)"), progress_group_box);
+	QCheckBox* display_available_episodes = new QCheckBox(tr("Display available episodes in library folders"), progress_group_box);
+
+	QVBoxLayout* progress_layout = new QVBoxLayout;
+	progress_layout->addWidget(display_aired_episodes);
+	progress_layout->addWidget(display_available_episodes);
+	progress_group_box->setLayout(progress_layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(actions_group_box);
+	full_layout->addWidget(appearance_group_box);
+	full_layout->addWidget(progress_group_box);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+void SettingsPageApplication::SaveInfo() {
+
+}
+
+SettingsPageApplication::SettingsPageApplication(QWidget* parent)
+	: SettingsPage(parent, tr("Application")) {
+	AddTab(CreateAnimeListPage(), tr("Anime list"));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialog/settings/services.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,86 @@
+#include "settings.h"
+#include "anilist.h"
+#include "window.h"
+#include <QGroupBox>
+#include <QComboBox>
+#include <QPushButton>
+#include <QSizePolicy>
+
+QWidget* SettingsPageServices::CreateMainPage() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QGroupBox* sync_group_box = new QGroupBox(tr("Synchronization"), result);
+	sync_group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* sync_combo_box_label = new QLabel(tr("Active service and metadata provider:"), sync_group_box);
+
+	sync_combo_box = new QComboBox(sync_group_box);
+	sync_combo_box->addItem(tr("AniList"));
+
+	QLabel* sync_note_label = new QLabel(tr("Note: Weeaboo is unable to synchronize multiple services at the same time."), sync_group_box);
+
+	QVBoxLayout* sync_layout = new QVBoxLayout;
+	sync_layout->addWidget(sync_combo_box_label);
+	sync_layout->addWidget(sync_combo_box);
+	sync_layout->addWidget(sync_note_label);
+	sync_group_box->setLayout(sync_layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(sync_group_box);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+QWidget* SettingsPageServices::CreateAniListPage() {
+	QWidget* result = new QWidget(this);
+	result->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+	QGroupBox* group_box = new QGroupBox(tr("Account"), result);
+	group_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+
+	QLabel* username_entry_label = new QLabel(tr("Username: (not your email address)"), group_box);
+
+	QWidget* auth_widget = new QWidget(group_box);
+	username_entry = new QLineEdit(QString::fromUtf8(session.config.anilist.username.c_str()), auth_widget);
+	QPushButton* auth_button = new QPushButton(auth_widget);
+	connect(auth_button, &QPushButton::clicked, this, [this]{
+		AniList a;
+		a.Authorize();
+	});
+	auth_button->setText(session.config.anilist.auth_token.empty() ? tr("Authorize...") : tr("Re-authorize..."));
+
+	QHBoxLayout* auth_layout = new QHBoxLayout;
+	auth_layout->addWidget(username_entry);
+	auth_layout->addWidget(auth_button);
+	auth_widget->setLayout(auth_layout);
+
+	QLabel* note_label = new QLabel(tr("<a href=\"http://anilist.co/\">Create a new AniList account</a>"), group_box);
+	note_label->setTextFormat(Qt::RichText);
+	note_label->setTextInteractionFlags(Qt::TextBrowserInteraction);
+	note_label->setOpenExternalLinks(true);
+
+	QVBoxLayout* layout = new QVBoxLayout;
+	layout->addWidget(username_entry_label);
+	layout->addWidget(auth_widget);
+	layout->addWidget(note_label);
+	group_box->setLayout(layout);
+
+	QVBoxLayout* full_layout = new QVBoxLayout;
+	full_layout->addWidget(group_box);
+	full_layout->addStretch();
+	result->setLayout(full_layout);
+	return result;
+}
+
+void SettingsPageServices::SaveInfo() {
+	session.config.anilist.username = username_entry->displayText().toStdString();
+	session.config.service = static_cast<enum AnimeListServices>(sync_combo_box->currentIndex()+1);
+}
+
+SettingsPageServices::SettingsPageServices(QWidget* parent)
+	: SettingsPage(parent, tr("Services")) {
+	AddTab(CreateMainPage(), tr("Main"));
+	AddTab(CreateAniListPage(), tr("AniList"));
+}
--- a/src/include/anilist.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/anilist.h	Wed Aug 16 00:49:17 2023 -0400
@@ -5,7 +5,7 @@
 #include "json.h"
 class AniList {
 	public:
-		int Authorize();
+		static int Authorize();
 		int GetUserId(std::string name);
 		int UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id);
 
--- a/src/include/anime.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/anime.h	Wed Aug 16 00:49:17 2023 -0400
@@ -2,8 +2,11 @@
 #define __anime_h
 #include <vector>
 #include <map>
+#include <QStyledItemDelegate>
+#include <QProgressBar>
 #include "date.h"
 #include "window.h"
+#include "progress.h"
 
 enum AnimeWatchingStatus {
 	CURRENT,
@@ -54,14 +57,14 @@
 		Date started;
 		Date completed;
 		int updated; /* this should be 64-bit */
-		std::wstring notes;
+		std::string notes;
 
 		/* Useful information */
 		int id;
 		struct {
-			std::wstring romaji;
-			std::wstring english;
-			std::wstring native;
+			std::string romaji;
+			std::string english;
+			std::string native;
 		} title;
 		int episodes;
 		enum AnimeAiringStatus airing;
@@ -71,8 +74,10 @@
 		enum AnimeFormat type;
 		enum AnimeSeason season;
 		int audience_score;
-		std::wstring synopsis;
+		std::string synopsis;
 		int duration;
+		
+		std::string GetUserPreferredTitle();
 };
 
 /* This is a simple wrapper on a vector that provides 
@@ -96,19 +101,44 @@
 		bool AnimeInList(int id);
 		Anime& operator[](size_t index);
 		const Anime& operator[](size_t index) const;
-		std::wstring name;
+		std::string name;
 
-	private:
+	protected:
 		std::vector<Anime> anime_list;
 		std::map<int, Anime*> anime_id_to_anime;
 };
 
+class AnimeListWidgetDelegate : public QStyledItemDelegate {
+    Q_OBJECT
+
+	public:
+		explicit AnimeListWidgetDelegate(QObject *parent);
+
+		QWidget *createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const override;
+		void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+
+	protected:
+		AnimeProgressBar progress_bar;
+};
+
+class AnimeListWidgetSortFilter : public QSortFilterProxyModel
+{
+    Q_OBJECT
+
+	public:
+		AnimeListWidgetSortFilter(QObject *parent = nullptr);
+
+	protected:
+		bool lessThan(const QModelIndex &l, const QModelIndex &r) const override;
+};
+
 class AnimeListWidgetModel : public QAbstractListModel {
 	Q_OBJECT
 	public:
 		enum columns {
 			AL_TITLE,
 			AL_PROGRESS,
+			AL_EPISODES,
 			AL_SCORE,
 			AL_AVG_SCORE,
 			AL_TYPE,
@@ -123,7 +153,6 @@
 
 		AnimeListWidgetModel(QWidget* parent, AnimeList* alist);
 		~AnimeListWidgetModel() override = default;
-		//QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const;
 		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
 		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
 		QVariant data(const QModelIndex& index, int role) const override;
@@ -150,6 +179,7 @@
 
 	private:
 		AnimeListWidgetModel* model = nullptr;
+		AnimeListWidgetSortFilter* sort_model = nullptr;
 };
 
 class AnimeListPage : public QTabWidget {
--- a/src/include/config.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/config.h	Wed Aug 16 00:49:17 2023 -0400
@@ -5,7 +5,8 @@
    whatever reason, so I'll just leave it here */
 enum AnimeListServices {
 	NONE,
-	ANILIST
+	ANILIST,
+	NB_SERVICES
 };
 
 /* todo: make this a class enum */
--- a/src/include/date.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/date.h	Wed Aug 16 00:49:17 2023 -0400
@@ -1,23 +1,27 @@
-#ifndef __date_h
-#define __date_h
-#include <cstdint>
-#include <QDate>
-class Date {
-	public:
-		Date();
-		Date(int32_t y);
-		Date(int32_t y, int8_t m, int8_t d);
-		void SetYear(int32_t y);
-		void SetMonth(int8_t m);
-		void SetDay(int8_t d);
-		int32_t GetYear();
-		int8_t GetMonth();
-		int8_t GetDay();
-		QDate GetAsQDate();
-
-	private:
-		int32_t year = -1;
-		int8_t month = -1;
-		int8_t day = -1;
-};
-#endif // __date_h
\ No newline at end of file
+#ifndef __date_h
+#define __date_h
+#include <cstdint>
+#include <QDate>
+class Date {
+	public:
+		Date();
+		Date(int32_t y);
+		Date(int32_t y, int8_t m, int8_t d);
+		void SetYear(int32_t y);
+		void SetMonth(int8_t m);
+		void SetDay(int8_t d);
+		int32_t GetYear() const;
+		int8_t GetMonth() const;
+		int8_t GetDay() const;
+		QDate GetAsQDate();
+		bool operator< (const Date& other) const;
+		bool operator> (const Date& other) const;
+		bool operator<= (const Date& other) const;
+		bool operator>= (const Date& other) const;
+
+	private:
+		int32_t year = -1;
+		int8_t month = -1;
+		int8_t day = -1;
+};
+#endif // __date_h
--- a/src/include/json.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/json.h	Wed Aug 16 00:49:17 2023 -0400
@@ -1,9 +1,8 @@
-#include "../../dep/json/json.h"
-
-namespace JSON {
-	std::string GetString(nlohmann::json const& json, std::string const& key);
-	int GetInt(nlohmann::json const& json, std::string const& key);
-	int64_t GetInt64(nlohmann::json const& json, std::string const& key);
-	bool GetBoolean(nlohmann::json const& json, std::string const& key);
-	double GetDouble(nlohmann::json const& json, std::string const& key);
-}
+#include "../../dep/json/json.h"
+
+namespace JSON {
+	std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr);
+	int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr);
+	bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr);
+	double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/include/progress.h	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,15 @@
+#ifndef __progress_h
+#define __progress_h
+#include <QPainter>
+#include <QStyleOptionViewItem>
+#include <QString>
+#include <QProgressBar>
+class AnimeProgressBar {
+	public:
+		AnimeProgressBar();
+		void paint(QPainter *painter, const QStyleOptionViewItem &option, const QString &text, const int progress, const int episodes) const;
+
+	private:
+		QProgressBar dummy_progress;
+};
+#endif // __progress_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/include/settings.h	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,60 @@
+#ifndef __settings_h
+#define __settings_h
+#include <QWidget>
+#include <QDialog>
+#include <QTabWidget>
+#include <QLabel>
+#include <QLineEdit>
+#include <QComboBox>
+#include <QHBoxLayout>
+#include "sidebar.h"
+class SettingsPage : public QWidget {
+	Q_OBJECT
+
+	public:
+		SettingsPage(QWidget* parent = nullptr, QString title = "");
+		void SetTitle(QString title);
+		virtual void SaveInfo();
+		void AddTab(QWidget* tab, QString title = "");
+
+	private:
+		QLabel* page_title;
+		QTabWidget* tab_widget;
+};
+
+class SettingsPageServices : public SettingsPage {
+	public:
+		SettingsPageServices(QWidget* parent = nullptr);
+		void SaveInfo() override;
+
+	private:
+		QWidget* CreateMainPage();
+		QWidget* CreateAniListPage();
+		QLineEdit* username_entry;
+		QComboBox* sync_combo_box;
+};
+
+class SettingsPageApplication : public SettingsPage {
+	public:
+		SettingsPageApplication(QWidget* parent = nullptr);
+		void SaveInfo() override;
+
+	private:
+		QWidget* CreateAnimeListPage();
+};
+
+class SettingsDialog : public QDialog {
+	Q_OBJECT
+
+	public:
+		SettingsDialog(QWidget* parent = nullptr);
+		QWidget* CreateServicesMainPage(QWidget* parent);
+		void OnSidebar(QListWidgetItem* item);
+		void OnOK();
+
+	private:
+		std::vector<SettingsPage*> pages;
+		QHBoxLayout* layout;
+		SideBar* sidebar;
+};
+#endif // __settings_h
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/include/sidebar.h	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,10 @@
+#ifndef __sidebar_h
+#define __sidebar_h
+#include <QListWidget>
+class SideBar : public QListWidget {
+	public:
+		SideBar(QWidget *parent = nullptr);
+		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
+		QListWidgetItem* AddSeparator();
+};
+#endif // __sidebar_h
--- a/src/include/time_utils.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/time_utils.h	Wed Aug 16 00:49:17 2023 -0400
@@ -1,20 +1,20 @@
-#ifndef __duration_h
-#define __duration_h
-#include <string>
-#include <cstdint>
-namespace Time {
-	class Duration {
-		public:
-			Duration(int64_t l);
-			int64_t InSeconds();
-			int64_t InMinutes();
-			int64_t InHours();
-			int64_t InDays();
-			std::string AsRelativeString();
-
-		private:
-			int64_t length;
-	};
-	int64_t GetSystemTime();
-};
+#ifndef __duration_h
+#define __duration_h
+#include <string>
+#include <cstdint>
+namespace Time {
+	class Duration {
+		public:
+			Duration(int64_t l);
+			int64_t InSeconds();
+			int64_t InMinutes();
+			int64_t InHours();
+			int64_t InDays();
+			std::string AsRelativeString();
+
+		private:
+			int64_t length;
+	};
+	int64_t GetSystemTime();
+};
 #endif // __duration_h
\ No newline at end of file
--- a/src/include/ui_utils.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/ui_utils.h	Wed Aug 16 00:49:17 2023 -0400
@@ -5,7 +5,9 @@
 #include <QPoint>
 #include <QSize>
 #include <QDateTime>
+#include <QIcon>
 namespace UiUtils {
+	QIcon CreateSideBarIcon(const char* file);
 	bool IsInDarkMode();
 	std::string GetLengthFromQDateTime(QDateTime stamp);
 	QPlainTextEdit* CreateTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size);
--- a/src/include/window.h	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/include/window.h	Wed Aug 16 00:49:17 2023 -0400
@@ -21,6 +21,7 @@
 # include <QLabel>
 # include <QHBoxLayout>
 # include <QTextStream>
+# include <QSortFilterProxyModel>
 # include "config.h"
 //# include "statistics.h"
 //# include "now_playing.h"
@@ -34,6 +35,7 @@
 		void closeEvent(QCloseEvent* event) override;
 
 	private:
+		QWidget* main_widget;
 		QWidget* anime_list_page;
 };
 
--- a/src/json.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/json.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -2,31 +2,27 @@
 
 namespace JSON {
 
-std::string GetString(nlohmann::json const& json, std::string const& key) {
-	auto item = json.find(key);
-	if (item != json.end() && item->is_string())
-		return item->get<std::string>();
+std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+	if (json.contains(ptr) && json[ptr].is_string())
+		return json[ptr].get<std::string>();
 	else return "";
 }
 
-int GetInt(nlohmann::json const& json, std::string const& key) {
-	auto item = json.find(key);
-	if (item != json.end() && item->is_number())
-		return item->get<int>();
+int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+	if (json.contains(ptr) && json[ptr].is_number())
+		return json[ptr].get<int>();
 	else return 0;
 }
 
-bool GetBoolean(nlohmann::json const& json, std::string const& key) {
-	auto item = json.find(key);
-	if (item != json.end() && item->is_boolean())
-		return item->get<bool>();
+bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+	if (json.contains(ptr) && json[ptr].is_boolean())
+		return json[ptr].get<bool>();
 	else return false;
 }
 
-double GetDouble(nlohmann::json const& json, std::string const& key) {
-	auto item = json.find(key);
-	if (item != json.end() && item->is_number())
-		return item->get<double>();
+double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+	if (json.contains(ptr) && json[ptr].is_number())
+		return json[ptr].get<double>();
 	else return 0;
 }
 
--- a/src/main.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/main.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -1,6 +1,9 @@
 #include "window.h"
 #include "config.h"
 #include "anime.h"
+#include "sidebar.h"
+#include "ui_utils.h"
+#include "settings.h"
 #if MACOSX
 #include "sys/osx/dark_theme.h"
 #elif WIN32
@@ -17,6 +20,7 @@
 
 MainWindow::MainWindow(QWidget* parent) :
            QMainWindow(parent) {
+	main_widget = new QWidget(parent);
 	/* Menu Bar */
 	QAction* action;
 	QMenuBar* menubar = new QMenuBar(parent);
@@ -40,8 +44,8 @@
 	menu->addSeparator();
 
 	submenu = menu->addMenu("&AniList");
-	action = menu->addAction("Go to my &profile");
-	action = menu->addAction("Go to my &stats");
+	action = submenu->addAction("Go to my &profile");
+	action = submenu->addAction("Go to my &stats");
 
 	submenu = menu->addMenu("&Kitsu");
 	action = submenu->addAction("Go to my &feed");
@@ -69,11 +73,34 @@
 
 	menu->addSeparator();
 
-	action = menu->addAction("&Settings");
+	action = menu->addAction("&Settings", [this]{
+		SettingsDialog dialog(this);
+		dialog.exec();
+	});
 
 	setMenuBar(menubar);
 	
 	/* Side toolbar */
+	SideBar* sidebar = new SideBar(main_widget);
+	sidebar->AddItem("Now Playing", UiUtils::CreateSideBarIcon(":/icons/16x16/film.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem("Anime List", UiUtils::CreateSideBarIcon(":/icons/16x16/document-list.png"));
+	sidebar->AddItem("History", UiUtils::CreateSideBarIcon(":/icons/16x16/clock-history-frame.png"));
+	sidebar->AddItem("Statistics", UiUtils::CreateSideBarIcon(":/icons/16x16/chart.png"));
+	sidebar->AddSeparator();
+	sidebar->AddItem("Search", UiUtils::CreateSideBarIcon(":/icons/16x16/magnifier.png"));
+	sidebar->AddItem("Seasons", UiUtils::CreateSideBarIcon(":/icons/16x16/calendar.png"));
+	sidebar->AddItem("Torrents", UiUtils::CreateSideBarIcon(":/icons/16x16/feed.png"));
+	sidebar->setFixedWidth(128);
+	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
+
+	anime_list_page = new AnimeListPage(parent);
+
+	QHBoxLayout* layout = new QHBoxLayout(main_widget);
+	layout->addWidget(sidebar, 0, Qt::AlignLeft | Qt::AlignTop);
+	layout->addWidget(anime_list_page);
+	SetActivePage(main_widget);
+/*
 	QToolBar* toolbar = new QToolBar(parent);
 	QActionGroup* tb_action_group = new QActionGroup(toolbar);
 
@@ -112,9 +139,14 @@
 
 	toolbar->setMovable(false);
 	toolbar->setFloatable(false);
+	toolbar->setMinimumSize(QSize(140, 0));
+	toolbar->setObjectName("sidebar");
+	toolbar->setStyleSheet("QToolBar#sidebar{margin: 6px;}");
+	//toolbar->setFrameShape(QFrame::NoFrame);
+	toolbar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum);
 
 	addToolBar(Qt::LeftToolBarArea, toolbar);
-	
+*/
 	ThemeChanged();
 }
 
@@ -174,6 +206,9 @@
 				}
 			}
 #else
+			/* 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) */
 			SetStyleSheet(LIGHT);
 #endif
 			break;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/progress.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,39 @@
+#include "progress.h"
+
+#include <QPainter>
+#include <QPalette>
+#include <QStyleOptionProgressBar>
+#include <QStyleOptionViewItem>
+#include <QProxyStyle>
+#include <QMessageBox>
+
+AnimeProgressBar::AnimeProgressBar() {
+#if (defined(WIN32) || defined(MACOSX))
+    auto *fusionStyle = new QProxyStyle {"fusion"};
+    fusionStyle->setParent(&dummy_progress);
+    dummy_progress.setStyle(fusionStyle);
+#endif
+}
+
+void AnimeProgressBar::paint(QPainter *painter, const QStyleOptionViewItem &option, const QString &text, const int progress, const int episodes) const {
+    QStyleOptionProgressBar styleOption;
+    styleOption.initFrom(&dummy_progress);
+
+    styleOption.maximum = episodes;
+    styleOption.minimum = 0;
+    styleOption.progress = progress;
+    styleOption.text = text;
+    styleOption.textVisible = true;
+
+    styleOption.rect = option.rect;
+    styleOption.state = option.state;
+
+    const bool enabled = option.state.testFlag(QStyle::State_Enabled);
+    styleOption.palette.setCurrentColorGroup(enabled ? QPalette::Active : QPalette::Disabled);
+
+    painter->save();
+    const QStyle *style = dummy_progress.style();
+    style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget);
+    style->drawControl(QStyle::CE_ProgressBar, &styleOption, painter, &dummy_progress);
+    painter->restore();
+}
\ No newline at end of file
--- a/src/sidebar.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/sidebar.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -0,0 +1,35 @@
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QFrame>
+#include "sidebar.h"
+
+SideBar::SideBar(QWidget *parent)
+    : QListWidget(parent)
+{
+	setObjectName("sidebar");
+	setFrameShape(QFrame::NoFrame);
+    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	viewport()->setAutoFillBackground(false);
+	setStyleSheet("font-size: 12px");
+}
+
+QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
+    QListWidgetItem* item = new QListWidgetItem(this);
+    item->setText(name);
+	if (!icon.isNull())
+		item->setIcon(icon);
+	return item;
+}
+
+QListWidgetItem* SideBar::AddSeparator() {
+	QListWidgetItem* item = new QListWidgetItem(this);
+	item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
+	setStyleSheet("QListWidget::item:disabled {background: transparent;}");
+	QFrame* line = new QFrame(this);
+	line->setFrameShape(QFrame::HLine);
+	line->setFrameShadow(QFrame::Sunken);
+	line->setEnabled(false);
+	setItemWidget(item, line);
+	return item;
+}
--- a/src/time.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/time.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -1,64 +1,64 @@
-#include "time_utils.h"
-#include <string>
-#include <cstdint>
-#include <cmath>
-#include <ctime>
-#include <cassert>
-
-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((double)length / 60.0);
-}
-
-int64_t Duration::InHours() {
-	return std::llround((double)length / 3600.0);
-}
-
-int64_t Duration::InDays() {
-	return std::llround((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);
-}
-
+#include "time_utils.h"
+#include <string>
+#include <cstdint>
+#include <cmath>
+#include <ctime>
+#include <cassert>
+
+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((double)length / 60.0);
+}
+
+int64_t Duration::InHours() {
+	return std::llround((double)length / 3600.0);
+}
+
+int64_t Duration::InDays() {
+	return std::llround((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);
+}
+
 }
\ No newline at end of file
--- a/src/ui_utils.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ b/src/ui_utils.cpp	Wed Aug 16 00:49:17 2023 -0400
@@ -6,6 +6,14 @@
 #include "sys/win32/dark_theme.h"
 #endif
 
+QIcon UiUtils::CreateSideBarIcon(const char* file) {
+	QPixmap pixmap(file, "PNG");
+	QIcon result;
+	result.addPixmap(pixmap, QIcon::Normal);
+	result.addPixmap(pixmap, QIcon::Selected);
+	return result;
+}
+
 bool UiUtils::IsInDarkMode() {
 	if (session.config.theme != OS)
 		return (session.config.theme == DARK);
@@ -48,8 +56,12 @@
 
 	QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent);
 	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
 	paragraph->setWordWrapMode(QTextOption::NoWrap);
 	paragraph->setFrameShape(QFrame::NoFrame);
+	paragraph->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	paragraph->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	paragraph->setStyleSheet("background: transparent;");
 	paragraph->move(point.x()+12, point.y()+32);
 	paragraph->resize(size.width()-12, size.height()-32);
 	return paragraph;
@@ -60,15 +72,23 @@
 
 	QPlainTextEdit* label_t = new QPlainTextEdit(label, parent);
 	label_t->setTextInteractionFlags(Qt::NoTextInteraction);
+	label_t->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
 	label_t->setWordWrapMode(QTextOption::NoWrap);
 	label_t->setFrameShape(QFrame::NoFrame);
+	label_t->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	label_t->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	label_t->setStyleSheet("background: transparent;");
 	label_t->move(point.x()+12, point.y()+32);
 	label_t->resize(90, size.height()-32);
 
 	QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent);
 	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
 	paragraph->setWordWrapMode(QTextOption::NoWrap);
 	paragraph->setFrameShape(QFrame::NoFrame);
+	paragraph->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	paragraph->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	paragraph->setStyleSheet("background: transparent;");
 	paragraph->move(point.x()+102, point.y()+32);
 	paragraph->resize(size.width()-102, size.height()-32);
 	return paragraph;
@@ -82,6 +102,9 @@
 	QPlainTextEdit* text_edit = new QPlainTextEdit(data, parent);
 	text_edit->setReadOnly(true);
 	text_edit->setFrameShape(QFrame::NoFrame);
+	text_edit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	text_edit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	text_edit->setStyleSheet("background: transparent;");
 	text_edit->move(point.x()+12, point.y()+32);
 	text_edit->resize(size.width()-12, size.height()-32);
 	return text_edit;
--- a/src/window.cpp	Sat Aug 12 13:10:34 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-#include "window.h"
-#include <curl/curl.h>
-#include "page.h"
-#include "config.h"
-#include "anime.h"
-#include "statistics.h"
-#include "now_playing.h"
-#include "16x16/document-list.png.h"
-#include "16x16/film.png.h"
-#include "16x16/chart.png.h"
-#include "16x16/clock-history-frame.png.h"
-#include "16x16/magnifier.png.h"
-#include "16x16/calendar.png.h"
-#include "16x16/feed.png.h"
-#include "24x24/arrow-circle-double-135.png.h"
-#include "24x24/folder-open.png.h"
-#include "24x24/gear.png.h"
-
-Config Weeaboo::config = Config();
-
-WeeabooFrame::WeeabooFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
-					: wxFrame(NULL, wxID_ANY, title, pos, size) {
-	/* ---- Menu Bar ---- */
-	wxMenu* library_folders_submenu = new wxMenu;
-	library_folders_submenu->Append(ID_AddLibraryFolder, "&Add library folder");
-	library_folders_submenu->Append(ID_ScanLibraryFolders, "&Rescan library folders");
-
-	wxMenu* file_menu = new wxMenu;
-	file_menu->AppendSubMenu(library_folders_submenu, "&Library folders");
-	file_menu->Append(ID_SyncAnimeList, "&Sync anime list\tCtrl+S");
-	file_menu->AppendSeparator();
-	file_menu->Append(ID_PlayNextEpisode, "Play &next episode\tCtrl+N");
-	file_menu->Append(ID_PlayRandomEpisode, "Play &random episode\tCtrl+R");
-	file_menu->AppendSeparator();
-	file_menu->Append(wxID_EXIT);
-
-	wxMenu* help_menu = new wxMenu;
-	help_menu->Append(wxID_ABOUT);
-
-	wxMenuBar* menu_bar = new wxMenuBar;
-	menu_bar->Append(file_menu, "&File");
-	menu_bar->Append(help_menu, "&Help");
-
-	SetMenuBar(menu_bar);
-
-	/* Toolbar */
-	wxToolBar* top_toolbar = CreateToolBar();
-	top_toolbar->SetToolBitmapSize(wxSize(24,24));
-	top_toolbar->AddTool(ID_ToolbarSync, wxT("Sync"), wxBITMAP_PNG_FROM_DATA(arrow_circle_double_135));
-	top_toolbar->Realize();
-
-	/* ---- Sidebar ---- */
-	/* This first panel is only for the sizer... */
-	wxPanel* left_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(140, 600), wxTAB_TRAVERSAL, wxPanelNameStr);
-	wxPanel* left_panel_inside = new wxPanel(left_panel, wxID_ANY, wxPoint(6, 6), wxSize(128, 588), wxTAB_TRAVERSAL);
-	wxToolBar* left_toolbar = new wxToolBar(left_panel_inside, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_FLAT | wxTB_VERTICAL | wxTB_HORZ_TEXT | wxTB_NODIVIDER);
-	left_toolbar->SetMargins(6, 6);
-	left_toolbar->SetToolBitmapSize(wxSize(16,16));
-	left_toolbar->AddRadioTool(ID_NowPlaying, "Now Playing", wxBITMAP_PNG_FROM_DATA(film));
-	left_toolbar->AddRadioTool(ID_AnimeList,  "Anime List",  wxBITMAP_PNG_FROM_DATA(document_list));
-	left_toolbar->AddRadioTool(ID_History,    "History",     wxBITMAP_PNG_FROM_DATA(clock_history_frame));
-	left_toolbar->AddRadioTool(ID_Statistics, "Statistics",  wxBITMAP_PNG_FROM_DATA(chart));
-	left_toolbar->AddRadioTool(ID_Search,     "Search",      wxBITMAP_PNG_FROM_DATA(magnifier));
-	left_toolbar->AddRadioTool(ID_Seasons,    "Seasons",     wxBITMAP_PNG_FROM_DATA(calendar));
-	left_toolbar->AddRadioTool(ID_Torrents,   "Torrents",    wxBITMAP_PNG_FROM_DATA(feed));
-
-	/* ---- Initialize our pages ---- */
-	wxPanel* right_panel = new wxPanel(this, wxID_ANY, wxPoint(140, 0), wxSize(460, 600), wxTAB_TRAVERSAL, wxPanelNameStr);
-	now_playing = new NowPlaying(&pages[PAGE_NOW_PLAYING], right_panel);
-	anime_list = new AnimeListPage(&pages[PAGE_ANIME_LIST], right_panel);
-	anime_list->SyncAnimeList();
-	anime_list->LoadAnimeList(this);
-	statistics = new Statistics(&pages[PAGE_STATISTICS], right_panel);
-	
-	status.current_page = PAGE_ANIME_LIST; // The below function depends on this value being set?
-	set_page(PAGE_ANIME_LIST);
-	left_toolbar->ToggleTool(ID_AnimeList, true);
-	left_toolbar->Realize();
-
-	wxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
-	sizer->Add(left_panel, 0, wxEXPAND, 10);
-	sizer->Add(right_panel, 1, wxEXPAND, 10);
-	sizer->SetMinSize(600, 600);
-	this->SetSizer(sizer);
-	sizer->SetSizeHints(this);
-
-}
-
-bool Weeaboo::OnInit() {
-	config.Load();
-	if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) {
-		wxMessageBox("libcurl failed to initialize!",
-				 "Error", wxOK | wxICON_ERROR);
-	}
-	wxSystemOptions::SetOption("msw.remap",
-		wxSystemOptions::HasOption("msw.remap")
-						 ? wxSystemOptions::GetOptionInt("msw.remap")
-						 : wxDisplayDepth() <= 8 ? 1 : 2
-	);
-	wxImage::AddHandler(new wxPNGHandler);
-	frame = new WeeabooFrame("Weeaboo", wxPoint(50, 50), wxSize(450, 340));
-	frame->Show(true);
-	return true;
-}
-
-#define TOOLBAR_HANDLER(name, page) \
-void WeeabooFrame::name(wxCommandEvent& event) { \
-	set_page(page); \
-}
-TOOLBAR_HANDLER(OnNowPlaying, PAGE_NOW_PLAYING)
-TOOLBAR_HANDLER(OnAnimeList,  PAGE_ANIME_LIST)
-TOOLBAR_HANDLER(OnHistory,    PAGE_HISTORY)
-TOOLBAR_HANDLER(OnStatistics, PAGE_STATISTICS)
-TOOLBAR_HANDLER(OnSearch,     PAGE_SEARCH)
-TOOLBAR_HANDLER(OnSeasons,    PAGE_SEASONS)
-TOOLBAR_HANDLER(OnTorrents,   PAGE_TORRENTS)
-#undef TOOLBAR_HANDLER
-
-void WeeabooFrame::OnClose(wxCloseEvent& event) {
-	Weeaboo::config.Save();
-	curl_global_cleanup();
-	delete anime_list;
-	event.Skip();
-}
-
-void WeeabooFrame::OnExit(wxCommandEvent& event) {
-	Close(true);
-}
-
-void WeeabooFrame::OnAbout(wxCommandEvent& event) {
-	wxMessageBox("To be written",
-				 "About Weeaboo", wxOK | wxICON_INFORMATION);
-}
-
-void WeeabooFrame::OnAddFolder(wxCommandEvent& event) {
-	wxLogMessage("OnAddFolder");
-}
-
-void WeeabooFrame::OnScanFolders(wxCommandEvent& event) {
-	wxLogMessage("OnScanFolders");
-}
-
-void WeeabooFrame::OnNextEpisode(wxCommandEvent& event) {
-	wxLogMessage("OnNextEpisode");
-}
-
-void WeeabooFrame::OnSyncList(wxCommandEvent& event) {
-	anime_list->SyncAnimeList();
-	anime_list->LoadAnimeList(this);
-}
-
-void WeeabooFrame::OnRandomEpisode(wxCommandEvent& event) {
-	wxLogMessage("OnRandomEpisode");
-}
-
-AnimeListPage* WeeabooFrame::GetAnimeList() {
-	return anime_list;
-}