changeset 7:07a9095eaeed

Update Refactored some code, moved some around
author Paper <mrpapersonic@gmail.com>
date Thu, 24 Aug 2023 23:11:38 -0400
parents 1d82f6e04d7d
children b1f73678ef61
files CMakeLists.txt src/anilist.cpp src/anime.cpp src/config.cpp src/dialog/information.cpp src/dialog/settings.cpp src/dialog/settings/application.cpp src/dialog/settings/services.cpp src/filesystem.cpp src/include/anilist.h src/include/anime.h src/include/anime_list.h src/include/config.h src/include/information.h src/include/json.h src/include/now_playing.h src/include/session.h src/include/settings.h src/include/sidebar.h src/include/statistics.h src/include/ui_utils.h src/include/window.h src/json.cpp src/main.cpp src/pages/anime_list.cpp src/pages/now_playing.cpp src/pages/statistics.cpp src/sidebar.cpp src/string_utils.cpp src/ui_utils.cpp
diffstat 30 files changed, 1209 insertions(+), 843 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Aug 16 00:49:17 2023 -0400
+++ b/CMakeLists.txt	Thu Aug 24 23:11:38 2023 -0400
@@ -12,6 +12,9 @@
 	src/time.cpp
 	src/sidebar.cpp
 	src/progress.cpp
+	src/pages/anime_list.cpp
+	src/pages/now_playing.cpp
+	src/pages/statistics.cpp
 	src/dialog/settings.cpp
 	src/dialog/information.cpp
 	src/dialog/settings/services.cpp
@@ -20,8 +23,6 @@
 	src/string_utils.cpp
 	rc/icons.qrc
 	dep/darkstyle/darkstyle.qrc
-#	src/pages/statistics.cpp
-#	src/pages/now_playing.cpp
 )
 
 if(APPLE)
@@ -33,7 +34,7 @@
 	list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp)
 endif()
 
-add_executable(weeaboo WIN32 MACOSX_BUNDLE ${SRC_FILES})
+add_executable(weeaboo MACOSX_BUNDLE ${SRC_FILES})
 set_property(TARGET weeaboo PROPERTY CXX_STANDARD 20)
 set_property(TARGET weeaboo PROPERTY AUTOMOC ON)
 set_property(TARGET weeaboo PROPERTY AUTORCC ON)
--- a/src/anilist.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/anilist.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,15 +1,22 @@
-#include "window.h"
-#include "json.h"
+#include <QMessageBox>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QLineEdit>
 #include <curl/curl.h>
 #include <chrono>
 #include <exception>
 #include <format>
+#include "json.h"
 #include "anilist.h"
 #include "anime.h"
 #include "config.h"
 #include "string_utils.h"
+#include "session.h"
 #define CLIENT_ID "13706"
 
+CURL* AniList::curl = NULL;
+CURLcode AniList::res = (CURLcode)0;
+
 size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) {
     ((std::string*)userdata)->append((char*)contents, size * nmemb);
     return size * nmemb;
@@ -190,10 +197,6 @@
 			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.id = JSON::GetInt(entry.value(), "/media/id"_json_pointer);
 			anime.episodes = JSON::GetInt(entry.value(), "/media/episodes"_json_pointer);
@@ -210,8 +213,10 @@
 			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>>();
+			if (entry.value().contains("/media/genres"_json_pointer) && entry.value()["/media/genres"_json_pointer].is_array())
+				anime.genres = entry.value()["/media/genres"_json_pointer].get<std::vector<std::string>>();
+			if (entry.value().contains("/media/synonyms"_json_pointer) && entry.value()["/media/synonyms"_json_pointer].is_array())
+				anime.synonyms = entry.value()["/media/synonyms"_json_pointer].get<std::vector<std::string>>();
 			anime_list.Add(anime);
 		}
 		anime_lists->push_back(anime_list);
--- a/src/anime.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/anime.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,16 +1,16 @@
+/*
+ * anime.cpp: defining of custom anime-related
+ * datatypes & variables
+*/
 #include <chrono>
 #include <string>
 #include <vector>
 #include <cmath>
-#include "window.h"
+#include <algorithm>
 #include "anilist.h"
-#include "config.h"
 #include "anime.h"
 #include "date.h"
-#include "time_utils.h"
-#include "information.h"
-#include "ui_utils.h"
-#include <iostream>
+#include "session.h"
 
 std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
 	{CURRENT,   "Watching"},
@@ -76,9 +76,31 @@
 }
 
 std::string Anime::GetUserPreferredTitle() {
-	if (title.english.empty())
-		return title.romaji;
-	return title.english;
+	switch (session.config.anime_list.language) {
+		case NATIVE:
+			return (title.native.empty()) ? title.romaji : title.native;
+		case ENGLISH:
+			return (title.english.empty()) ? title.romaji : title.english;
+		default:
+			return title.romaji;
+	}
+}
+
+std::vector<std::string> Anime::GetTitleSynonyms() {
+	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, title.english);
+	ADD_TO_SYNONYMS(result, title.romaji);
+	ADD_TO_SYNONYMS(result, title.native);
+	for (auto& synonym : synonyms) {
+		ADD_TO_SYNONYMS(result, synonym);
+	}
+#undef ADD_TO_SYNONYMS
+#undef IN_VECTOR
+	return result;
 }
 
 void AnimeList::Add(Anime& anime) {
@@ -131,6 +153,16 @@
 	name = l.name;
 }
 
+AnimeList& AnimeList::operator=(const AnimeList& l) {
+	if (this != &l) {
+		for (unsigned long long i = 0; i < l.Size(); i++) {
+			this->anime_list.push_back(Anime(l[i]));
+		}
+		this->name = l.name;
+	}
+	return *this;
+}
+
 AnimeList::~AnimeList() {
 	anime_list.clear();
 	anime_list.shrink_to_fit();
@@ -160,466 +192,3 @@
 const Anime& AnimeList::operator[](std::size_t index) const {
 	return anime_list.at(index);
 }
-
-/* ------------------------------------------------------------------------- */
-
-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)
-                                          : QAbstractListModel(parent)
-										  , list(*alist) {
-	return;
-}
-
-int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
-	return list.Size();
-	(void)(parent);
-}
-
-int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
-	return NB_COLUMNS;
-	(void)(parent);
-}
-
-QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
-	if (role == Qt::DisplayRole) {
-		switch (section) {
-			case AL_TITLE:
-				return tr("Anime title");
-			case AL_PROGRESS:
-				return tr("Progress");
-			case AL_EPISODES:
-				return tr("Episodes");
-			case AL_TYPE:
-				return tr("Type");
-			case AL_SCORE:
-				return tr("Score");
-			case AL_SEASON:
-				return tr("Season");
-			case AL_STARTED:
-				return tr("Date started");
-			case AL_COMPLETED:
-				return tr("Date completed");
-			case AL_NOTES:
-				return tr("Notes");
-			case AL_AVG_SCORE:
-				return tr("Average score");
-			case AL_UPDATED:
-				return tr("Last updated");
-			default:
-				return {};
-		}
-	} else if (role == Qt::TextAlignmentRole) {
-		switch (section) {
-			case AL_TITLE:
-			case AL_NOTES:
-				return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
-			case AL_PROGRESS:
-			case AL_EPISODES:
-			case AL_TYPE:
-			case AL_SCORE:
-			case AL_AVG_SCORE:
-				return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
-			case AL_SEASON:
-			case AL_STARTED:
-			case AL_COMPLETED:
-			case AL_UPDATED:
-				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
-			default:
-				return QAbstractListModel::headerData(section, orientation, role);
-		}
-	}
-	return QAbstractListModel::headerData(section, orientation, role);
-}
-
-Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
-	return (index.isValid()) ? &(list[index.row()]) : nullptr;
-}
-
-QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
-	if (!index.isValid())
-		return QVariant();
-	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 "";
-			}
-			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();
-}
-
-void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
-	int i = list.GetAnimeIndex(anime);
-	emit dataChanged(index(i), index(i));
-}
-
-int AnimeListWidget::VisibleColumnsCount() const {
-    int count = 0;
-
-    for (int i = 0, end = header()->count(); i < end; i++)
-    {
-        if (!isColumnHidden(i))
-            count++;
-    }
-
-    return count;
-}
-
-void AnimeListWidget::SetColumnDefaults() {
-	setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
-	setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
-	setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
-	setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
-	setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
-	setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
-	setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
-	setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
-	setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
-	setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
-	setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
-	setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
-}
-
-void AnimeListWidget::DisplayColumnHeaderMenu() {
-    QMenu *menu = new QMenu(this);
-    menu->setAttribute(Qt::WA_DeleteOnClose);
-    menu->setTitle(tr("Column visibility"));
-    menu->setToolTipsVisible(true);
-
-    for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
-		if (i == AnimeListWidgetModel::AL_TITLE)
-			continue;
-        const auto column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
-        QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
-            if (!checked && (VisibleColumnsCount() <= 1))
-                return;
-
-            setColumnHidden(i, !checked);
-
-            if (checked && (columnWidth(i) <= 5))
-                resizeColumnToContents(i);
-
-            // SaveSettings();
-        });
-        action->setCheckable(true);
-        action->setChecked(!isColumnHidden(i));
-    }
-
-    menu->addSeparator();
-    QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
-        for (int i = 0, count = header()->count(); i < count; ++i)
-        {
-            SetColumnDefaults();
-        }
-		// SaveSettings();
-    });
-    menu->popup(QCursor::pos());
-	(void)(resetAction);
-}
-
-void AnimeListWidget::DisplayListMenu() {
-    QMenu *menu = new QMenu(this);
-    menu->setAttribute(Qt::WA_DeleteOnClose);
-    menu->setTitle(tr("Column visibility"));
-    menu->setToolTipsVisible(true);
-
-    const QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
-    if (!selection.indexes().first().isValid()) {
-        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 QItemSelection selection = sort_model->mapSelectionToSource(selectionModel()->selection());
-    if (!selection.indexes().first().isValid()) {
-        return;
-	}
-
-	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;
-	}
-
-	InformationDialog* dialog = new InformationDialog(*anime, model, this);
-
-    dialog->show();
-    dialog->raise();
-    dialog->activateWindow();
-}
-
-AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
-                               : QTreeView(parent) {
-	setItemDelegate(new AnimeListWidgetDelegate(this));
-	model = new AnimeListWidgetModel(parent, alist);
-	sort_model = new AnimeListWidgetSortFilter(this);
-	sort_model->setSourceModel(model);
-	sort_model->setSortRole(Qt::UserRole);
-	setModel(sort_model);
-	setObjectName("listwidget");
-	setStyleSheet("QTreeView#listwidget{border:0px;}");
-	setUniformRowHeights(true);
-	setAllColumnsShowFocus(false);
-	setSortingEnabled(true);
-	setSelectionMode(QAbstractItemView::ExtendedSelection);
-	setItemsExpandable(false);
-	setRootIsDecorated(false);
-	setContextMenuPolicy(Qt::CustomContextMenu);
-	connect(this, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
-	connect(this, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
-
-	/* Enter & return keys */
-    connect(new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut),
-	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
-
-    connect(new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut),
-	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
-
-	header()->setStretchLastSection(false);
-	header()->setContextMenuPolicy(Qt::CustomContextMenu);
-	connect(header(), &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayColumnHeaderMenu);
-	// if(!session.config.anime_list.columns) {
-		SetColumnDefaults();
-	// }
-}
-
-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::fromUtf8(list.name.c_str()));
-	}
-}
-
-void AnimeListPage::SyncAnimeList() {
-	switch (session.config.service) {
-		case ANILIST: {
-			AniList anilist = AniList();
-			session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username);
-			FreeAnimeList();
-			anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
-			break;
-		}
-		default:
-			break;
-	}
-}
-
-void AnimeListPage::FreeAnimeList() {
-	for (auto& list : anime_lists) {
-		list.Clear();
-	}
-	anime_lists.clear();
-}
-
-int AnimeListPage::GetTotalAnimeAmount() {
-	int total = 0;
-	for (auto& list : anime_lists) {
-		total += list.Size();
-	}
-	return total;
-}
-
-int AnimeListPage::GetTotalEpisodeAmount() {
-	/* FIXME: this also needs to take into account rewatches... */
-	int total = 0;
-	for (auto& list : anime_lists) {
-		for (auto& anime : list) {
-			total += anime.progress;
-		}
-	}
-	return total;
-}
-
-/* Returns the total watched amount in minutes. */
-int AnimeListPage::GetTotalWatchedAmount() {
-	int total = 0;
-	for (auto& list : anime_lists) {
-		for (auto& anime : list) {
-			total += anime.duration*anime.progress;
-		}
-	}
-	return total;
-}
-
-/* Returns the total planned amount in minutes.
-   Note that we should probably limit progress to the
-   amount of episodes, as AniList will let you
-   set episode counts up to 32768. But that should
-   rather be handled elsewhere. */
-int AnimeListPage::GetTotalPlannedAmount() {
-	int total = 0;
-	for (auto& list : anime_lists) {
-		for (auto& anime : list) {
-			total += anime.duration*(anime.episodes-anime.progress);
-		}
-	}
-	return total;
-}
-
-double AnimeListPage::GetAverageScore() {
-	double avg = 0;
-	int amt = 0;
-	for (auto& list : anime_lists) {
-		for (auto& anime : list) {
-			avg += anime.score;
-			if (anime.score != 0)
-				amt++;
-		}
-	}
-	return avg/amt;
-}
-
-double AnimeListPage::GetScoreDeviation() {
-	double squares_sum = 0, avg = GetAverageScore();
-	int amt = 0;
-	for (auto& list : anime_lists) {
-		for (auto& anime : list) {
-			if (anime.score != 0) {
-				squares_sum += std::pow((double)anime.score - avg, 2);
-				amt++;
-			}
-		}
-	}
-	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
-}
-
-#include "moc_anime.cpp"
--- a/src/config.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/config.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -41,6 +41,18 @@
 	{"AniList", ANILIST}
 };
 
+std::map<enum AnimeTitleLanguage, std::string> AnimeTitleToStringMap = {
+	{ROMAJI,  "Romaji"},
+	{NATIVE,  "Native"},
+	{ENGLISH, "English"}
+};
+
+std::map<std::string, enum AnimeTitleLanguage> StringToAnimeTitleMap = {
+	{"Romaji", ROMAJI},
+	{"Native", NATIVE},
+	{"English", ENGLISH}
+};
+
 int Config::Load() {
 	std::filesystem::path cfg_path = get_config_path();
 	if (!std::filesystem::exists(cfg_path))
@@ -48,6 +60,11 @@
 	std::ifstream config_in(cfg_path.string().c_str(), std::ifstream::in);
 	auto config_js = nlohmann::json::parse(config_in);
 	service = StringToService[JSON::GetString(config_js, "/General/Service"_json_pointer)];
+	anime_list.language = StringToAnimeTitleMap[JSON::GetString(config_js, "/Anime List/Display only aired episodes"_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);
@@ -65,6 +82,13 @@
 		{"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},
--- a/src/dialog/information.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/dialog/information.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,5 +1,10 @@
+#include <QPlainTextEdit>
+#include <QVBoxLayout>
+#include <QTextStream>
+#include <QDebug>
 #include "window.h"
 #include "anime.h"
+#include "anime_list.h"
 #include "information.h"
 #include "ui_utils.h"
 #include "string_utils.h"
@@ -25,7 +30,7 @@
 	widget->move(175, 0);
 	widget->setStyleSheet(UiUtils::IsInDarkMode() ? "" : "background-color: white");
 	widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-	QPlainTextEdit* anime_title = new QPlainTextEdit(QString::fromUtf8(anime->title.english.c_str()), widget);
+	QPlainTextEdit* anime_title = new QPlainTextEdit(QString::fromUtf8(anime->GetUserPreferredTitle().c_str()), widget);
 	anime_title->setReadOnly(true);
 	anime_title->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 	anime_title->setWordWrapMode(QTextOption::NoWrap);
@@ -41,7 +46,14 @@
 	tabbed_widget->move(0, 45);
 	tabbed_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
 	QWidget* main_information_widget = new QWidget(tabbed_widget);
-	UiUtils::CreateSelectableTextParagraph(main_information_widget, "Alternative titles", "-", QPoint(6, 6), QSize(636-18, 56));
+	main_information_widget->setLayout(new QVBoxLayout);
+
+	QString alternative_titles = QString::fromUtf8(StringUtils::Implode(anime->GetTitleSynonyms(), ", ").c_str());
+
+	QWidget* alternative_titles_w = UiUtils::CreateSelectableTextParagraph(main_information_widget, "Alternative titles", alternative_titles)->parentWidget()->parentWidget();
+	//alternative_titles_w->setFixedHeight(60);
+	main_information_widget->layout()->addWidget(alternative_titles_w);
+
 	QString details_data("");
 	QTextStream details_data_s(&details_data);
 	details_data_s << AnimeFormatToStringMap[anime->type].c_str() << "\n"
@@ -50,8 +62,17 @@
 	               << AnimeSeasonToStringMap[anime->season].c_str() << " " << anime->air_date.GetYear() << "\n"
 	               << 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::fromUtf8(anime->synopsis.c_str()), QPoint(6, 202), QSize(636-18, 253));
+	QWidget* soidjhfh = UiUtils::CreateTextParagraphWithLabels(main_information_widget, "Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data)->parentWidget()->parentWidget();
+	main_information_widget->layout()->addWidget(soidjhfh);
+
+	QPlainTextEdit* synopsis = UiUtils::CreateSelectableTextParagraph(main_information_widget, "Synopsis", QString::fromUtf8(anime->synopsis.c_str()));
+	synopsis->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	synopsis->parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	synopsis->parentWidget()->parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	((QVBoxLayout*)main_information_widget->layout())->addWidget(synopsis->parentWidget()->parentWidget());
+
+	//((QVBoxLayout*)main_information_widget->layout())->addStretch();
+
 	tabbed_widget->addTab(main_information_widget, "Main information");
 	QWidget* settings_widget = new QWidget(tabbed_widget);
 	tabbed_widget->addTab(settings_widget, "My list and settings");
--- a/src/dialog/settings.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/dialog/settings.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -6,7 +6,7 @@
 #include <QComboBox>
 #include <QGroupBox>
 #include <QWidget>
-#include <QTextDocument>
+#include <QStackedWidget>
 #include "settings.h"
 #include "sidebar.h"
 #include "ui_utils.h"
@@ -44,15 +44,10 @@
 	// 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();
+	QStackedWidget* stacked = (QStackedWidget*)layout->itemAt(1)->widget();
+	for (int i = 0; i < stacked->count(); i++) {
+		((SettingsPage*)stacked->widget(i))->SaveInfo();
 	}
 	QDialog::accept();
 }
@@ -77,17 +72,17 @@
 	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);
+	QStackedWidget* stacked = new QStackedWidget;
+	stacked->addWidget(new SettingsPageServices(stacked));
+	stacked->addWidget(new SettingsPageApplication(stacked));
+	stacked->setCurrentIndex(0);
+
+	connect(sidebar, &QListWidget::currentRowChanged, stacked, &QStackedWidget::setCurrentIndex);
 
 	layout = new QHBoxLayout;
 	layout->addWidget(sidebar);
-	layout->addWidget(services_page);
+	layout->addWidget(stacked);
 	layout->setMargin(0);
 	widget->setLayout(layout);
 	
--- a/src/dialog/settings/application.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/dialog/settings/application.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,13 +1,13 @@
 #include "settings.h"
 #include "anilist.h"
-#include "window.h"
+#include "session.h"
 #include <QGroupBox>
 #include <QComboBox>
 #include <QCheckBox>
 #include <QPushButton>
 #include <QSizePolicy>
 
-QWidget* SettingsPageApplication::CreateAnimeListPage() {
+QWidget* SettingsPageApplication::CreateAnimeListWidget() {
 	QWidget* result = new QWidget(this);
 	result->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
 
@@ -23,6 +23,7 @@
 	QVBoxLayout* double_click_layout = new QVBoxLayout;
 	double_click_layout->addWidget(dc_combo_box_label);
 	double_click_layout->addWidget(dc_combo_box);
+	double_click_layout->setMargin(0);
 	double_click_widget->setLayout(double_click_layout);
 
 	/* Actions/Middle click */
@@ -34,6 +35,7 @@
 	QVBoxLayout* middle_click_layout = new QVBoxLayout;
 	middle_click_layout->addWidget(mc_combo_box_label);
 	middle_click_layout->addWidget(mc_combo_box);
+	middle_click_layout->setMargin(0);
 	middle_click_widget->setLayout(middle_click_layout);
 
 	/* Actions */
@@ -47,15 +49,27 @@
 
 	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<enum AnimeTitleLanguage>(index);
+	});
+	lang_combo_box->setCurrentIndex(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);
-	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){
+	connect(hl_anime_box, &QCheckBox::stateChanged, this, [this, hl_above_anime_box](int state){
+		highlight_anime_if_available = (state == Qt::Unchecked) ? false : true;
 		hl_above_anime_box->setEnabled(state);
 	});
+	connect(hl_above_anime_box, &QCheckBox::stateChanged, this, [this](int state){
+		highlight_anime_if_available = (state == Qt::Unchecked) ? false : true;
+	});
+	hl_anime_box->setCheckState(highlight_anime_if_available ? Qt::Checked : Qt::Unchecked);
+	hl_above_anime_box->setCheckState(highlighted_anime_above_others ? Qt::Checked : Qt::Unchecked);
+	hl_above_anime_box->setEnabled(hl_anime_box->checkState() != Qt::Unchecked);
+	hl_above_anime_box->setStyleSheet("margin-left: 10px;");
 
 	/* Appearance */
 	QVBoxLayout* appearance_layout = new QVBoxLayout;
@@ -68,12 +82,21 @@
 	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);
+	QCheckBox* progress_display_aired_episodes = new QCheckBox(tr("Display aired episodes (estimated)"), progress_group_box);
+	connect(progress_display_aired_episodes, &QCheckBox::stateChanged, this, [this](int state){
+		display_aired_episodes = (state == Qt::Unchecked) ? false : true;
+	});
+	progress_display_aired_episodes->setCheckState(display_aired_episodes ? Qt::Checked : Qt::Unchecked);
+
+	QCheckBox* progress_display_available_episodes = new QCheckBox(tr("Display available episodes in library folders"), progress_group_box);
+	connect(progress_display_available_episodes, &QCheckBox::stateChanged, this, [this](int state){
+		display_available_episodes = (state == Qt::Unchecked) ? false : true;
+	});
+	progress_display_available_episodes->setCheckState(display_available_episodes ? Qt::Checked : Qt::Unchecked);
 
 	QVBoxLayout* progress_layout = new QVBoxLayout;
-	progress_layout->addWidget(display_aired_episodes);
-	progress_layout->addWidget(display_available_episodes);
+	progress_layout->addWidget(progress_display_aired_episodes);
+	progress_layout->addWidget(progress_display_available_episodes);
 	progress_group_box->setLayout(progress_layout);
 
 	QVBoxLayout* full_layout = new QVBoxLayout;
@@ -86,10 +109,19 @@
 }
 
 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")) {
-	AddTab(CreateAnimeListPage(), tr("Anime list"));
+	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/dialog/settings/services.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/dialog/settings/services.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,6 +1,6 @@
 #include "settings.h"
 #include "anilist.h"
-#include "window.h"
+#include "session.h"
 #include <QGroupBox>
 #include <QComboBox>
 #include <QPushButton>
@@ -15,8 +15,12 @@
 
 	QLabel* sync_combo_box_label = new QLabel(tr("Active service and metadata provider:"), sync_group_box);
 
-	sync_combo_box = new QComboBox(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<enum AnimeListServices>(index + 1);
+	});
+	sync_combo_box->setCurrentIndex(service - 1);
 
 	QLabel* sync_note_label = new QLabel(tr("Note: Weeaboo is unable to synchronize multiple services at the same time."), sync_group_box);
 
@@ -43,9 +47,13 @@
 	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);
+	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, [this]{
+	connect(auth_button, &QPushButton::clicked, this, []{
 		AniList a;
 		a.Authorize();
 	});
@@ -75,12 +83,14 @@
 }
 
 void SettingsPageServices::SaveInfo() {
-	session.config.anilist.username = username_entry->displayText().toStdString();
-	session.config.service = static_cast<enum AnimeListServices>(sync_combo_box->currentIndex()+1);
+	session.config.anilist.username = username.toStdString();
+	session.config.service = service;
 }
 
 SettingsPageServices::SettingsPageServices(QWidget* parent)
 	: SettingsPage(parent, tr("Services")) {
+	username = QString::fromUtf8(session.config.anilist.username.c_str());
+	service = session.config.service;
 	AddTab(CreateMainPage(), tr("Main"));
 	AddTab(CreateAniListPage(), tr("AniList"));
 }
--- a/src/filesystem.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/filesystem.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -2,6 +2,10 @@
 #include <shlobj.h>
 #elif defined(MACOSX)
 #include "sys/osx/filesystem.h"
+#elif defined(__linux__)
+#include <unistd.h>
+#include <sys/types.h>
+#include <pwd.h>
 #endif
 #include <filesystem>
 #include <limits.h>
@@ -17,10 +21,15 @@
 	if (SHGetFolderPathAndSubDir(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, CONFIG_DIR, buf) == S_OK)
 		cfg_path = std::filesystem::path(buf) / CONFIG_NAME;
 #elif defined(MACOSX)
-	/* hope and pray that std::filesystem can handle tildes... */
+	/* pass all of our problems to */
  	cfg_path = std::filesystem::path(StringUtils::Utf8ToWstr(osx::GetApplicationSupportDirectory())) / CONFIG_DIR / CONFIG_NAME;
 #else // just assume POSIX
-	cfg_path = std::filesystem::path(getenv("HOME")) / ".config" / CONFIG_DIR / CONFIG_NAME;
+	if (getenv("HOME") != NULL)
+		cfg_path = std::filesystem::path(getenv("HOME")) / ".config" / CONFIG_DIR / CONFIG_NAME;
+#ifdef __linux__
+	else
+		cfg_path = std::filesystem::path(getpwuid(getuid())->pw_dir) / ".config" / CONFIG_DIR / CONFIG_NAME;
+#endif // __linux__
 #endif
 	return cfg_path;
 }
--- a/src/include/anilist.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/anilist.h	Thu Aug 24 23:11:38 2023 -0400
@@ -6,17 +6,17 @@
 class AniList {
 	public:
 		static int Authorize();
-		int GetUserId(std::string name);
-		int UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id);
+		static int GetUserId(std::string name);
+		static int UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id);
 
 	private:
 		static size_t CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata);
-		enum AnimeWatchingStatus ConvertWatchingStatusToEnum(std::string status);
-		enum AnimeAiringStatus ConvertAiringStatusToEnum(std::string status);
-		enum AnimeFormat ConvertFormatToEnum(std::string format);
-		enum AnimeSeason ConvertSeasonToEnum(std::string season);
-		std::string SendRequest(std::string data);
-		CURL* curl;
-		CURLcode res;
+		static std::string SendRequest(std::string data);
+		static enum AnimeWatchingStatus ConvertWatchingStatusToEnum(std::string status);
+		static enum AnimeAiringStatus ConvertAiringStatusToEnum(std::string status);
+		static enum AnimeFormat ConvertFormatToEnum(std::string format);
+		static enum AnimeSeason ConvertSeasonToEnum(std::string season);
+		static CURL* curl;
+		static CURLcode res;
 };
 #endif // __anilist_h
--- a/src/include/anime.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/anime.h	Thu Aug 24 23:11:38 2023 -0400
@@ -2,8 +2,6 @@
 #define __anime_h
 #include <vector>
 #include <map>
-#include <QStyledItemDelegate>
-#include <QProgressBar>
 #include "date.h"
 #include "window.h"
 #include "progress.h"
@@ -66,6 +64,7 @@
 			std::string english;
 			std::string native;
 		} title;
+		std::vector<std::string> synonyms;
 		int episodes;
 		enum AnimeAiringStatus airing;
 		Date air_date;
@@ -78,6 +77,7 @@
 		int duration;
 		
 		std::string GetUserPreferredTitle();
+		std::vector<std::string> GetTitleSynonyms();
 };
 
 /* This is a simple wrapper on a vector that provides 
@@ -86,6 +86,7 @@
 	public:
 		AnimeList();
 		AnimeList(const AnimeList& l);
+		AnimeList& operator=(const AnimeList& l);
 		~AnimeList();
 		void Add(Anime& anime);
 		void Insert(size_t pos, Anime& anime);
@@ -108,96 +109,6 @@
 		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,
-			AL_SEASON,
-			AL_STARTED,
-			AL_COMPLETED,
-			AL_UPDATED,
-			AL_NOTES,
-			
-			NB_COLUMNS
-		};
-
-		AnimeListWidgetModel(QWidget* parent, AnimeList* alist);
-		~AnimeListWidgetModel() override = default;
-		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
-		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
-		QVariant data(const QModelIndex& index, int role) const override;
-		QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override;
-		Anime* GetAnimeFromIndex(const QModelIndex& index);
-		void UpdateAnime(Anime& anime);
-
-	private:
-		//void AddAnime(AnimeList& list);
-		AnimeList& list;
-};
-
-class AnimeListWidget : public QTreeView {
-	Q_OBJECT
-	public:
-		AnimeListWidget(QWidget* parent, AnimeList* alist);
-
-	private slots:
-		void DisplayColumnHeaderMenu();
-		void DisplayListMenu();
-		void ItemDoubleClicked();
-		void SetColumnDefaults();
-		int VisibleColumnsCount() const;
-
-	private:
-		AnimeListWidgetModel* model = nullptr;
-		AnimeListWidgetSortFilter* sort_model = nullptr;
-};
-
-class AnimeListPage : public QTabWidget {
-	public:
-		AnimeListPage(QWidget* parent = nullptr);
-		void SyncAnimeList();
-		void FreeAnimeList();
-		int GetTotalAnimeAmount();
-		int GetTotalEpisodeAmount();
-		int GetTotalWatchedAmount();
-		int GetTotalPlannedAmount();
-		double GetAverageScore();
-		double GetScoreDeviation();
-
-	private:
-		std::vector<AnimeList> anime_lists;
-};
-
 extern std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap;
 extern std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap;
 extern std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/include/anime_list.h	Thu Aug 24 23:11:38 2023 -0400
@@ -0,0 +1,109 @@
+#ifndef __anime_list_h
+#define __anime_list_h
+#include <vector>
+#include <QStyledItemDelegate>
+#include <QSortFilterProxyModel>
+#include <QAbstractListModel>
+#include <QTreeView>
+#include <QWidget>
+#include "anime.h"
+#include "progress.h"
+
+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,
+			AL_SEASON,
+			AL_STARTED,
+			AL_COMPLETED,
+			AL_UPDATED,
+			AL_NOTES,
+			
+			NB_COLUMNS
+		};
+
+		AnimeListWidgetModel(QWidget* parent, AnimeList* alist);
+		~AnimeListWidgetModel() override = default;
+		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+		QVariant data(const QModelIndex& index, int role) const override;
+		QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override;
+		Anime* GetAnimeFromIndex(const QModelIndex& index);
+		void UpdateAnime(Anime& anime);
+		void Update(const AnimeList& new_list);
+
+	private:
+		//void AddAnime(AnimeList& list);
+		AnimeList& list;
+};
+
+/* todo: rename these to "page" or something more
+   sensible than "widget" */
+class AnimeListWidget : public QWidget {
+	Q_OBJECT
+
+	public:
+		AnimeListWidget(QWidget* parent);
+		void SyncAnimeList();
+		void FreeAnimeList();
+		int GetTotalAnimeAmount();
+		int GetTotalEpisodeAmount();
+		int GetTotalWatchedAmount();
+		int GetTotalPlannedAmount();
+		double GetAverageScore();
+		double GetScoreDeviation();
+
+	protected:
+		void paintEvent(QPaintEvent*) override;
+		void InitStyle(QStyleOptionTabWidgetFrame *option) const;
+		void InitBasicStyle(QStyleOptionTabWidgetFrame *option) const;
+		void SetupLayout();
+		void showEvent(QShowEvent*) override;
+		void resizeEvent(QResizeEvent* e) override;
+
+	private slots:
+		void DisplayColumnHeaderMenu();
+		void DisplayListMenu();
+		void ItemDoubleClicked();
+		void SetColumnDefaults();
+		int VisibleColumnsCount() const;
+
+	private:
+		QTabBar* tab_bar;
+		QTreeView* tree_view;
+		QRect panelRect;
+		std::vector<AnimeListWidgetSortFilter*> sort_models;
+		std::vector<AnimeList> anime_lists;
+};
+#endif // __anime_list_h
\ No newline at end of file
--- a/src/include/config.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/config.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,15 +1,17 @@
 #ifndef __config_h
 #define __config_h
-/* This should be moved to anime_list.h, but unfortunately
-   #include-ing anime_list.h in this file causes a shitstorm for
-   whatever reason, so I'll just leave it here */
+enum AnimeTitleLanguage {
+	ROMAJI,
+	NATIVE,
+	ENGLISH
+};
+
 enum AnimeListServices {
 	NONE,
 	ANILIST,
 	NB_SERVICES
 };
 
-/* todo: make this a class enum */
 enum Themes {
 	LIGHT,
 	DARK,
@@ -23,6 +25,15 @@
 
 		enum AnimeListServices service;
 		enum Themes theme;
+
+		struct {
+			enum AnimeTitleLanguage language;
+			bool display_aired_episodes;
+			bool display_available_episodes;
+			bool highlight_anime_if_available;
+			bool highlighted_anime_above_others;
+		} anime_list;
+
 		struct {
 			std::string auth_token;
 			std::string username;
--- a/src/include/information.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/information.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,5 +1,6 @@
 #ifndef __information_h
 #define __information_h
+#include <QDialog>
 #include "anime.h"
 class InformationDialog: public QDialog {
 	Q_OBJECT
--- a/src/include/json.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/json.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,8 +1,10 @@
 #include "../../dep/json/json.h"
-
+#ifndef __json_h
+#define __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);
+	std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, std::string def = "");
+	int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, int def = 0);
+	bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, bool def = false);
+	double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, double def = 0);
 }
+#endif // __json_h
\ No newline at end of file
--- a/src/include/now_playing.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/now_playing.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,9 +1,12 @@
 #ifndef __now_playing_h
 #define __now_playing_h
+#include <QWidget>
 
-class NowPlaying {
+class NowPlayingWidget : public QWidget {
+	Q_OBJECT
+
 	public:
-		NowPlaying(page_t* page, wxPanel* frame);
+		NowPlayingWidget(QWidget* parent = nullptr);
 };
 
 #endif // __now_playing_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/include/session.h	Thu Aug 24 23:11:38 2023 -0400
@@ -0,0 +1,16 @@
+#ifndef __session_h
+#define __session_h
+#include <QElapsedTimer>
+#include "config.h"
+
+struct Session {
+	Config config;
+	Session() { timer.start(); }
+	int uptime() { return timer.nsecsElapsed() / 1000; }
+
+	private: QElapsedTimer timer;
+};
+
+extern Session session;
+
+#endif // __session_h
\ No newline at end of file
--- a/src/include/settings.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/settings.h	Thu Aug 24 23:11:38 2023 -0400
@@ -8,6 +8,7 @@
 #include <QComboBox>
 #include <QHBoxLayout>
 #include "sidebar.h"
+#include "anime.h"
 class SettingsPage : public QWidget {
 	Q_OBJECT
 
@@ -30,8 +31,8 @@
 	private:
 		QWidget* CreateMainPage();
 		QWidget* CreateAniListPage();
-		QLineEdit* username_entry;
-		QComboBox* sync_combo_box;
+		QString username;
+		enum AnimeListServices service;
 };
 
 class SettingsPageApplication : public SettingsPage {
@@ -40,7 +41,12 @@
 		void SaveInfo() override;
 
 	private:
-		QWidget* CreateAnimeListPage();
+		QWidget* CreateAnimeListWidget();
+		enum AnimeTitleLanguage language;
+		bool display_aired_episodes;
+		bool display_available_episodes;
+		bool highlight_anime_if_available;
+		bool highlighted_anime_above_others;
 };
 
 class SettingsDialog : public QDialog {
@@ -49,11 +55,9 @@
 	public:
 		SettingsDialog(QWidget* parent = nullptr);
 		QWidget* CreateServicesMainPage(QWidget* parent);
-		void OnSidebar(QListWidgetItem* item);
 		void OnOK();
 
 	private:
-		std::vector<SettingsPage*> pages;
 		QHBoxLayout* layout;
 		SideBar* sidebar;
 };
--- a/src/include/sidebar.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/sidebar.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,10 +1,23 @@
 #ifndef __sidebar_h
 #define __sidebar_h
 #include <QListWidget>
+#include <QListWidgetItem>
+#include <QItemSelectionModel>
 class SideBar : public QListWidget {
+	Q_OBJECT
+
 	public:
 		SideBar(QWidget *parent = nullptr);
 		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
 		QListWidgetItem* AddSeparator();
+		bool IndexIsSeparator(QModelIndex index) const;
+
+	signals:
+		void CurrentItemChanged(int index);
+
+	protected:
+		virtual void mouseMoveEvent(QMouseEvent* event) override;
+		QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex & index, const QEvent * event) const override;
+		int RemoveSeparatorsFromIndex(int index);
 };
 #endif // __sidebar_h
--- a/src/include/statistics.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/statistics.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,30 +1,23 @@
 #ifndef __statistics_h
 #define __statistics_h
-class Statistics;
-
-class StatisticsTimer : public wxTimer {
-	public:
-		StatisticsTimer(Statistics* caller);
-		virtual void Notify();
+#include <QWidget>
+#include <QFrame>
+#include <QPlainTextEdit>
+#include "anime_list.h"
 
-	private:
-		Statistics* statistics;
-};
-
-class Statistics {
+class StatisticsWidget : public QFrame {
 	public:
-		Statistics(page_t* page, wxPanel* frame);
+		StatisticsWidget(AnimeListWidget* listwidget, QWidget* parent = nullptr);
 		void UpdateStatistics();
 
 	private:
 		std::string MinutesToDateString(int minutes);
 
-		wxPanel* panel;
-		AnimeListPage* anime_list;
-		wxStaticText* anime_list_data;
+		AnimeListWidget* anime_list;
+		QPlainTextEdit* anime_list_data;
 
-		wxStaticText* score_distribution_title;
-		wxStaticText* score_distribution_labels;
+		//QPlainTextEdit* score_distribution_title;
+		//QPlainTextEdit* score_distribution_labels;
 		//wxStaticText* score_distribution_graph; // how am I gonna do this
 
 		/* we don't HAVE a local database (yet ;)) */
@@ -32,9 +25,6 @@
 		//wxStaticText* local_database_labels;
 		//wxStaticText* local_database_data;
 
-		wxStaticText* application_title;
-		wxStaticText* application_labels;
-		wxStaticText* application_data;
-		StatisticsTimer* timer;
+		QPlainTextEdit* application_data;
 };
 #endif // __statistics_h
\ No newline at end of file
--- a/src/include/ui_utils.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/ui_utils.h	Thu Aug 24 23:11:38 2023 -0400
@@ -6,13 +6,22 @@
 #include <QSize>
 #include <QDateTime>
 #include <QIcon>
+#include <QPlainTextEdit>
 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);
-	QPlainTextEdit* CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data, QPoint point, QSize size);
-	QPlainTextEdit* CreateSelectableTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size);
-	void CreateTextHeader(QWidget* parent, QString title, QPoint point, QSize size);
+	QPlainTextEdit* CreateTextParagraph(QWidget* parent, QString title, QString data);
+	QPlainTextEdit* CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data);
+	QPlainTextEdit* CreateSelectableTextParagraph(QWidget* parent, QString title, QString data);
+	void SetPlainTextEditData(QPlainTextEdit* text_edit, QString data);
+	void CreateTextHeader(QWidget* parent, QString title);
+};
+
+class Paragraph : public QPlainTextEdit {
+	public:
+		Paragraph(QString text, QWidget* parent = nullptr);
+		QSize minimumSizeHint() const override;
+		QSize sizeHint() const override;
 };
 #endif // __ui_utils_h
\ No newline at end of file
--- a/src/include/window.h	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/include/window.h	Thu Aug 24 23:11:38 2023 -0400
@@ -1,30 +1,9 @@
 #ifndef __window_h
-# define __window_h
-/* FIXME: include these in specific .cpp files */
-# include <QApplication>
-# include <QMainWindow>
-# include <QToolBar>
-# include <QMenuBar>
-# include <QWidget>
-# include <QTreeView>
-# include <QMessageBox>
-# include <QDesktopServices>
-# include <QUrl>
-# include <QInputDialog>
-# include <QDate>
-# include <QHeaderView>
-# include <QShortcut>
-# include <QFile>
-# include <QTextStream>
-# include <QCloseEvent>
-# include <QPlainTextEdit>
-# include <QLabel>
-# include <QHBoxLayout>
-# include <QTextStream>
-# include <QSortFilterProxyModel>
-# include "config.h"
-//# include "statistics.h"
-//# include "now_playing.h"
+#define __window_h
+#include <QMainWindow>
+#include <QWidget>
+#include <QCloseEvent>
+#include "config.h"
 
 class MainWindow : public QMainWindow {
 	public:
@@ -36,12 +15,6 @@
 
 	private:
 		QWidget* main_widget;
-		QWidget* anime_list_page;
 };
 
-struct Session {
-	Config config;
-};
-
-extern Session session;
 #endif // __window_h
--- a/src/json.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/json.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -2,28 +2,28 @@
 
 namespace JSON {
 
-std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+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 "";
+	else return def;
 }
 
-int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+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 0;
+	else return def;
 }
 
-bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+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 false;
+	else return def;
 }
 
-double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr) {
+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 0;
+	else return def;
 }
 
 }
--- a/src/main.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/main.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,18 +1,27 @@
+#include <QApplication>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QPlainTextEdit>
+#include <QStackedWidget>
+#include <QFile>
+#include <QTextStream>
+#include <QMessageBox>
 #include "window.h"
 #include "config.h"
-#include "anime.h"
+#include "anime_list.h"
+#include "now_playing.h"
+#include "statistics.h"
 #include "sidebar.h"
 #include "ui_utils.h"
 #include "settings.h"
+#include "session.h"
 #if MACOSX
 #include "sys/osx/dark_theme.h"
 #elif WIN32
 #include "sys/win32/dark_theme.h"
 #endif
 
-Session session = {
-	.config = Config()
-};
+Session session;
 
 /* note that this code was originally created for use in 
    wxWidgets, but I thought the API was a little meh, so
@@ -79,8 +88,7 @@
 	});
 
 	setMenuBar(menubar);
-	
-	/* Side toolbar */
+
 	SideBar* sidebar = new SideBar(main_widget);
 	sidebar->AddItem("Now Playing", UiUtils::CreateSideBarIcon(":/icons/16x16/film.png"));
 	sidebar->AddSeparator();
@@ -94,59 +102,31 @@
 	sidebar->setFixedWidth(128);
 	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
 
-	anime_list_page = new AnimeListPage(parent);
+	QStackedWidget* stack = new QStackedWidget(main_widget);
+	stack->addWidget(new NowPlayingWidget(parent));
+	AnimeListWidget* list_widget = new AnimeListWidget(parent);
+	list_widget->SyncAnimeList();
+	stack->addWidget(list_widget);
+	stack->addWidget(new StatisticsWidget(list_widget, parent));
+
+	connect(sidebar, &SideBar::CurrentItemChanged, stack, [stack](int index){
+		switch (index) {
+			case 0:
+			case 1:
+			case 2:
+				stack->setCurrentIndex(index);
+				break;
+			default:
+				break;
+		}
+	});
+	sidebar->setCurrentRow(2);
 
 	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);
-
-	action = toolbar->addAction("Now Playing");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-
-	toolbar->addSeparator();
+	layout->addWidget(stack);
+	setCentralWidget(main_widget);
 
-	action = toolbar->addAction("Anime List", [this]() {
-		setCentralWidget(anime_list_page);
-	});
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-	action->setChecked(true);
-	anime_list_page = new AnimeListPage(parent);
-	SetActivePage(anime_list_page);
-	action = toolbar->addAction("History");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-	action = toolbar->addAction("Statistics");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-
-	toolbar->addSeparator();
-
-	action = toolbar->addAction("Search");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-	action = toolbar->addAction("Seasons");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-	action = toolbar->addAction("Torrents");
-	action->setActionGroup(tb_action_group);
-	action->setCheckable(true);
-
-	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();
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pages/anime_list.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -0,0 +1,565 @@
+/**
+ * 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 <cmath>
+#include <QStyledItemDelegate>
+#include <QProgressBar>
+#include <QShortcut>
+#include <QHBoxLayout>
+#include <QStylePainter>
+#include <QMenu>
+#include <QHeaderView>
+#include "anilist.h"
+#include "anime.h"
+#include "anime_list.h"
+#include "information.h"
+#include "session.h"
+#include "time_utils.h"
+
+AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent)
+	: QStyledItemDelegate (parent) {
+}
+
+QWidget* AnimeListWidgetDelegate::createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const {
+    // no edit 4 u
+    return nullptr;
+}
+
+void AnimeListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+    switch (index.column()) {
+		case AnimeListWidgetModel::AL_PROGRESS: {
+			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
+			const int episodes = static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());
+
+			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 QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
+	}
+}
+
+AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
+                                          : QAbstractListModel(parent)
+										  , list(*alist) {
+	return;
+}
+
+int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
+	return list.Size();
+	(void)(parent);
+}
+
+int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const {
+	return NB_COLUMNS;
+	(void)(parent);
+}
+
+QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
+	if (role == Qt::DisplayRole) {
+		switch (section) {
+			case AL_TITLE:
+				return tr("Anime title");
+			case AL_PROGRESS:
+				return tr("Progress");
+			case AL_EPISODES:
+				return tr("Episodes");
+			case AL_TYPE:
+				return tr("Type");
+			case AL_SCORE:
+				return tr("Score");
+			case AL_SEASON:
+				return tr("Season");
+			case AL_STARTED:
+				return tr("Date started");
+			case AL_COMPLETED:
+				return tr("Date completed");
+			case AL_NOTES:
+				return tr("Notes");
+			case AL_AVG_SCORE:
+				return tr("Average score");
+			case AL_UPDATED:
+				return tr("Last updated");
+			default:
+				return {};
+		}
+	} else if (role == Qt::TextAlignmentRole) {
+		switch (section) {
+			case AL_TITLE:
+			case AL_NOTES:
+				return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+			case AL_PROGRESS:
+			case AL_EPISODES:
+			case AL_TYPE:
+			case AL_SCORE:
+			case AL_AVG_SCORE:
+				return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+			case AL_SEASON:
+			case AL_STARTED:
+			case AL_COMPLETED:
+			case AL_UPDATED:
+				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+			default:
+				return QAbstractListModel::headerData(section, orientation, role);
+		}
+	}
+	return QAbstractListModel::headerData(section, orientation, role);
+}
+
+Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
+	return (index.isValid()) ? &(list[index.row()]) : nullptr;
+}
+
+QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
+	if (!index.isValid())
+		return QVariant();
+	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 "";
+			}
+			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();
+}
+
+void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
+	int i = list.GetAnimeIndex(anime);
+	emit dataChanged(index(i), index(i));
+}
+
+void AnimeListWidgetModel::Update(AnimeList const& new_list) {
+	list = AnimeList(new_list);
+	emit dataChanged(index(0), index(rowCount()));
+}
+
+int AnimeListWidget::VisibleColumnsCount() const {
+    int count = 0;
+
+    for (int i = 0, end = tree_view->header()->count(); i < end; i++)
+    {
+        if (!tree_view->isColumnHidden(i))
+            count++;
+    }
+
+    return count;
+}
+
+void AnimeListWidget::SetColumnDefaults() {
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
+	tree_view->setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
+}
+
+void AnimeListWidget::DisplayColumnHeaderMenu() {
+    QMenu *menu = new QMenu(this);
+    menu->setAttribute(Qt::WA_DeleteOnClose);
+    menu->setTitle(tr("Column visibility"));
+    menu->setToolTipsVisible(true);
+
+    for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
+		if (i == AnimeListWidgetModel::AL_TITLE)
+			continue;
+        const auto column_name = sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
+        QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
+            if (!checked && (VisibleColumnsCount() <= 1))
+                return;
+
+            tree_view->setColumnHidden(i, !checked);
+
+            if (checked && (tree_view->columnWidth(i) <= 5))
+                tree_view->resizeColumnToContents(i);
+
+            // SaveSettings();
+        });
+        action->setCheckable(true);
+        action->setChecked(!tree_view->isColumnHidden(i));
+    }
+
+    menu->addSeparator();
+    QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
+        for (int i = 0, count = tree_view->header()->count(); i < count; ++i)
+        {
+            SetColumnDefaults();
+        }
+		// SaveSettings();
+    });
+    menu->popup(QCursor::pos());
+	(void)(resetAction);
+}
+
+void AnimeListWidget::DisplayListMenu() {
+    QMenu *menu = new QMenu(this);
+    menu->setAttribute(Qt::WA_DeleteOnClose);
+    menu->setTitle(tr("Column visibility"));
+    menu->setToolTipsVisible(true);
+
+    const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+    if (!selection.indexes().first().isValid()) {
+        return;
+	}
+
+	QAction* action = menu->addAction("Information", [this, selection]{
+		const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
+		Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
+		if (!anime) {
+			return;
+		}
+
+		InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);
+
+		dialog->show();
+		dialog->raise();
+		dialog->activateWindow();
+	});
+	menu->popup(QCursor::pos());
+}
+
+void AnimeListWidget::ItemDoubleClicked() {
+	/* throw out any other garbage */
+    const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+    if (!selection.indexes().first().isValid()) {
+        return;
+	}
+
+	const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
+	Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
+	if (!anime) {
+		return;
+	}
+
+	InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);
+
+    dialog->show();
+    dialog->raise();
+    dialog->activateWindow();
+}
+
+void AnimeListWidget::paintEvent(QPaintEvent*) {
+    QStylePainter p(this);
+
+    QStyleOptionTabWidgetFrame opt;
+	InitStyle(&opt);
+	opt.rect = panelRect;
+    p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
+}
+
+void AnimeListWidget::resizeEvent(QResizeEvent* e) {
+	QWidget::resizeEvent(e);
+	SetupLayout();
+}
+
+void AnimeListWidget::showEvent(QShowEvent*) {
+	SetupLayout();
+}
+
+void AnimeListWidget::InitBasicStyle(QStyleOptionTabWidgetFrame *option) const
+{
+	if (!option)
+		return;
+
+    option->initFrom(this);
+    option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
+	option->shape = QTabBar::RoundedNorth;
+    option->tabBarRect = tab_bar->geometry();
+}
+
+void AnimeListWidget::InitStyle(QStyleOptionTabWidgetFrame *option) const
+{
+	if (!option)
+		return;
+
+	InitBasicStyle(option);
+
+    //int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
+    QSize t(0, tree_view->frameWidth());
+    if (tab_bar->isVisibleTo(this)) {
+        t = tab_bar->sizeHint();
+		t.setWidth(width());
+    }
+	option->tabBarSize = t;
+
+	QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
+	selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
+	option->selectedTabRect = selected_tab_rect;
+
+	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
+}
+
+void AnimeListWidget::SetupLayout() {
+	QStyleOptionTabWidgetFrame option;
+	InitStyle(&option);
+
+	QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
+	tabRect.setLeft(tabRect.left()+1);
+	panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
+	QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);
+
+	tab_bar->setGeometry(tabRect);
+	tree_view->parentWidget()->setGeometry(contentsRect);
+}
+
+AnimeListWidget::AnimeListWidget(QWidget* parent)
+                               : QWidget(parent) {
+	/* Tab bar */
+	tab_bar = new QTabBar(this);
+	tab_bar->setExpanding(false);
+	tab_bar->setDrawBase(false);
+
+	/* Tree view... */
+	QWidget* tree_widget = new QWidget(this);
+	tree_view = new QTreeView(tree_widget);
+	tree_view->setItemDelegate(new AnimeListWidgetDelegate(tree_view));
+	tree_view->setUniformRowHeights(true);
+	tree_view->setAllColumnsShowFocus(false);
+	tree_view->setSortingEnabled(true);
+	tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
+	tree_view->setItemsExpandable(false);
+	tree_view->setRootIsDecorated(false);
+	tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
+	tree_view->setFrameShape(QFrame::NoFrame);
+	QHBoxLayout* layout = new QHBoxLayout;
+	layout->addWidget(tree_view);
+	layout->setMargin(0);
+	tree_widget->setLayout(layout);
+	connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
+	connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);
+
+	/* Enter & return keys */
+    connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
+	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
+
+    connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
+	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);
+
+	tree_view->header()->setStretchLastSection(false);
+	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
+	connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayColumnHeaderMenu);
+
+	connect(tab_bar, &QTabBar::currentChanged, this, [this](int index){
+		if (index < sort_models.size())
+			tree_view->setModel(sort_models[index]);
+	});
+
+	setFocusPolicy(Qt::TabFocus);
+	setFocusProxy(tab_bar);
+}
+
+void AnimeListWidget::SyncAnimeList() {
+	switch (session.config.service) {
+		case ANILIST: {
+			session.config.anilist.user_id = AniList::GetUserId(session.config.anilist.username);
+			FreeAnimeList();
+			AniList::UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
+			break;
+		}
+		default:
+			break;
+	}
+	for (unsigned int i = 0; i < anime_lists.size(); i++) {
+		tab_bar->addTab(QString::fromStdString(anime_lists[i].name));
+		AnimeListWidgetSortFilter* sort_model = new AnimeListWidgetSortFilter(tree_view);
+		sort_model->setSourceModel(new AnimeListWidgetModel(this, &anime_lists[i]));
+		sort_model->setSortRole(Qt::UserRole);
+		sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
+		sort_models.push_back(sort_model);
+	}
+	if (anime_lists.size() > 0)
+		tree_view->setModel(sort_models.at(0));
+	SetColumnDefaults();
+	SetupLayout();
+}
+
+void AnimeListWidget::FreeAnimeList() {
+	while (tab_bar->count())
+		tab_bar->removeTab(0);
+	while (sort_models.size()) {
+		delete sort_models[sort_models.size()-1];
+		sort_models.pop_back();
+	}
+	for (auto& list : anime_lists) {
+		list.Clear();
+	}
+	anime_lists.clear();
+}
+
+int AnimeListWidget::GetTotalAnimeAmount() {
+	int total = 0;
+	for (auto& list : anime_lists) {
+		total += list.Size();
+	}
+	return total;
+}
+
+int AnimeListWidget::GetTotalEpisodeAmount() {
+	/* FIXME: this also needs to take into account rewatches... */
+	int total = 0;
+	for (auto& list : anime_lists) {
+		for (auto& anime : list) {
+			total += anime.progress;
+		}
+	}
+	return total;
+}
+
+/* Returns the total watched amount in minutes. */
+int AnimeListWidget::GetTotalWatchedAmount() {
+	int total = 0;
+	for (auto& list : anime_lists) {
+		for (auto& anime : list) {
+			total += anime.duration*anime.progress;
+		}
+	}
+	return total;
+}
+
+/* Returns the total planned amount in minutes.
+   Note that we should probably limit progress to the
+   amount of episodes, as AniList will let you
+   set episode counts up to 32768. But that should
+   rather be handled elsewhere. */
+int AnimeListWidget::GetTotalPlannedAmount() {
+	int total = 0;
+	for (auto& list : anime_lists) {
+		for (auto& anime : list) {
+			total += anime.duration*(anime.episodes-anime.progress);
+		}
+	}
+	return total;
+}
+
+double AnimeListWidget::GetAverageScore() {
+	double avg = 0;
+	int amt = 0;
+	for (auto& list : anime_lists) {
+		for (auto& anime : list) {
+			avg += anime.score;
+			if (anime.score != 0)
+				amt++;
+		}
+	}
+	return avg/amt;
+}
+
+double AnimeListWidget::GetScoreDeviation() {
+	double squares_sum = 0, avg = GetAverageScore();
+	int amt = 0;
+	for (auto& list : anime_lists) {
+		for (auto& anime : list) {
+			if (anime.score != 0) {
+				squares_sum += std::pow((double)anime.score - avg, 2);
+				amt++;
+			}
+		}
+	}
+	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
+}
+
+#include "moc_anime_list.cpp"
--- a/src/pages/now_playing.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/pages/now_playing.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,6 +1,7 @@
-#include "window.h"
+#include "now_playing.h"
 
-NowPlaying::NowPlaying(page_t* page, wxPanel* frame) {
-	page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600));
-	page->panel->Show(false);
+NowPlayingWidget::NowPlayingWidget(QWidget* parent) : QWidget(parent) {
+	
 }
+
+#include "moc_now_playing.cpp"
\ No newline at end of file
--- a/src/pages/statistics.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/pages/statistics.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,32 +1,41 @@
-#include "window.h"
+#include <sstream>
+#include <QWidget>
+#include <QTimer>
+#include <QTextStream>
+#include <QString>
+#include <QTextDocument>
+#include <QVBoxLayout>
+#include "anime_list.h"
 #include "ui_utils.h"
-#include <sstream>
+#include "statistics.h"
+
+StatisticsWidget::StatisticsWidget(AnimeListWidget* listwidget, QWidget* parent)
+	: QFrame(parent) {
+	setLayout(new QVBoxLayout);
+	anime_list = listwidget;
 
-StatisticsTimer::StatisticsTimer(Statistics* caller) {
-	statistics = caller;
-}
+	setFrameShape(QFrame::Panel);
+	setFrameShadow(QFrame::Plain);
+
+	layout()->addWidget((anime_list_data = UiUtils::CreateTextParagraphWithLabels(this, "Anime list", "Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", ""))->parentWidget()->parentWidget());
+	((QBoxLayout*)layout())->addStretch();
 
-void StatisticsTimer::Notify() {
-	if (status.current_page == PAGE_STATISTICS)
-		statistics->UpdateStatistics();
+	QPalette pal = QPalette();
+	pal.setColor(QPalette::Window, Qt::white);
+	setAutoFillBackground(true); 
+	setPalette(pal);
+
+	UpdateStatistics(); // load in statistics as soon as possible
+
+	QTimer* timer = new QTimer(this);
+	connect(timer, &QTimer::timeout, this, [this]{
+		if (isVisible())
+			UpdateStatistics();
+	});
+	timer->start(1000); // update statistics every second
 }
 
-Statistics::Statistics(page_t* page, wxPanel* frame) {
-	page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600));
-	page->panel->Show(false);
-	panel = new wxPanel(page->panel, wxID_ANY, wxPoint(12, 12), wxSize(376, 576));
-	anime_list = ((WeeabooFrame*)frame->GetParent())->GetAnimeList();
-
-	/* FIXME: this should be moved to a separate function, it's also used in information.cpp */
-	// wxWindow* parent, const char* title, const char* label, const char* data, int width, int height, int x = 0, int y = 0, int selectable = 0
-	anime_list_data = UiUtils::CreateTextParagraphWithLabels(panel, L"Anime list", L"Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", L"", 376, 94);
-
-	UpdateStatistics(); // load in statistics as soon as possible
-	timer = new StatisticsTimer(this);
-	timer->Start(1000); // update statistics every second
-}
-
-std::string Statistics::MinutesToDateString(int minutes) {
+std::string StatisticsWidget::MinutesToDateString(int minutes) {
 	/* NOTE: these duration_casts may not be needed... */
 	std::chrono::duration<int, std::ratio<60>> int_total_mins(minutes);
 	auto int_years = std::chrono::duration_cast<std::chrono::years>(int_total_mins);
@@ -51,13 +60,14 @@
 	return return_stream.str();
 }
 
-void Statistics::UpdateStatistics() {
-	wxString string = "";
-	string << anime_list->GetTotalAnimeAmount() << '\n';
-	string << anime_list->GetTotalEpisodeAmount() << '\n';
-	string << MinutesToDateString(anime_list->GetTotalWatchedAmount()) << '\n';
-	string << MinutesToDateString(anime_list->GetTotalPlannedAmount()) << '\n';
-	string << anime_list->GetAverageScore() << '\n';
-	string << anime_list->GetScoreDeviation() << '\n';
-	anime_list_data->SetLabel(string);
+void StatisticsWidget::UpdateStatistics() {
+	QString string = "";
+	QTextStream ts(&string);
+	ts << anime_list->GetTotalAnimeAmount() << '\n';
+	ts << anime_list->GetTotalEpisodeAmount() << '\n';
+	ts << MinutesToDateString(anime_list->GetTotalWatchedAmount()).c_str() << '\n';
+	ts << MinutesToDateString(anime_list->GetTotalPlannedAmount()).c_str() << '\n';
+	ts << anime_list->GetAverageScore() << '\n';
+	ts << anime_list->GetScoreDeviation() << '\n';
+	UiUtils::SetPlainTextEditData(anime_list_data, string);
 }
--- a/src/sidebar.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/sidebar.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,7 +1,9 @@
+#include "sidebar.h"
 #include <QListWidget>
 #include <QListWidgetItem>
 #include <QFrame>
-#include "sidebar.h"
+#include <QMouseEvent>
+#include <QMessageBox>
 
 SideBar::SideBar(QWidget *parent)
     : QListWidget(parent)
@@ -10,8 +12,14 @@
 	setFrameShape(QFrame::NoFrame);
     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setSelectionMode(QAbstractItemView::SingleSelection);
+	setSelectionBehavior(QAbstractItemView::SelectItems);
+	setMouseTracking(true);
 	viewport()->setAutoFillBackground(false);
 	setStyleSheet("font-size: 12px");
+	connect(this, &QListWidget::currentRowChanged, this, [this](int index){
+		emit CurrentItemChanged(RemoveSeparatorsFromIndex(index));
+	});
 }
 
 QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
@@ -24,12 +32,44 @@
 
 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->setMouseTracking(true);
 	line->setEnabled(false);
 	setItemWidget(item, line);
+	item->setFlags(Qt::NoItemFlags);
 	return item;
 }
+
+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 * event) const {
+	if (IndexIsSeparator(index))
+		return QItemSelectionModel::NoUpdate;
+	return QItemSelectionModel::ClearAndSelect;
+	/* silence unused parameter warnings */
+	(void)event;
+}
+
+void SideBar::mouseMoveEvent(QMouseEvent *event) {
+	if (!IndexIsSeparator(indexAt(event->pos())))
+		setCursor(Qt::PointingHandCursor);
+	else
+		unsetCursor();
+	QListView::mouseMoveEvent(event);
+}
+
+#include "moc_sidebar.cpp"
--- a/src/string_utils.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/string_utils.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,7 +1,8 @@
 /**
  * string_utils.cpp: Useful functions for manipulating strings
  *
- * Every function in here *should* have a working wstring equivalent.
+ * Every function in here *should* have a working wstring equivalent,
+ * although we don't use wstrings anymore...
 **/
 #include <vector>
 #include <string>
@@ -9,10 +10,10 @@
 #include <locale>
 #include "string_utils.h"
 
-/* It's actually pretty insane how the standard library still doesn't
-   have a function for this. Look at how simple this is. */
 std::string StringUtils::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));
--- a/src/ui_utils.cpp	Wed Aug 16 00:49:17 2023 -0400
+++ b/src/ui_utils.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -1,5 +1,11 @@
+#include <QPixmap>
+#include <QLabel>
+#include <QFrame>
+#include <QVBoxLayout>
+#include <QTextBlock>
 #include "window.h"
 #include "ui_utils.h"
+#include "session.h"
 #ifdef MACOSX
 #include "sys/osx/dark_theme.h"
 #else
@@ -37,75 +43,130 @@
 	return (session.config.theme == DARK);
 }
 
-void UiUtils::CreateTextHeader(QWidget* parent, QString title, QPoint point, QSize size) {
+void UiUtils::CreateTextHeader(QWidget* parent, QString title) {
 	QLabel* static_text_title  = new QLabel(title, parent);
 	static_text_title->setTextFormat(Qt::PlainText);
-	static_text_title->setStyleSheet("font-weight: bold");
-	static_text_title->move(point.x(), point.y());
-	static_text_title->resize(size.width(), 16);
+
+	QFont font = static_text_title->font();
+	font.setWeight(QFont::Bold);
+	static_text_title->setFont(font);
+
+	static_text_title->setFixedHeight(16);
+	parent->layout()->addWidget(static_text_title);
 
 	QFrame* static_text_line = new QFrame(parent);
 	static_text_line->setFrameShape(QFrame::HLine);
 	static_text_line->setFrameShadow(QFrame::Sunken);
-	static_text_line->resize(size.width(), 2);
-	static_text_line->move(point.x(), point.y()+18);
+	static_text_line->setFixedHeight(2);
+	parent->layout()->addWidget(static_text_line);
 }
 
-QPlainTextEdit* UiUtils::CreateTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size) {
-	CreateTextHeader(parent, title, point, size);
+QPlainTextEdit* UiUtils::CreateTextParagraph(QWidget* parent, QString title, QString data) {
+	QWidget* paragraph_master = new QWidget(parent);
+	paragraph_master->setLayout(new QVBoxLayout);
 
-	QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent);
+	CreateTextHeader(paragraph_master, title);
+
+	Paragraph* paragraph = new Paragraph(data, paragraph_master);
 	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);
+	paragraph->setContentsMargins(12, 0, 0, 0);
+
+	paragraph_master->layout()->addWidget(paragraph);
+	paragraph_master->layout()->setSpacing(0);
+	paragraph_master->layout()->setMargin(0);
+	paragraph_master->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
 	return paragraph;
 }
 
-QPlainTextEdit* UiUtils::CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data, QPoint point, QSize size) {
-	CreateTextHeader(parent, title, point, size);
+QPlainTextEdit* UiUtils::CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data) {
+	QWidget* paragraph_master = new QWidget(parent);
+	paragraph_master->setLayout(new QVBoxLayout);
+
+	CreateTextHeader(paragraph_master, title);
 
-	QPlainTextEdit* label_t = new QPlainTextEdit(label, parent);
+	QWidget* paragraph_and_label = new QWidget(paragraph_master);
+	paragraph_and_label->setLayout(new QHBoxLayout);
+	paragraph_and_label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
+	Paragraph* label_t = new Paragraph(label, paragraph_and_label);
 	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);
+	label_t->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+	label_t->setFixedWidth(123);
 
-	QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent);
+	Paragraph* paragraph = new Paragraph(data, paragraph_and_label);
 	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);
+
+	((QBoxLayout*)paragraph_and_label->layout())->addWidget(label_t, 0, Qt::AlignTop);
+	((QBoxLayout*)paragraph_and_label->layout())->addWidget(paragraph, 0, Qt::AlignTop);
+
+	paragraph_and_label->setContentsMargins(12, 0, 0, 0);
+
+	paragraph_master->layout()->addWidget(paragraph_and_label);
+	paragraph_master->layout()->setSpacing(0);
+	paragraph_master->layout()->setMargin(0);
+	paragraph_master->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
 	return paragraph;
 }
 
 /* As far as I can tell, this is identical to the way Taiga implements it.
    Kind of cool, I didn't even look into the source code for it :p */
-QPlainTextEdit* UiUtils::CreateSelectableTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size) {
-	CreateTextHeader(parent, title, point, size);
+QPlainTextEdit* UiUtils::CreateSelectableTextParagraph(QWidget* parent, QString title, QString data) {
+	QWidget* paragraph_master = new QWidget(parent);
+	paragraph_master->setLayout(new QVBoxLayout);
+
+	CreateTextHeader(paragraph_master, title);
+
+	QWidget* paragraph_widget = new QWidget(paragraph_master);
+	paragraph_widget->setLayout(new QHBoxLayout);
+	paragraph_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
-	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);
+	Paragraph* text_edit = new Paragraph(data, paragraph_widget);
+
+	paragraph_widget->layout()->addWidget(text_edit);
+
+	paragraph_widget->setContentsMargins(12, 0, 0, 0);
+
+	paragraph_master->layout()->addWidget(paragraph_widget);
+	paragraph_master->layout()->setSpacing(0);
+	paragraph_master->layout()->setMargin(0);
+
+	paragraph_master->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
 	return text_edit;
 }
+
+void UiUtils::SetPlainTextEditData(QPlainTextEdit* text_edit, QString data) {
+	QTextDocument* document = new QTextDocument(text_edit);
+	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
+	document->setPlainText(data);
+	text_edit->setDocument(document);
+}
+
+Paragraph::Paragraph(QString text, QWidget* parent)
+	: QPlainTextEdit(text, parent) {
+	setReadOnly(true);
+	setTextInteractionFlags(Qt::TextBrowserInteraction);
+	setFrameShape(QFrame::NoFrame);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setStyleSheet("background: transparent;");
+	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+}
+
+QSize Paragraph::minimumSizeHint() const {
+	QTextDocument* doc = document();
+	long h = (long)(blockBoundingGeometry(doc->findBlockByNumber(doc->blockCount() - 1)).bottom() + (2 * doc->documentMargin()));
+	return QSize(QPlainTextEdit::sizeHint().width(), (long)h);
+}
+
+QSize Paragraph::sizeHint() const {
+	return minimumSizeHint();
+}