changeset 291:9a88e1725fd2

*: refactor lots of stuff I forgot to put this into different commits, oops! anyway, it doesn't really matter *that* much since this is an unfinished hobby project anyway. once it starts getting stable commit history will be more important, but for now it's not that big of a deal
author Paper <paper@paper.us.eu.org>
date Sun, 12 May 2024 16:31:07 -0400
parents 9347e2eaf6e5
children ac1451035c85
files Makefile.am configure.ac include/core/http.h include/core/session.h include/gui/pages/anime_list.h include/gui/widgets/anime_info.h include/gui/widgets/poster.h include/gui/widgets/text.h include/gui/window.h src/core/http.cc src/gui/dialog/information.cc src/gui/pages/anime_list.cc src/gui/pages/seasons.cc src/gui/pages/statistics.cc src/gui/pages/torrents.cc src/gui/theme.cc src/gui/widgets/anime_button.cc src/gui/widgets/anime_info.cc src/gui/widgets/poster.cc src/gui/widgets/sidebar.cc src/gui/widgets/text.cc src/gui/window.cc src/services/anilist.cc
diffstat 23 files changed, 438 insertions(+), 266 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.am	Wed May 08 17:32:28 2024 -0400
+++ b/Makefile.am	Sun May 12 16:31:07 2024 -0400
@@ -20,7 +20,8 @@
 
 minori_qtrc = \
 	$(top_srcdir)/rc/icons/icons.qrc	\
-	$(top_srcdir)/rc/animone.qrc
+	$(top_srcdir)/rc/animone.qrc	\
+	rc/locale/translations.qrc
 
 # various things we want to distribute
 
@@ -182,6 +183,8 @@
 minori_utf8proc_sources = \
 	dep/utf8proc/utf8proc.c
 
+minori_moc_sources = $(minori_qtheaders:.h=_moc.cc)
+
 minori_SOURCES = \
 	src/core/anime_db.cc		\
 	src/core/anime.cc		\
@@ -239,11 +242,8 @@
 	$(minori_locale_qm)	\
 	$(minori_moc_sources)	\
 	$(minori_utf8proc_sources)	\
-	rc/locale/translations.qrc	\
 	rc/final_qrc.cc
 
-minori_moc_sources = $(minori_qtheaders:.h=_moc.cc)
-
 minori_includes = \
 	-I$(top_srcdir)/include \
 	-I$(top_srcdir)/dep/animone/include \
--- a/configure.ac	Wed May 08 17:32:28 2024 -0400
+++ b/configure.ac	Sun May 12 16:31:07 2024 -0400
@@ -23,49 +23,65 @@
 
 
 dnl Qt?
+
+
 PKG_CHECK_MODULES([QT], [Qt5Core >= 5.7.0 Qt5Widgets Qt5Gui], [
 	QT_PATH="$(eval $PKG_CONFIG --variable=exec_prefix Qt5Core)"
 	QT_HOST_PATH="$(eval $PKG_CONFIG --variable=host_bins Qt5Core)"
-	QT_VERSION="$(eval $PKG_CONFIG --modversion Qt5Gui)"
+
 	AC_PATH_PROGS(QT_MOC,      [moc-qt5 moc],           moc,      ["${QT_HOST_PATH}" "${QT_PATH}/bin"])
 	AC_PATH_PROGS(QT_RCC,      [rcc-qt5 rcc],           rcc,      ["${QT_HOST_PATH}" "${QT_PATH}/bin"])
 	AC_PATH_PROGS(QT_UIC,      [uic-qt5 uic],           uic,      ["${QT_HOST_PATH}" "${QT_PATH}/bin"])
 	AC_PATH_PROGS(QT_LRELEASE, [lrelease-qt5 lrelease], lrelease, ["${QT_HOST_PATH}" "${QT_PATH}/bin"])
 	AC_PATH_PROGS(QT_LUPDATE,  [lupdate-qt5 lupdate],   lupdate,  ["${QT_HOST_PATH}" "${QT_PATH}/bin"])
-], [
-	AC_MSG_ERROR([${QT_PKG_ERRORS}.])
-])
 
-dnl On some platforms (see: Debian), Qt is built with
-dnl `-reduce-relocations`, which requires applications
-dnl to be built with position-independent code.
-dnl
-dnl Unfortunately there's no way to check for this
-dnl without using qmake (bleugh), so we use this check
-dnl to see if qglobal.h can be included without PIC.
+	dnl On some platforms (see: Debian), Qt is built with
+	dnl `-reduce-relocations`, which requires applications
+	dnl to be built with position-independent code.
+	dnl
+	dnl Unfortunately there's no way to check for this
+	dnl without using qmake (bleugh), so we use this check
+	dnl to see if qglobal.h can be included without PIC.
 
-AC_MSG_CHECKING([for Qt requiring -fPIC])
+	AC_MSG_CHECKING([for Qt requiring -fPIC])
 
-saved_CXXFLAGS="$CXXFLAGS"
-CXXFLAGS="$CXXFLAGS $QT_CFLAGS"
-AC_COMPILE_IFELSE(
-	[AC_LANG_PROGRAM([#include <qglobal.h>], [])],
-	[
-		AC_MSG_RESULT([no])
-		CXXFLAGS="$saved_CXXFLAGS"
-	],
-	[
-		AC_MSG_RESULT([yes])
-		CXXFLAGS="$saved_CXXFLAGS -fPIC -DPIC"
-	]
-)
+	saved_CXXFLAGS="$CXXFLAGS"
+	CXXFLAGS="$CXXFLAGS $QT_CFLAGS"
+	AC_COMPILE_IFELSE(
+		[AC_LANG_PROGRAM([#include <qglobal.h>], [])],
+		[
+			AC_MSG_RESULT([no])
+			CXXFLAGS="$saved_CXXFLAGS"
+		],
+		[
+			AC_MSG_RESULT([yes])
+			CXXFLAGS="$saved_CXXFLAGS -fPIC -DPIC"
+		]
+	)
+], [
+	PKG_CHECK_MODULES([QT], [Qt6Core Qt6Widgets Qt6Gui], [
+        QT_BIN_DIRECTORY="$(eval $PKG_CONFIG --variable=bindir Qt6Core)"
+        QT_LIBEXEC_DIRECTORY="$(eval $PKG_CONFIG --variable=libexecdir Qt6Core)"
+
+		AC_PATH_PROGS(QT_MOC, [moc], moc, ["${QT_LIBEXEC_DIRECTORY}"])
+		AC_PATH_PROGS(QT_RCC, [rcc], rcc, ["${QT_LIBEXEC_DIRECTORY}"])
+		AC_PATH_PROGS(QT_UIC, [uic], uic, ["${QT_LIBEXEC_DIRECTORY}"])
+		AC_PATH_PROGS(QT_LRELEASE, [lrelease], lrelease, ["${QT_BIN_DIRECTORY}"])
+		AC_PATH_PROGS(QT_LUPDATE, [lupdate], lupdate, ["${QT_BIN_DIRECTORY}"])
+
+		dnl in my testing Qt 6 seems to require PIC unconditionally...
+		CXXFLAGS="$CXXFLAGS -fPIC"
+	], [
+		AC_MSG_ERROR([${QT_PKG_ERRORS}.])
+	])
+])
 
 dnl need this for moc
 AC_PROG_MKDIR_P
 AC_SUBST([MKDIR_P])
 
 dnl libcurl?
-LIBCURL_CHECK_CONFIG([yes], [7.7.2], [have_libcurl=yes], [have_libcurl=no])
+LIBCURL_CHECK_CONFIG([yes], [7.87.0], [have_libcurl=yes], [have_libcurl=no])
 
 AS_IF([test "x$have_libcurl" = "xno"], [AC_MSG_ERROR([*** libcurl not found.])])
 
--- a/include/core/http.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/core/http.h	Sun May 12 16:31:07 2024 -0400
@@ -3,55 +3,54 @@
 
 #include <QByteArray>
 #include <QThread>
+
 #include <string>
 #include <vector>
+#include <mutex>
 
 namespace HTTP {
 
-QByteArray Get(const std::string& url, const std::vector<std::string>& headers = {});
-QByteArray Post(const std::string& url, const std::string& data, const std::vector<std::string>& headers = {});
+enum class Type {
+	Get,
+	Post
+};
 
-class GetThread : public QThread {
+QByteArray Request(const std::string& url, const std::vector<std::string>& headers = {}, const std::string& data = "", Type type = Type::Get);
+
+class RequestThread final : public QThread {
 	Q_OBJECT
 
 public:
-	GetThread(const std::string& u, const std::vector<std::string>& h = {}, QObject* parent = nullptr)
-	    : QThread(parent) {
-		url = u;
-		headers = h;
-	}
+	RequestThread(Type type = Type::Get, QObject* parent = nullptr);
+	RequestThread(const std::string& url, const std::vector<std::string>& headers = {},
+		          const std::string& data = "", Type type = Type::Get, QObject* parent = nullptr);
+	~RequestThread();
+
+	void SetUrl(const std::string& url);
+	void SetHeaders(const std::vector<std::string>& headers);
+	void SetData(const std::string& data);
+	void SetType(Type type);
+
+	void Stop();
 
 signals:
 	void ReceivedData(const QByteArray& ba);
 
 protected:
-	void run() override { emit ReceivedData(Get(url, headers)); }
-
-	std::string url;
-	std::vector<std::string> headers;
-};
-
-class PostThread : public QThread {
-	Q_OBJECT
+	void run() override;
+	static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userdata);
 
-public:
-	PostThread(const std::string& u, const std::string& d, const std::vector<std::string>& h = {},
-	           QObject* parent = nullptr)
-	    : QThread(parent) {
-		url = u;
-		data = d;
-		headers = h;
-	}
+	std::string url_;
+	std::string data_;
+	std::vector<std::string> headers_;
+	Type type_;
 
-signals:
-	void ReceivedData(const QByteArray& ba);
+	/* these are passed to the write callback */
+	QByteArray array_;
+	bool cancelled_ = false;
 
-protected:
-	void run() override { emit ReceivedData(Post(url, data, headers)); }
-
-	std::string url;
-	std::string data;
-	std::vector<std::string> headers;
+	/* don't fuck this up */
+	std::mutex callback_data_mutex_;
 };
 
 } // namespace HTTP
--- a/include/core/session.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/core/session.h	Sun May 12 16:31:07 2024 -0400
@@ -7,21 +7,22 @@
 
 #include "semver/semver.hpp"
 
-class MainWindow;
+#include <atomic>
 
 struct Session {
 public:
 	Session() { timer.start(); }
 	/* we literally *cannot* be lying to the user by doing this */
 	void IncrementRequests() { requests++; };
-	int GetRequests() { return requests; };
+	unsigned int GetRequests() { return requests; };
 	int uptime() { return timer.elapsed(); }
 
 	Config config;
 	static constexpr semver::version version{PACKAGE_VERSION};
 
 private:
-	unsigned int requests = 0;
+	/* IncrementRequests() gets called by different threads */
+	std::atomic<unsigned int> requests = 0;
 	QElapsedTimer timer;
 };
 
--- a/include/gui/pages/anime_list.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/gui/pages/anime_list.h	Sun May 12 16:31:07 2024 -0400
@@ -79,7 +79,7 @@
 	Q_OBJECT
 
 public:
-	AnimeListPage(QWidget* parent);
+	AnimeListPage(QWidget* parent = nullptr);
 	void Refresh();
 
 protected:
--- a/include/gui/widgets/anime_info.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/gui/widgets/anime_info.h	Sun May 12 16:31:07 2024 -0400
@@ -2,12 +2,7 @@
 #define MINORI_GUI_WIDGETS_ANIME_INFO_H_
 
 #include <QWidget>
-
-namespace TextWidgets {
-class OneLineSection;
-class LabelledSection;
-class SelectableSection;
-} // namespace TextWidgets
+#include "gui/widgets/text.h"
 
 namespace Anime {
 class Anime;
@@ -22,9 +17,9 @@
 	void SetAnime(const Anime::Anime& anime);
 
 private:
-	std::shared_ptr<TextWidgets::OneLineSection> _title = nullptr;
-	std::shared_ptr<TextWidgets::LabelledSection> _details = nullptr;
-	std::shared_ptr<TextWidgets::SelectableSection> _synopsis = nullptr;
+	TextWidgets::OneLineSection _title;
+	TextWidgets::LabelledSection _details;
+	TextWidgets::SelectableSection _synopsis;
 };
 
 #endif // MINORI_GUI_WIDGETS_ANIME_INFO_H_
--- a/include/gui/widgets/poster.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/gui/widgets/poster.h	Sun May 12 16:31:07 2024 -0400
@@ -4,6 +4,7 @@
 #include <QImage>
 
 #include "gui/widgets/clickable_label.h"
+#include "core/http.h"
 
 class QWidget;
 namespace Anime {
@@ -25,6 +26,8 @@
 	void RenderToLabel();
 
 private:
+	HTTP::RequestThread get_thread_;
+
 	QImage img_;
 	QString service_url_;
 	ClickableLabel label_;
--- a/include/gui/widgets/text.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/gui/widgets/text.h	Sun May 12 16:31:07 2024 -0400
@@ -21,16 +21,24 @@
 	void SetText(const QString& title);
 
 private:
-	QLabel* static_text_title;
-	QFrame* static_text_line;
+	QLabel static_text_title;
+	QFrame static_text_line;
 };
 
-class Paragraph : public QLabel {
+class Paragraph : public QWidget {
 	Q_OBJECT
 
 public:
 	Paragraph(const QString& text, QWidget* parent = nullptr);
 	void SetText(const QString& text);
+	QPlainTextEdit* GetLabel();
+
+protected:
+	QSize minimumSizeHint() const;
+	QSize sizeHint() const;
+
+private:
+	QPlainTextEdit text_edit;
 };
 
 class LabelledParagraph final : public QWidget {
@@ -38,21 +46,27 @@
 
 public:
 	LabelledParagraph(const QString& label, const QString& data, QWidget* parent = nullptr);
-	Paragraph* GetLabels();
-	Paragraph* GetParagraph();
+	QLabel* GetLabels();
+	QLabel* GetData();
+
+	/* synonymous with GetData(), kept for compatibility. don't use in new code!!! */
+	QLabel* GetParagraph();
 
 private:
-	Paragraph* labels;
-	Paragraph* paragraph;
+	QLabel labels_;
+	QLabel data_;
 };
 
-class Line : public Paragraph {
+class Line : public QWidget {
 	Q_OBJECT
 
 public:
 	Line(QWidget* parent = nullptr);
 	Line(const QString& text, QWidget* parent = nullptr);
 	void SetText(const QString& text);
+
+protected:
+	QLineEdit line_edit_;
 };
 
 class Title final : public Line {
@@ -81,8 +95,9 @@
 public:
 	LabelledSection(const QString& title, const QString& label, const QString& data, QWidget* parent = nullptr);
 	Header* GetHeader();
-	Paragraph* GetLabels();
-	Paragraph* GetParagraph();
+	QLabel* GetLabels();
+	QLabel* GetData();
+	QLabel* GetParagraph();
 
 private:
 	Header* header;
--- a/include/gui/window.h	Wed May 08 17:32:28 2024 -0400
+++ b/include/gui/window.h	Sun May 12 16:31:07 2024 -0400
@@ -1,22 +1,31 @@
 #ifndef MINORI_WINDOW_H_
 #define MINORI_WINDOW_H_
+
 #include "core/config.h"
-#include <QMainWindow>
-#include <memory>
+#include "gui/widgets/sidebar.h"
 
-/* *could* be forward-declared, but this causes
-   any file that #includes this to have to #include
-   these as well due to unique_ptr */
-#include "gui/widgets/sidebar.h"
+/* pages; these should really be in a namespace */
+#include "gui/pages/anime_list.h"
+#include "gui/pages/history.h"
+#include "gui/pages/now_playing.h"
+#include "gui/pages/search.h"
+#include "gui/pages/seasons.h"
+#include "gui/pages/statistics.h"
+#include "gui/pages/torrents.h"
+
+#include <QMainWindow>
 #include <QCloseEvent>
 #include <QStackedWidget>
 #include <QThread>
 #include <QTimer>
 #include <QWidget>
 
+#include <memory>
+
 class QMenu;
 class AnimeListPage;
 
+/* ... :) */
 Q_DECLARE_METATYPE(std::vector<std::string>);
 
 class MainWindowPlayingThread final : public QThread {
@@ -80,6 +89,14 @@
 	QStackedWidget stack_;
 	SideBar sidebar_;
 
+	AnimeListPage anime_list_page_;
+	HistoryPage history_page_;
+	NowPlayingPage now_playing_page_;
+	SearchPage search_page_;
+	SeasonsPage seasons_page_;
+	StatisticsPage statistics_page_;
+	TorrentsPage torrents_page_;
+
 	MainWindowPlayingThread playing_thread_;
 	QTimer playing_thread_timer_;
 
--- a/src/core/http.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/core/http.cc	Sun May 12 16:31:07 2024 -0400
@@ -13,16 +13,18 @@
 	return size * nmemb;
 }
 
-QByteArray Get(const std::string& url, const std::vector<std::string>& headers) {
+QByteArray Request(const std::string& url, const std::vector<std::string>& headers, const std::string& data, Type type) {
 	struct curl_slist* list = NULL;
 	QByteArray userdata;
 
 	CURL* curl = curl_easy_init();
 	if (curl) {
-		for (const std::string& h : headers) {
+		for (const std::string& h : headers)
 			list = curl_slist_append(list, h.c_str());
-		}
+
 		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+		if (type == Type::Post)
+			curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
 		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
 		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
 		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
@@ -38,30 +40,99 @@
 	return userdata;
 }
 
-QByteArray Post(const std::string& url, const std::string& data, const std::vector<std::string>& headers) {
+/* this function is static */
+size_t RequestThread::WriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) {
+	RequestThread* thread = reinterpret_cast<RequestThread*>(userdata);
+
+	const std::lock_guard<std::mutex> lock(thread->callback_data_mutex_);
+
+	/* stop writing, then! */
+	if (thread->cancelled_)
+		return CURL_WRITEFUNC_ERROR;
+
+	/* else, continue on as normal */
+	thread->array_.append(reinterpret_cast<char*>(contents), size * nmemb);
+	return size * nmemb;
+}
+
+RequestThread::RequestThread(Type type, QObject* parent) : QThread(parent) {
+	SetType(type);
+}
+
+RequestThread::RequestThread(const std::string& url, const std::vector<std::string>& headers,
+	                         const std::string& data, Type type, QObject* parent)
+	: QThread(parent) {
+	SetUrl(url);
+	SetData(data);
+	SetHeaders(headers);
+	SetType(type);
+}
+
+RequestThread::~RequestThread() {
+	/* block until the function can safely exit.
+	 *
+	 * this sucks. find out a better way to do this, which will probably
+	 * be to put all of the threads in a pool */
+	Stop();
+	wait();
+}
+
+void RequestThread::SetUrl(const std::string& url) {
+	url_ = url;
+}
+
+void RequestThread::SetHeaders(const std::vector<std::string>& headers) {
+	headers_ = headers;
+}
+
+void RequestThread::SetData(const std::string& data) {
+	data_ = data;
+}
+
+void RequestThread::SetType(Type type) {
+	type_ = type;
+}
+
+void RequestThread::run() {
 	struct curl_slist* list = NULL;
-	QByteArray userdata;
 
 	CURL* curl = curl_easy_init();
 	if (curl) {
-		for (const std::string& h : headers) {
+		curl_easy_setopt(curl, CURLOPT_URL, url_.c_str());
+
+		if (type_ == Type::Post)
+			curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data_.c_str());
+
+		for (const std::string& h : headers_)
 			list = curl_slist_append(list, h.c_str());
-		}
-		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
 		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
+
+		curl_easy_setopt(curl, CURLOPT_WRITEDATA, this);
+		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &RequestThread::WriteCallback);
+
 		/* Use system certs... useful on Windows. */
 		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
-		curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); // threading
+
+		/* does something with threading, don't remember what though */
+		curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
+
 		CURLcode res = curl_easy_perform(curl);
 		session.IncrementRequests();
 		curl_easy_cleanup(curl);
-		if (res != CURLE_OK)
+
+		callback_data_mutex_.lock();
+		if (res != CURLE_OK && !(res == CURLE_WRITE_ERROR && cancelled_))
 			std::cerr << "curl_easy_perform(curl) failed!: " << curl_easy_strerror(res) << std::endl;
+		callback_data_mutex_.unlock();
 	}
-	return userdata;
+
+	emit ReceivedData(array_);
+	array_.clear();
+}
+
+void RequestThread::Stop() {
+	const std::lock_guard<std::mutex> lock(callback_data_mutex_);
+	cancelled_ = true;
 }
 
 } // namespace HTTP
--- a/src/gui/dialog/information.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/dialog/information.cc	Sun May 12 16:31:07 2024 -0400
@@ -171,7 +171,7 @@
 								QComboBox* combo_box = new QComboBox(section);
 
 								for (unsigned int i = 0; i < Anime::ListStatuses.size(); i++)
-									combo_box->addItem(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])),
+									combo_box->addItem(Strings::ToQString(Translate::ToLocalString(Anime::ListStatuses[i])),
 									                   static_cast<int>(Anime::ListStatuses[i]));
 
 								connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
--- a/src/gui/pages/anime_list.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/pages/anime_list.cc	Sun May 12 16:31:07 2024 -0400
@@ -320,6 +320,7 @@
 			dialog->show();
 			dialog->raise();
 			dialog->activateWindow();
+			connect(dialog, &InformationDialog::finished, dialog, &InformationDialog::deleteLater);
 		}
 	});
 	menu->addSeparator();
@@ -331,6 +332,7 @@
 			dialog->show();
 			dialog->raise();
 			dialog->activateWindow();
+			connect(dialog, &InformationDialog::finished, dialog, &InformationDialog::deleteLater);
 		}
 	});
 	menu->addAction(tr("Delete from list..."), [this, animes] {
@@ -361,6 +363,7 @@
 	dialog->show();
 	dialog->raise();
 	dialog->activateWindow();
+	connect(dialog, &InformationDialog::finished, dialog, &InformationDialog::deleteLater);
 }
 
 void AnimeListPage::RefreshList() {
--- a/src/gui/pages/seasons.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/pages/seasons.cc	Sun May 12 16:31:07 2024 -0400
@@ -48,7 +48,7 @@
 		toolbar->setMovable(false);
 
 		{
-			/* hard-coded this value */
+			/* todo: clean this up... this sucks... */
 			static constexpr Date::Year last_year = 1960;
 
 			auto create_year_menu = [this](QWidget* parent, QMenu* parent_menu, Date::Year year){
@@ -72,12 +72,12 @@
 			};
 
 			/* we'll be extinct by the time this code breaks, so I guess it's fine :) */
-			const Date::Year year = static_cast<Date::Year>(QDate::currentDate().year());
-			const Date::Year year_before_collapse = GetClosestDecade(year) - 10;
+			const Date::Year current_year = static_cast<Date::Year>(QDate::currentDate().year());
+			const Date::Year year_before_collapse = GetClosestDecade(current_year) - 10;
 			season_button = new QToolButton(toolbar);
 			QMenu* full_season_menu = new QMenu(season_button);
 
-			for (Date::Year c = year; c >= year_before_collapse; c--)
+			for (Date::Year c = current_year; c >= year_before_collapse; c--)
 				create_year_menu(season_button, full_season_menu, c);
 
 			full_season_menu->addSeparator();
@@ -103,15 +103,15 @@
 			{
 				/* links */
 				QMenu* menu = new QMenu(button);
-				menu->addAction("Airing status");
-				menu->addAction("List status");
-				menu->addAction("Type");
+				menu->addAction(tr("Airing status"));
+				menu->addAction(tr("List status"));
+				menu->addAction(tr("Type"));
 				button->setMenu(menu);
 			}
 
 			button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
 			button->setIcon(QIcon(":/icons/16x16/category.png"));
-			button->setText("Group by:");
+			button->setText(tr("Group by:"));
 			button->setPopupMode(QToolButton::InstantPopup);
 			toolbar->addWidget(button);
 		}
@@ -122,17 +122,17 @@
 			{
 				/* links */
 				QMenu* menu = new QMenu(button);
-				menu->addAction("Airing date");
-				menu->addAction("Episodes");
-				menu->addAction("Popularity");
-				menu->addAction("Score");
-				menu->addAction("Title");
+				menu->addAction(tr("Airing date"));
+				menu->addAction(tr("Episodes"));
+				menu->addAction(tr("Popularity"));
+				menu->addAction(tr("Score"));
+				menu->addAction(tr("Title"));
 				button->setMenu(menu);
 			}
 
 			button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
 			button->setIcon(QIcon(":/icons/16x16/sort-quantity-descending.png"));
-			button->setText("Sort by:");
+			button->setText(tr("Sort by:"));
 			button->setPopupMode(QToolButton::InstantPopup);
 			toolbar->addWidget(button);
 		}
@@ -143,14 +143,14 @@
 			{
 				/* links */
 				QMenu* menu = new QMenu(button);
-				menu->addAction("Images");
-				menu->addAction("Details");
+				menu->addAction(tr("Images"));
+				menu->addAction(tr("Details"));
 				button->setMenu(menu);
 			}
 
 			button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
 			button->setIcon(QIcon(":/icons/16x16/ui-scroll-pane-detail.png"));
-			button->setText("View:");
+			button->setText(tr("View:"));
 			button->setPopupMode(QToolButton::InstantPopup);
 			toolbar->addWidget(button);
 		}
--- a/src/gui/pages/statistics.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/pages/statistics.cc	Sun May 12 16:31:07 2024 -0400
@@ -141,7 +141,7 @@
 	ts << Strings::ToQString(TimeToDateString(TimeUnits::MINUTES, Anime::db.GetTotalPlannedAmount(), 60.0)) << '\n';
 	ts << Anime::db.GetAverageScore() << '\n';
 	ts << Anime::db.GetScoreDeviation();
-	_anime_list->GetParagraph()->SetText(string);
+	_anime_list->GetData()->setText(string);
 
 	_score_distribution_graph->Clear();
 	for (int i = 10; i <= 100; i += 10)
@@ -152,5 +152,5 @@
 	ts << session.GetRequests();
 	/* Application */
 	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
-	_application->GetParagraph()->SetText(string);
+	_application->GetData()->setText(string);
 }
--- a/src/gui/pages/torrents.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/pages/torrents.cc	Sun May 12 16:31:07 2024 -0400
@@ -65,9 +65,10 @@
 		const std::filesystem::path torrents_dir = Filesystem::GetTorrentsPath();
 		std::filesystem::create_directories(torrents_dir);
 
-		HTTP::GetThread* thread = new HTTP::GetThread(link, {}, this);
+		/* this sucks */
+		HTTP::RequestThread* thread = new HTTP::RequestThread(link, {}, "", HTTP::Type::Get, this);
 
-		connect(thread, &HTTP::GetThread::ReceivedData, this, [this, torrents_dir, filename](const QByteArray& data) {
+		connect(thread, &HTTP::RequestThread::ReceivedData, this, [this, torrents_dir, filename](const QByteArray& data) {
 			std::ofstream file(torrents_dir / filename, std::ofstream::out | std::ofstream::trunc);
 			if (!file)
 				return; // wat
@@ -75,14 +76,14 @@
 			file.write(data.data(), data.size());
 			file.close();
 		});
-		connect(thread, &HTTP::GetThread::finished, thread, &HTTP::GetThread::deleteLater);
+		connect(thread, &HTTP::RequestThread::finished, thread, &HTTP::RequestThread::deleteLater);
 
 		thread->start();
 	}
 }
 
 QByteArray TorrentsPageListModel::DownloadTorrentList() {
-	return HTTP::Get(session.config.torrents.feed_link);
+	return HTTP::Request(session.config.torrents.feed_link);
 }
 
 void TorrentsPageListModel::ParseFeedDescription(const std::string& description, Torrent& torrent) {
@@ -416,9 +417,9 @@
 	if (!model)
 		return;
 
-	HTTP::GetThread* thread = new HTTP::GetThread(session.config.torrents.feed_link);
+	HTTP::RequestThread* thread = new HTTP::RequestThread(session.config.torrents.feed_link);
 
-	connect(thread, &HTTP::GetThread::ReceivedData, this, [&](const QByteArray& ba) {
+	connect(thread, &HTTP::RequestThread::ReceivedData, this, [&](const QByteArray& ba) {
 		/* This is to make sure we aren't in a different thread
 		 * messing around with GUI stuff
 		 */
@@ -426,7 +427,7 @@
 		model->ParseTorrentList(ba);
 		treeview->setUpdatesEnabled(true);
 	});
-	connect(thread, &QThread::finished, thread, &QThread::deleteLater);
+	connect(thread, &HTTP::RequestThread::finished, thread, &HTTP::RequestThread::deleteLater);
 
 	thread->start();
 }
--- a/src/gui/theme.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/theme.cc	Sun May 12 16:31:07 2024 -0400
@@ -7,6 +7,7 @@
 #include <QStyleFactory>
 #include <QTextStream>
 #include <QStyle>
+#include <QtGlobal>
 #ifdef MACOSX
 #	include "sys/osx/dark_theme.h"
 #elif defined(WIN32)
--- a/src/gui/widgets/anime_button.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/widgets/anime_button.cc	Sun May 12 16:31:07 2024 -0400
@@ -91,10 +91,10 @@
 
 	{
 		const QLocale& locale = session.config.locale.GetLocale();
-		_info.GetParagraph()->SetText(locale.toString(anime.GetAirDate().GetAsQDate(), "dd MMM yyyy") + "\n" +
-		                              QString::number(anime.GetEpisodes()) + "\n" +
-		                              Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) + "\n" + "...\n" +
-		                              QString::number(anime.GetAudienceScore()) + "%\n" + "...");
+		_info.GetData()->setText(locale.toString(anime.GetAirDate().GetAsQDate(), "dd MMM yyyy") + "\n" +
+		                         QString::number(anime.GetEpisodes()) + "\n" +
+		                         Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) + "\n" + "...\n" +
+		                         QString::number(anime.GetAudienceScore()) + "%\n" + "...");
 	}
 
 	_synopsis.SetText(Strings::ToQString(anime.GetSynopsis()));
--- a/src/gui/widgets/anime_info.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/widgets/anime_info.cc	Sun May 12 16:31:07 2024 -0400
@@ -6,21 +6,16 @@
 #include <QHBoxLayout>
 #include <QTextStream>
 
-AnimeInfoWidget::AnimeInfoWidget(QWidget* parent) : QWidget(parent) {
+AnimeInfoWidget::AnimeInfoWidget(QWidget* parent)
+	: QWidget(parent)
+	, _title(tr("Alternative titles"), "")
+	, _details(tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), "")
+	, _synopsis(tr("Synopsis"), "") {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
-	_title.reset(new TextWidgets::OneLineSection(tr("Alternative titles"), "", this));
-	layout->addWidget(_title.get());
-
-	_details.reset(new TextWidgets::LabelledSection(
-	    tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), "", this));
-	layout->addWidget(_details.get());
-
-	_synopsis.reset(new TextWidgets::SelectableSection(tr("Synopsis"), "", this));
-	_synopsis->GetParagraph()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
-	layout->addWidget(_synopsis.get());
-
-	layout->addStretch();
+	layout->addWidget(&_title);
+	layout->addWidget(&_details);
+	layout->addWidget(&_synopsis);
 }
 
 AnimeInfoWidget::AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent) : AnimeInfoWidget(parent) {
@@ -29,15 +24,15 @@
 
 void AnimeInfoWidget::SetAnime(const Anime::Anime& anime) {
 	/* alt titles */
-	_title->GetLine()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	_title.GetLine()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
 
 	/* details */
 	QString details_data;
 	QTextStream details_data_s(&details_data);
+
 	/* we have to convert ALL of these strings to
 	 * QString because QTextStream sucks and assumes
-	 * Latin1 (on Windows?)
-	 */
+	 * Latin1 (on Windows?) */
 	const auto genres = anime.GetGenres();
 	details_data_s << Strings::ToQString(Translate::ToLocalString(anime.GetFormat())) << "\n"
 	               << anime.GetEpisodes() << "\n"
@@ -46,9 +41,9 @@
 	               << anime.GetAirDate().GetYear().value_or(2000) << "\n"
 	               << Strings::ToQString((genres.size() > 1) ? Strings::Implode(genres, ", ") : "-") << "\n"
 	               << anime.GetAudienceScore() << "%";
-	_details->GetParagraph()->SetText(details_data);
+	_details.GetData()->setText(details_data);
 
-	_synopsis->GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis()));
+	_synopsis.GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis()));
 
 	updateGeometry();
 }
--- a/src/gui/widgets/poster.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/widgets/poster.cc	Sun May 12 16:31:07 2024 -0400
@@ -27,6 +27,8 @@
 
 	label_.setAlignment(Qt::AlignCenter);
 	layout->addWidget(&label_);
+
+	connect(&get_thread_, &HTTP::RequestThread::ReceivedData, this, &Poster::ImageDownloadFinished);
 }
 
 Poster::Poster(const Anime::Anime& anime, QWidget* parent) : Poster(parent) {
@@ -35,14 +37,12 @@
 
 void Poster::SetAnime(const Anime::Anime& anime) {
 	/* todo: only download on showEvent() */
-	{
-		HTTP::GetThread* thread = new HTTP::GetThread(anime.GetPosterUrl(), {}, this);
+	if (get_thread_.isRunning())
+		get_thread_.Stop();
+	get_thread_.wait();
 
-		connect(thread, &HTTP::GetThread::ReceivedData, this, &Poster::ImageDownloadFinished);
-		connect(thread, &HTTP::GetThread::finished, thread, &HTTP::GetThread::deleteLater);
-
-		thread->start();
-	}
+	get_thread_.SetUrl(anime.GetPosterUrl());
+	get_thread_.start();
 
 	std::optional<std::string> url = anime.GetServiceUrl(session.config.service);
 	if (url)
--- a/src/gui/widgets/sidebar.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/widgets/sidebar.cc	Sun May 12 16:31:07 2024 -0400
@@ -30,10 +30,6 @@
 
 void SideBar::SetBackgroundColor(QColor color) {
 	viewport()->setAutoFillBackground(color != Qt::transparent);
-
-	QPalette pal(palette());
-	pal.setColor(QPalette::Window, color);
-	setPalette(pal);
 }
 
 QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
--- a/src/gui/widgets/text.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/widgets/text.cc	Sun May 12 16:31:07 2024 -0400
@@ -6,82 +6,130 @@
 #include <QLabel>
 #include <QTextBlock>
 #include <QVBoxLayout>
+#include <QScrollArea>
+#include <QDebug>
+
+/* WARNING: GARBAGE CODE FOLLOWS
+ *
+ * This file is filled with spaghetti to make this
+ * stupid text render how I want it to.
+ *
+ * many cases of hacking with setSizePolicy() are seen
+ * all around this file. Edit it only if really
+ * necessary, please.
+*/
 
 namespace TextWidgets {
 
-Header::Header(const QString& title, QWidget* parent) : QWidget(parent) {
+Header::Header(const QString& title, QWidget* parent)
+	: QWidget(parent)
+	, static_text_title(title) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
-	static_text_title = new QLabel(title, this);
-	static_text_title->setTextFormat(Qt::PlainText);
+	static_text_title.setTextFormat(Qt::PlainText);
 
 	{
-		QFont font = static_text_title->font();
+		QFont font = static_text_title.font();
 		font.setWeight(QFont::Bold);
-		static_text_title->setFont(font);
+		static_text_title.setFont(font);
 	}
 
-	static_text_line = new QFrame(this);
-	static_text_line->setFrameShape(QFrame::HLine);
-	static_text_line->setFrameShadow(QFrame::Sunken);
-	static_text_line->setFixedHeight(2);
+	static_text_line.setFrameShape(QFrame::HLine);
+	static_text_line.setFrameShadow(QFrame::Sunken);
+	static_text_line.setFixedHeight(2);
 
-	layout->addWidget(static_text_title);
-	layout->addWidget(static_text_line);
+	layout->addWidget(&static_text_title);
+	layout->addWidget(&static_text_line);
 	layout->setSpacing(0);
 	layout->setContentsMargins(0, 0, 0, 0);
 }
 
 void Header::SetText(const QString& text) {
-	static_text_title->setText(text);
+	static_text_title.setText(text);
 	updateGeometry();
 }
 
-/* for now, this is a QLabel with a couple of default settings.
- *
- * eventually I'll have to implement this as a QScrollArea, just in case
- * some random text decides to overflow or something.
- */
-Paragraph::Paragraph(const QString& text, QWidget* parent) : QLabel(text, parent) {
-	setTextInteractionFlags(Qt::TextBrowserInteraction);
-	setFrameShape(QFrame::NoFrame);
-	setCursor(Qt::IBeamCursor); /* emulate Taiga */
-	setWordWrap(true);
+Paragraph::Paragraph(const QString& text, QWidget* parent) : QWidget(parent) {
+	/* meh */
+	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	text_edit.setTextInteractionFlags(Qt::TextBrowserInteraction);
+	text_edit.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	text_edit.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	text_edit.setStyleSheet("background: transparent;");
+
+	text_edit.document()->setDocumentMargin(0);
 
-	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+	SetText(text);
+
+	layout->addWidget(&text_edit);
+}
+
+void Paragraph::SetText(const QString& text) {
+	text_edit.document()->setPlainText(text);
+
+	/* return the view to the start */
+	QTextCursor cursor = text_edit.textCursor();
+	cursor.setPosition(0);
+	text_edit.setTextCursor(cursor);
+}
+
+QSize Paragraph::minimumSizeHint() const {
+	return QSize(0, 0);
 }
 
-/* kept here for legacy reasons, see explanation above */
-void Paragraph::SetText(const QString& text) {
-	setText(text);
+/* highly based upon... some stackoverflow answer for PyQt */
+QSize Paragraph::sizeHint() const {
+	QTextDocument* doc = text_edit.document();
+	doc->adjustSize();
+
+	long h = 0;
+	for (QTextBlock line = doc->begin(); line != doc->end(); line = line.next())
+		h += doc->documentLayout()->blockBoundingRect(line).height();
+
+	return QSize(doc->size().width(), h);
+}
+
+QPlainTextEdit* Paragraph::GetLabel() {
+	return &text_edit;
 }
 
-/* Equivalent to Paragraph(), but disables word wrap. */
-Line::Line(QWidget* parent) : Paragraph("", parent) {
-	setWordWrap(false);
+Line::Line(QWidget* parent) : QWidget(parent) {
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	layout->setSpacing(0);
+	layout->setContentsMargins(0, 0, 0, 0);
+
+	line_edit_.setReadOnly(true);
+	line_edit_.setFrame(false);
+	line_edit_.setStyleSheet("background: transparent;");
+
+	layout->addWidget(&line_edit_);
 }
 
-Line::Line(const QString& text, QWidget* parent) : Paragraph(text, parent) {
-	setWordWrap(false);
+Line::Line(const QString& text, QWidget* parent) : Line(parent) {
+	SetText(text);
 }
 
-/* legacy function, don't use in new code */
 void Line::SetText(const QString& text) {
-	setText(text);
+	line_edit_.setText(text);
+	line_edit_.setCursorPosition(0);
 }
 
 Title::Title(const QString& title, QWidget* parent) : Line(title, parent) {
-	QFont fnt(font());
+	QFont fnt(line_edit_.font());
 	fnt.setPixelSize(16);
-	setFont(fnt);
+	line_edit_.setFont(fnt);
 
-	QPalette pal(palette());
-	pal.setColor(QPalette::Text, pal.color(QPalette::Highlight));
-	setPalette(pal);
+	line_edit_.setForegroundRole(QPalette::Highlight);
 }
 
-Section::Section(const QString& title, const QString& data, QWidget* parent) : QWidget(parent) {
+Section::Section(const QString& title, const QString& data, QWidget* parent)
+	: QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
 	header = new Header(title, this);
@@ -90,9 +138,9 @@
 	QHBoxLayout* content_layout = new QHBoxLayout(content);
 
 	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrap(QTextOption::NoWrap);
+	paragraph->GetLabel()->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->GetLabel()->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	paragraph->GetLabel()->setWordWrapMode(QTextOption::NoWrap);
 
 	content_layout->addWidget(paragraph);
 	content_layout->setSpacing(0);
@@ -113,33 +161,32 @@
 	return paragraph;
 }
 
-LabelledParagraph::LabelledParagraph(const QString& label, const QString& data, QWidget* parent) : QWidget(parent) {
+/* despite being named a "labelled paragraph" this uses QLabels for simplicity */
+LabelledParagraph::LabelledParagraph(const QString& label, const QString& data, QWidget* parent)
+	: QWidget(parent)
+	, labels_(label)
+	, data_(data) {
 	QHBoxLayout* ly = new QHBoxLayout(this);
 
-	labels = new Paragraph(label, this);
-	labels->setTextInteractionFlags(Qt::NoTextInteraction);
-	labels->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	labels->setWordWrap(QTextOption::NoWrap);
-	labels->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
+	labels_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+	data_.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
-	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrap(QTextOption::NoWrap);
-	paragraph->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-
-	ly->addWidget(labels, 0, Qt::AlignTop);
-	ly->addWidget(paragraph, 0, Qt::AlignTop);
+	ly->addWidget(&labels_, 0, Qt::AlignTop);
+	ly->addWidget(&data_, 0, Qt::AlignTop);
 	ly->setSpacing(20);
 	ly->setContentsMargins(0, 0, 0, 0);
 }
 
-Paragraph* LabelledParagraph::GetLabels() {
-	return labels;
+QLabel* LabelledParagraph::GetLabels() {
+	return &labels_;
 }
 
-Paragraph* LabelledParagraph::GetParagraph() {
-	return paragraph;
+QLabel* LabelledParagraph::GetData() {
+	return &data_;
+}
+
+QLabel* LabelledParagraph::GetParagraph() {
+	return GetData();
 }
 
 LabelledSection::LabelledSection(const QString& title, const QString& label, const QString& data, QWidget* parent)
@@ -151,7 +198,7 @@
 	// this is not accessible from the object because there's really
 	// no reason to make it accessible...
 	content = new LabelledParagraph(label, data, this);
-	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
+	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 	content->setContentsMargins(12, 0, 0, 0);
 
 	layout->addWidget(header);
@@ -164,11 +211,15 @@
 	return header;
 }
 
-Paragraph* LabelledSection::GetLabels() {
+QLabel* LabelledSection::GetLabels() {
 	return content->GetLabels();
 }
 
-Paragraph* LabelledSection::GetParagraph() {
+QLabel* LabelledSection::GetData() {
+	return content->GetData();
+}
+
+QLabel* LabelledSection::GetParagraph() {
 	return content->GetParagraph();
 }
 
--- a/src/gui/window.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/gui/window.cc	Sun May 12 16:31:07 2024 -0400
@@ -74,21 +74,32 @@
 	action_->setEnabled(true);
 }
 
-MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), async_synchronize_thread_(nullptr, nullptr) {
+MainWindow::MainWindow(QWidget* parent)
+	: QMainWindow(parent)
+	, async_synchronize_thread_(nullptr, nullptr) {
 	setWindowIcon(QIcon(":/icons/favicon.png"));
 
 	sidebar_.setFixedWidth(128);
 	sidebar_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
 
-	connect(&sidebar_, &SideBar::CurrentItemChanged, &stack_, &QStackedWidget::setCurrentIndex);
-
 	new QHBoxLayout(&main_widget_);
 
+	CreateBars();
+
+	stack_.addWidget(&now_playing_page_);
+	/* ---- */
+	stack_.addWidget(&anime_list_page_);
+	stack_.addWidget(&history_page_);
+	stack_.addWidget(&statistics_page_);
+	/* ---- */
+	stack_.addWidget(&search_page_);
+	stack_.addWidget(&seasons_page_);
+	stack_.addWidget(&torrents_page_);
+
 	AddMainWidgets();
+	sidebar_.SetCurrentItem(static_cast<int>(Pages::ANIME_LIST));
 	setCentralWidget(&main_widget_);
 
-	CreateBars();
-
 	NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack_.widget(static_cast<int>(Pages::NOW_PLAYING)));
 
 	connect(&playing_thread_, &MainWindowPlayingThread::Done, this, [page](const std::vector<std::string>& files) {
@@ -123,8 +134,9 @@
 	playing_thread_timer_.start(5000);
 }
 
+/* Does the main part of what Qt's generic "RetranslateUI" function would do */
 void MainWindow::AddMainWidgets() {
-	int page = static_cast<int>(Pages::ANIME_LIST);
+	int page = sidebar_.GetCurrentItem();
 
 	sidebar_.clear();
 
@@ -138,20 +150,6 @@
 	sidebar_.AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
 	sidebar_.AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
 
-	while (stack_.count())
-		stack_.removeWidget(stack_.widget(0));
-
-	/* can we allocate these on the stack? */
-	stack_.addWidget(new NowPlayingPage(&main_widget_));
-	/* ---- */
-	stack_.addWidget(new AnimeListPage(&main_widget_));
-	stack_.addWidget(new HistoryPage(&main_widget_));
-	stack_.addWidget(new StatisticsPage(&main_widget_));
-	/* ---- */
-	stack_.addWidget(new SearchPage(&main_widget_));
-	stack_.addWidget(new SeasonsPage(&main_widget_));
-	stack_.addWidget(new TorrentsPage(&main_widget_));
-
 	sidebar_.SetCurrentItem(page);
 
 	main_widget_.layout()->addWidget(&sidebar_);
@@ -334,6 +332,8 @@
 			}
 
 			/* pain in the ass */
+			disconnect(&sidebar_, &SideBar::CurrentItemChanged, nullptr, nullptr);
+			connect(&sidebar_, &SideBar::CurrentItemChanged, &stack_, &QStackedWidget::setCurrentIndex);
 			connect(&sidebar_, &SideBar::CurrentItemChanged, this, [pages_group](int index) {
 				QAction* checked = pages_group->checkedAction();
 
@@ -508,8 +508,7 @@
 }
 
 void MainWindow::RetranslateUI() {
-	/* This kinda sucks but nobody's really going to be changing
-	   the application language all the time :p */
+	/* This sucks a LOT */
 	setUpdatesEnabled(false);
 	AddMainWidgets();
 	CreateBars();
@@ -532,14 +531,18 @@
 	QMainWindow::showEvent(event);
 #ifdef WIN32
 	/* Technically this *should* be
-	   session.config.theme.IsInDarkTheme() && win32::IsInDarkTheme()
-	   but I prefer the title bar being black even when light mode
-	   is enabled :/ */
+	 * session.config.theme.IsInDarkTheme() && win32::IsInDarkTheme()
+	 * but I prefer the title bar being black even when light mode
+	 * is enabled :/ */
 	win32::SetTitleBarsToBlack(session.config.theme.IsInDarkTheme());
 #endif
 }
 
 void MainWindow::closeEvent(QCloseEvent* event) {
+	playing_thread_timer_.stop();
+	playing_thread_.wait();
+	async_synchronize_thread_.wait();
+
 	session.config.Save();
 	Anime::db.SaveDatabaseToDisk();
 	event->accept();
--- a/src/services/anilist.cc	Wed May 08 17:32:28 2024 -0400
+++ b/src/services/anilist.cc	Sun May 12 16:31:07 2024 -0400
@@ -18,6 +18,7 @@
 
 #include <chrono>
 #include <exception>
+#include <string_view>
 
 #include <iostream>
 
@@ -26,7 +27,7 @@
 namespace Services {
 namespace AniList {
 
-static constexpr int CLIENT_ID = 13706;
+static constexpr std::string_view CLIENT_ID = "13706";
 
 class Account {
 public:
@@ -42,13 +43,13 @@
 
 static Account account;
 
-std::string SendRequest(std::string data) {
+static std::string SendRequest(const std::string& data) {
 	std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json",
 	                                    "Content-Type: application/json"};
-	return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
+	return Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data, HTTP::Type::Post));
 }
 
-nlohmann::json SendJSONRequest(nlohmann::json data) {
+static nlohmann::json SendJSONRequest(const nlohmann::json& data) {
 	std::string request = SendRequest(data.dump());
 	if (request.empty()) {
 		std::cerr << "[AniList] JSON Request returned an empty result!" << std::endl;
@@ -72,7 +73,7 @@
 	return ret;
 }
 
-void ParseListStatus(std::string status, Anime::Anime& anime) {
+static void ParseListStatus(std::string status, Anime::Anime& anime) {
 	static const std::unordered_map<std::string, Anime::ListStatus> map = {
 	    {"CURRENT",   Anime::ListStatus::Current  },
 	    {"PLANNING",  Anime::ListStatus::Planning },
@@ -95,7 +96,7 @@
 	anime.SetUserStatus(map.at(status));
 }
 
-std::string ListStatusToString(const Anime::Anime& anime) {
+static std::string ListStatusToString(const Anime::Anime& anime) {
 	if (anime.GetUserIsRewatching() && anime.GetUserStatus() == Anime::ListStatus::Current)
 		return "REWATCHING";
 
@@ -109,7 +110,7 @@
 	return "CURRENT";
 }
 
-void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
+static void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
 	nlohmann::json::json_pointer g = "/native"_json_pointer;
 	if (json.contains(g) && json[g].is_string())
 		anime.SetTitle(Anime::TitleLanguage::Native, json[g]);
@@ -123,7 +124,7 @@
 		anime.SetTitle(Anime::TitleLanguage::Romaji, json[g]);
 }
 
-int ParseMediaJson(const nlohmann::json& json) {
+static int ParseMediaJson(const nlohmann::json& json) {
 	int id = JSON::GetNumber(json, "/id"_json_pointer);
 	if (!id)
 		return 0;
@@ -159,7 +160,7 @@
 	return id;
 }
 
-int ParseListItem(const nlohmann::json& json) {
+static int ParseListItem(const nlohmann::json& json) {
 	int id = ParseMediaJson(json["media"]);
 
 	Anime::Anime& anime = Anime::db.items[id];
@@ -179,7 +180,7 @@
 	return id;
 }
 
-int ParseList(const nlohmann::json& json) {
+static int ParseList(const nlohmann::json& json) {
 	for (const auto& entry : json["entries"].items()) {
 		ParseListItem(entry.value());
 	}
@@ -336,6 +337,10 @@
 	if (!anime.IsInUserList())
 		return 0;
 
+	std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::AniList);
+	if (!service_id)
+		return 0;
+
 	constexpr std::string_view query =
 	    "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: "
 	    "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
@@ -348,7 +353,7 @@
 	nlohmann::json json = {
 		{"query", query},
 		{"variables", {
-			{"media_id", anime.GetId()},
+			{"media_id", Strings::ToInt<int64_t>(service_id.value())},
 			{"progress", anime.GetUserProgress()},
 			{"status",   ListStatusToString(anime)},
 			{"score",    anime.GetUserScore()},
@@ -365,7 +370,7 @@
 	return JSON::GetNumber(ret, "/data/SaveMediaListEntry/id"_json_pointer, 0);
 }
 
-int ParseUser(const nlohmann::json& json) {
+static int ParseUser(const nlohmann::json& json) {
 	account.SetUserId(JSON::GetNumber(json, "/id"_json_pointer, 0));
 	return account.UserId();
 }
@@ -373,7 +378,7 @@
 bool AuthorizeUser() {
 	/* Prompt for PIN */
 	QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" +
-	                                                  Strings::ToUtf8String(CLIENT_ID) + "&response_type=token")));
+	                                                  std::string(CLIENT_ID) + "&response_type=token")));
 
 	bool ok;
 	QString token = QInputDialog::getText(