changeset 46:d0adc4aedfc8

*: update... this commit: 1. consolidates dark theme stuff to dark_theme.cpp 2. creates a new widgets folder to store all of our custom widgets 3. creates the list settings page in the information dialog, although much of it is nonfunctional: it doesn't save, and the status doesn't even get filled in... we'll fix this later!
author Paper <mrpapersonic@gmail.com>
date Sat, 23 Sep 2023 01:02:15 -0400
parents 4b05bc7668eb
children d8eb763e6661
files CMakeLists.txt include/gui/dark_theme.h include/gui/dialog/information.h include/gui/dialog/settings.h include/gui/sidebar.h include/gui/ui_utils.h include/gui/widgets/sidebar.h include/gui/widgets/text.h include/gui/window.h src/gui/dark_theme.cpp src/gui/dialog/information.cpp src/gui/dialog/settings.cpp src/gui/pages/anime_list.cpp src/gui/pages/now_playing.cpp src/gui/pages/statistics.cpp src/gui/sidebar.cpp src/gui/translate/anilist.cpp src/gui/translate/anime.cpp src/gui/ui_utils.cpp src/gui/widgets/sidebar.cpp src/gui/widgets/text.cpp src/gui/window.cpp src/main.cpp
diffstat 23 files changed, 707 insertions(+), 512 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Sep 22 15:21:55 2023 -0400
+++ b/CMakeLists.txt	Sat Sep 23 01:02:15 2023 -0400
@@ -39,10 +39,18 @@
 	src/core/strings.cpp
 	src/core/time.cpp
 
-	# GUI stuff
+	# Main window
 	src/gui/window.cpp
-	src/gui/sidebar.cpp
-	src/gui/ui_utils.cpp
+	src/gui/dark_theme.cpp
+
+	# Main window pages
+	src/gui/pages/anime_list.cpp
+	src/gui/pages/now_playing.cpp
+	src/gui/pages/statistics.cpp
+
+	# Custom widgets
+	src/gui/widgets/sidebar.cpp
+	src/gui/widgets/text.cpp
 
 	# Dialogs
 	src/gui/dialog/information.cpp
@@ -50,11 +58,6 @@
 	src/gui/dialog/settings/application.cpp
 	src/gui/dialog/settings/services.cpp
 
-	# Main window pages
-	src/gui/pages/anime_list.cpp
-	src/gui/pages/now_playing.cpp
-	src/gui/pages/statistics.cpp
-
 	# Translate
 	src/gui/translate/anime.cpp
 	src/gui/translate/anilist.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/gui/dark_theme.h	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,16 @@
+#ifndef __gui__dark_theme_h
+#define __gui__dark_theme_h
+
+namespace DarkTheme {
+
+bool IsInDarkMode();
+/* this function is private, and should stay that way */
+// void SetStyleSheet(enum Themes theme);
+void SetToDarkTheme();
+void SetToLightTheme();
+enum Themes GetCurrentOSTheme();
+void SetTheme(enum Themes theme);
+
+}
+
+#endif // __gui__dark_theme_h
--- a/include/gui/dialog/information.h	Fri Sep 22 15:21:55 2023 -0400
+++ b/include/gui/dialog/information.h	Sat Sep 23 01:02:15 2023 -0400
@@ -10,6 +10,6 @@
 		Q_OBJECT
 
 	public:
-		InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
+		InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
 };
-#endif // __gui__dialog__information_h
\ No newline at end of file
+#endif // __gui__dialog__information_h
--- a/include/gui/dialog/settings.h	Fri Sep 22 15:21:55 2023 -0400
+++ b/include/gui/dialog/settings.h	Sat Sep 23 01:02:15 2023 -0400
@@ -1,7 +1,7 @@
 #ifndef __gui__dialog__settings_h
 #define __gui__dialog__settings_h
 #include "core/anime.h"
-#include "gui/sidebar.h"
+#include "gui/widgets/sidebar.h"
 #include <QComboBox>
 #include <QDialog>
 #include <QHBoxLayout>
@@ -62,4 +62,4 @@
 		QHBoxLayout* layout;
 		SideBar* sidebar;
 };
-#endif // __gui__dialog__settings_h
\ No newline at end of file
+#endif // __gui__dialog__settings_h
--- a/include/gui/sidebar.h	Fri Sep 22 15:21:55 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-#ifndef __gui__sidebar_h
-#define __gui__sidebar_h
-#include <QItemSelectionModel>
-#include <QListWidget>
-#include <QListWidgetItem>
-class SideBar : public QListWidget {
-		Q_OBJECT
-
-	public:
-		SideBar(QWidget* parent = nullptr);
-		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
-		QListWidgetItem* AddSeparator();
-		bool IndexIsSeparator(QModelIndex index) const;
-		static QIcon CreateIcon(const char* file);
-
-	signals:
-		void CurrentItemChanged(int index);
-
-	protected:
-		virtual void mouseMoveEvent(QMouseEvent* event) override;
-		QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& index,
-		                                                     const QEvent* event) const override;
-		int RemoveSeparatorsFromIndex(int index);
-};
-#endif // __gui__sidebar_h
--- a/include/gui/ui_utils.h	Fri Sep 22 15:21:55 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-#ifndef __gui__ui_utils_h
-#define __gui__ui_utils_h
-#include <QFrame>
-#include <QLabel>
-#include <QPlainTextEdit>
-#include <QSize>
-#include <QString>
-#include <QWidget>
-namespace UiUtils {
-bool IsInDarkMode();
-void SetPlainTextEditData(QPlainTextEdit* text_edit, QString data);
-
-class Header : public QWidget {
-		Q_OBJECT
-
-	public:
-		Header(QString title, QWidget* parent = nullptr);
-		void SetTitle(QString title);
-
-	private:
-		QLabel* static_text_title;
-		QFrame* static_text_line;
-};
-
-class Paragraph : public QPlainTextEdit {
-		Q_OBJECT
-
-	public:
-		Paragraph(QString text, QWidget* parent = nullptr);
-		QSize minimumSizeHint() const override;
-		QSize sizeHint() const override;
-};
-
-/* technically a paragraph and a heading is actually a
-   "section", but that name is equally as confusing as
-   "text paragraph". */
-class TextParagraph : public QWidget {
-		Q_OBJECT
-
-	public:
-		TextParagraph(QString title, QString data, QWidget* parent = nullptr);
-		Header* GetHeader();
-		Paragraph* GetParagraph();
-
-	private:
-		Header* header;
-		Paragraph* paragraph;
-};
-
-class LabelledTextParagraph : public QWidget {
-		Q_OBJECT
-
-	public:
-		LabelledTextParagraph(QString title, QString label, QString data, QWidget* parent = nullptr);
-		Header* GetHeader();
-		Paragraph* GetLabels();
-		Paragraph* GetParagraph();
-
-	private:
-		Header* header;
-		Paragraph* labels;
-		Paragraph* paragraph;
-};
-
-class SelectableTextParagraph : public QWidget {
-		Q_OBJECT
-
-	public:
-		SelectableTextParagraph(QString title, QString data, QWidget* parent = nullptr);
-		Header* GetHeader();
-		Paragraph* GetParagraph();
-
-	private:
-		Header* header;
-		Paragraph* paragraph;
-};
-};     // namespace UiUtils
-#endif // __gui__ui_utils_h
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/gui/widgets/sidebar.h	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,25 @@
+#ifndef __gui__sidebar_h
+#define __gui__sidebar_h
+#include <QItemSelectionModel>
+#include <QListWidget>
+#include <QListWidgetItem>
+class SideBar : public QListWidget {
+		Q_OBJECT
+
+	public:
+		SideBar(QWidget* parent = nullptr);
+		QListWidgetItem* AddItem(QString name, QIcon icon = QIcon());
+		QListWidgetItem* AddSeparator();
+		bool IndexIsSeparator(QModelIndex index) const;
+		static QIcon CreateIcon(const char* file);
+
+	signals:
+		void CurrentItemChanged(int index);
+
+	protected:
+		virtual void mouseMoveEvent(QMouseEvent* event) override;
+		QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& index,
+		                                                     const QEvent* event) const override;
+		int RemoveSeparatorsFromIndex(int index);
+};
+#endif // __gui__sidebar_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/gui/widgets/text.h	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,77 @@
+#ifndef __gui__ui_utils_h
+#define __gui__ui_utils_h
+#include <QFrame>
+#include <QLabel>
+#include <QPlainTextEdit>
+#include <QSize>
+#include <QString>
+#include <QWidget>
+namespace TextWidgets {
+void SetPlainTextEditData(QPlainTextEdit* text_edit, QString data);
+
+class Header : public QWidget {
+		Q_OBJECT
+
+	public:
+		Header(QString title, QWidget* parent = nullptr);
+		void SetTitle(QString title);
+
+	private:
+		QLabel* static_text_title;
+		QFrame* static_text_line;
+};
+
+class Paragraph : public QPlainTextEdit {
+		Q_OBJECT
+
+	public:
+		Paragraph(QString text, QWidget* parent = nullptr);
+		QSize minimumSizeHint() const override;
+		QSize sizeHint() const override;
+};
+
+/* technically a paragraph and a heading is actually a
+   "section", but that name is equally as confusing as
+   "text paragraph". */
+class TextParagraph : public QWidget {
+		Q_OBJECT
+
+	public:
+		TextParagraph(QString title, QString data, QWidget* parent = nullptr);
+		Header* GetHeader();
+		Paragraph* GetParagraph();
+
+	private:
+		Header* header;
+		Paragraph* paragraph;
+};
+
+class LabelledTextParagraph : public QWidget {
+		Q_OBJECT
+
+	public:
+		LabelledTextParagraph(QString title, QString label, QString data, QWidget* parent = nullptr);
+		Header* GetHeader();
+		Paragraph* GetLabels();
+		Paragraph* GetParagraph();
+
+	private:
+		Header* header;
+		Paragraph* labels;
+		Paragraph* paragraph;
+};
+
+class SelectableTextParagraph : public QWidget {
+		Q_OBJECT
+
+	public:
+		SelectableTextParagraph(QString title, QString data, QWidget* parent = nullptr);
+		Header* GetHeader();
+		Paragraph* GetParagraph();
+
+	private:
+		Header* header;
+		Paragraph* paragraph;
+};
+};     // namespace UiUtils
+#endif // __gui__ui_utils_h
--- a/include/gui/window.h	Fri Sep 22 15:21:55 2023 -0400
+++ b/include/gui/window.h	Sat Sep 23 01:02:15 2023 -0400
@@ -11,8 +11,6 @@
 	public:
 		MainWindow(QWidget* parent = nullptr);
 		void SetActivePage(QWidget* page);
-		void SetStyleSheet(enum Themes theme);
-		void ThemeChanged();
 		void closeEvent(QCloseEvent* event) override;
 
 	private:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dark_theme.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,100 @@
+#include <QApplication>
+#include <QFile>
+#include <QTextStream>
+#include "core/config.h"
+#include "core/session.h"
+#ifdef MACOSX
+#	include "sys/osx/dark_theme.h"
+#else
+#	include "sys/win32/dark_theme.h"
+#endif
+
+namespace DarkTheme {
+
+bool IsInDarkMode() {
+	if (session.config.theme != Themes::OS)
+		return (session.config.theme == Themes::DARK);
+#ifdef MACOSX
+	if (osx::DarkThemeAvailable()) {
+		if (osx::IsInDarkTheme()) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+#elif defined(WIN32)
+	if (win32::DarkThemeAvailable()) {
+		if (win32::IsInDarkTheme()) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+#endif
+	return (session.config.theme == Themes::DARK);
+}
+
+/* this function is private, and should stay that way */
+void SetStyleSheet(enum Themes theme) {
+	switch (theme) {
+		case Themes::DARK: {
+			QFile f(":qdarkstyle/dark/darkstyle.qss");
+			if (!f.exists())
+				return; // fail
+			f.open(QFile::ReadOnly | QFile::Text);
+			QTextStream ts(&f);
+			qApp->setStyleSheet(ts.readAll());
+			break;
+		}
+		default: qApp->setStyleSheet(""); break;
+	}
+}
+
+void SetToDarkTheme() {
+	/* macOS >= 10.14 has its own global dark theme,
+	   use it :) */
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		osx::SetToDarkTheme();
+	else
+#endif
+	SetStyleSheet(Themes::DARK);
+}
+
+void SetToLightTheme() {
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		osx::SetToLightTheme();
+	else
+#endif
+	SetStyleSheet(Themes::LIGHT);
+}
+
+enum Themes GetCurrentOSTheme() {
+#if MACOSX
+	if (osx::DarkThemeAvailable())
+		return osx::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
+#elif defined(WIN32)
+	if (win32::DarkThemeAvailable())
+		return win32::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
+#endif
+	/* Currently OS detection only supports Windows and macOS.
+	   Please don't be shy if you're willing to port it to other OSes
+	   (or desktop environments, or window managers) */
+	return Themes::LIGHT;
+}
+
+void SetTheme(enum Themes theme) {
+	switch (theme) {
+		case Themes::LIGHT: SetToLightTheme(); break;
+		case Themes::DARK: SetToDarkTheme(); break;
+		case Themes::OS:
+			if (GetCurrentOSTheme() == Themes::LIGHT)
+				SetToLightTheme();
+			else
+				SetToDarkTheme();
+			break;
+	}
+}
+
+} // namespace DarkTheme
--- a/src/gui/dialog/information.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/dialog/information.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -1,18 +1,26 @@
 #include "gui/dialog/information.h"
 #include "core/anime.h"
+#include "core/array.h"
 #include "core/strings.h"
 #include "gui/pages/anime_list.h"
 #include "gui/translate/anime.h"
-#include "gui/ui_utils.h"
+#include "gui/widgets/text.h"
 #include "gui/window.h"
+#include <QCheckBox>
+#include <QComboBox>
 #include <QDebug>
 #include <QDialogButtonBox>
+#include <QLineEdit>
 #include <QPlainTextEdit>
+#include <QSpinBox>
+#include <QStringList>
 #include <QTextStream>
 #include <QVBoxLayout>
 #include <functional>
 
-InformationDialog::InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
+/* TODO: Taiga disables rendering of the tab widget entirely when the anime is not part of a list,
+   which sucks. Think of a better way to implement this later. */
+InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
     : QDialog(parent) {
 	setFixedSize(842, 613);
 	setWindowTitle(tr("Anime Information"));
@@ -42,8 +50,8 @@
 	main_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 
 	/* anime title header text */
-	UiUtils::Paragraph* anime_title =
-	    new UiUtils::Paragraph(QString::fromUtf8(anime.GetUserPreferredTitle().c_str()), main_widget);
+	TextWidgets::Paragraph* anime_title =
+	    new TextWidgets::Paragraph(QString::fromUtf8(anime.GetUserPreferredTitle().c_str()), main_widget);
 	anime_title->setReadOnly(true);
 	anime_title->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 	anime_title->setWordWrapMode(QTextOption::NoWrap);
@@ -60,7 +68,7 @@
 
 	{
 		QPalette pal;
-		pal.setColor(QPalette::Window, QColor(255, 255, 255, 0));
+		pal.setColor(QPalette::Window, Qt::transparent);
 		pal.setColor(QPalette::WindowText, Qt::blue);
 	}
 
@@ -73,7 +81,7 @@
 	main_information_widget->setLayout(new QVBoxLayout);
 
 	/* alt titles */
-	main_information_widget->layout()->addWidget(new UiUtils::SelectableTextParagraph(
+	main_information_widget->layout()->addWidget(new TextWidgets::SelectableTextParagraph(
 	    "Alternative titles", QString::fromUtf8(Strings::Implode(anime.GetTitleSynonyms(), ", ").c_str()),
 	    main_information_widget));
 
@@ -83,21 +91,176 @@
 	details_data_s << Translate::ToString(anime.GetFormat()).c_str() << "\n"
 	               << anime.GetEpisodes() << "\n"
 	               << Translate::ToString(anime.GetUserStatus()).c_str() << "\n"
-	               << Translate::ToString(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear()
-	               << "\n"
+	               << Translate::ToString(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear() << "\n"
 	               << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
 	               << anime.GetAudienceScore() << "%";
-	main_information_widget->layout()->addWidget(new UiUtils::LabelledTextParagraph(
+	main_information_widget->layout()->addWidget(new TextWidgets::LabelledTextParagraph(
 	    "Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data, main_information_widget));
 
 	/* synopsis */
-	UiUtils::SelectableTextParagraph* synopsis = new UiUtils::SelectableTextParagraph(
+	TextWidgets::SelectableTextParagraph* synopsis = new TextWidgets::SelectableTextParagraph(
 	    "Synopsis", QString::fromUtf8(anime.GetSynopsis().c_str()), main_information_widget);
 
 	synopsis->GetParagraph()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-	((QVBoxLayout*)main_information_widget->layout())->addWidget(synopsis);
+	main_information_widget->layout()->addWidget(synopsis);
 
 	QWidget* settings_widget = new QWidget(tabbed_widget);
+	settings_widget->setLayout(new QVBoxLayout);
+	settings_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
+	settings_widget->layout()->addWidget(new TextWidgets::Header(tr("Anime list"), settings_widget));
+
+	QWidget* sg_anime_list_content = new QWidget(settings_widget);
+	settings_widget->layout()->addWidget(sg_anime_list_content);
+	sg_anime_list_content->setLayout(new QVBoxLayout);
+	sg_anime_list_content->layout()->setSpacing(5);
+	sg_anime_list_content->layout()->setContentsMargins(12, 0, 0, 0);
+
+	/* Note: PLEASE find a way we can consolidate these. By ANY other means than
+	   putting them in a separate function. Macros are very much preferred. */
+#define LAYOUT_HORIZ_SPACING 9
+#define LAYOUT_VERT_SPACING 5
+	{
+		/* Episodes watched section */
+		QWidget* section = new QWidget(sg_anime_list_content);
+		QHBoxLayout* layout = new QHBoxLayout;
+		layout->setSpacing(LAYOUT_HORIZ_SPACING);
+		layout->setMargin(0);
+		section->setLayout(layout);
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr("Episodes watched:"), subsection));
+
+			QSpinBox* spin_box = new QSpinBox(subsection);
+			spin_box->setRange(0, anime.GetEpisodes());
+			spin_box->setSingleStep(1);
+			spin_box->setValue(anime.GetUserProgress());
+			subsection->layout()->addWidget(spin_box);
+
+			layout->addWidget(subsection);
+		}
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr(" "), subsection));
+
+			QCheckBox* rewatched_checkbox = new QCheckBox("Rewatching");
+			subsection->layout()->addWidget(rewatched_checkbox);
+
+			layout->addWidget(subsection);
+		}
+		sg_anime_list_content->layout()->addWidget(section);
+	}
+	{
+		/* Status & score section */
+		QWidget* section = new QWidget(sg_anime_list_content);
+		QHBoxLayout* layout = new QHBoxLayout;
+		layout->setSpacing(LAYOUT_HORIZ_SPACING);
+		layout->setMargin(0);
+		section->setLayout(layout);
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr("Status:"), subsection));
+
+			QStringList string_list;
+			for (unsigned int i = 0; i < ARRAYSIZE(Anime::ListStatuses); i++)
+				string_list.append(QString::fromStdString(Translate::ToString(Anime::ListStatuses[i])));
+
+			QComboBox* combo_box = new QComboBox(subsection);
+			combo_box->addItems(string_list);
+			subsection->layout()->addWidget(combo_box);
+
+			layout->addWidget(subsection);
+		}
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr("Score:"), subsection));
+
+			QSpinBox* spin_box = new QSpinBox(subsection);
+			spin_box->setRange(0, 100);
+			spin_box->setSingleStep(5);
+			spin_box->setValue(anime.GetUserScore());
+			subsection->layout()->addWidget(spin_box);
+
+			layout->addWidget(subsection);
+		}
+		sg_anime_list_content->layout()->addWidget(section);
+	}
+	{
+		/* Notes section */
+		QWidget* section = new QWidget(sg_anime_list_content);
+		QHBoxLayout* layout = new QHBoxLayout;
+		layout->setSpacing(LAYOUT_HORIZ_SPACING);
+		layout->setMargin(0);
+		section->setLayout(layout);
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr("Notes:"), subsection));
+
+			QLineEdit* line_edit = new QLineEdit(QString::fromStdString(anime.GetUserNotes()), subsection);
+			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
+			subsection->layout()->addWidget(line_edit);
+
+			layout->addWidget(subsection);
+		}
+		sg_anime_list_content->layout()->addWidget(section);
+	}
+
+	settings_widget->layout()->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
+
+	QWidget* sg_local_content = new QWidget(settings_widget);
+	settings_widget->layout()->addWidget(sg_local_content);
+	sg_local_content->setLayout(new QVBoxLayout);
+	sg_local_content->layout()->setSpacing(5);
+	sg_local_content->layout()->setContentsMargins(12, 0, 0, 0);
+
+	{
+		/* Alternative titles */
+		QWidget* section = new QWidget(sg_local_content);
+		QHBoxLayout* layout = new QHBoxLayout;
+		layout->setSpacing(LAYOUT_HORIZ_SPACING);
+		layout->setMargin(0);
+		section->setLayout(layout);
+		{
+			QWidget* subsection = new QWidget(section);
+			subsection->setLayout(new QVBoxLayout);
+			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
+			subsection->layout()->setMargin(0);
+
+			subsection->layout()->addWidget(new QLabel(tr("Alternative titles:"), subsection));
+
+			QLineEdit* line_edit = new QLineEdit(QString::fromStdString(anime.GetUserNotes()), subsection);
+			line_edit->setPlaceholderText(tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
+			subsection->layout()->addWidget(line_edit);
+
+			QCheckBox* checkbox = new QCheckBox("Use the first alternative title to search for torrents");
+			subsection->layout()->addWidget(checkbox);
+
+			layout->addWidget(subsection);
+		}
+		sg_local_content->layout()->addWidget(section);
+	}
+
+	static_cast<QBoxLayout*>(settings_widget->layout())->addStretch();
 
 	tabbed_widget->addTab(main_information_widget, "Main information");
 	tabbed_widget->addTab(settings_widget, "My list and settings");
--- a/src/gui/dialog/settings.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/dialog/settings.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -1,6 +1,6 @@
 #include "gui/dialog/settings.h"
-#include "gui/sidebar.h"
-#include "gui/ui_utils.h"
+#include "gui/widgets/sidebar.h"
+#include "gui/widgets/text.h"
 #include <QComboBox>
 #include <QDialogButtonBox>
 #include <QGroupBox>
@@ -23,14 +23,16 @@
 	font.setWeight(QFont::Bold);
 	page_title->setFont(font);
 
-	QPalette pal;
-	pal.setColor(QPalette::WindowText, QColor(0xAB, 0xAB, 0xAB));
-	pal.setColor(QPalette::Window, Qt::white);
+	QPalette pal = page_title->palette();
+	pal.setColor(QPalette::Window, QColor(0xAB, 0xAB, 0xAB));
+	pal.setColor(QPalette::WindowText, Qt::white);
 	page_title->setPalette(pal);
+	page_title->setAutoFillBackground(true);
 
 	page_title->setFixedHeight(23);
 	page_title->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
 	page_title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+
 	tab_widget = new QTabWidget(this);
 	tab_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
 
--- a/src/gui/pages/anime_list.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/pages/anime_list.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -395,6 +395,7 @@
 		t = tab_bar->sizeHint();
 		t.setWidth(width());
 	}
+
 	option->tabBarSize = t;
 
 	QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
--- a/src/gui/pages/now_playing.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/pages/now_playing.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -3,4 +3,4 @@
 NowPlayingWidget::NowPlayingWidget(QWidget* parent) : QWidget(parent) {
 }
 
-#include "gui/pages/moc_now_playing.cpp"
\ No newline at end of file
+#include "gui/pages/moc_now_playing.cpp"
--- a/src/gui/pages/statistics.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/pages/statistics.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -2,7 +2,7 @@
 #include "core/anime_db.h"
 #include "core/session.h"
 #include "gui/pages/anime_list.h"
-#include "gui/ui_utils.h"
+#include "gui/widgets/text.h"
 #include <QString>
 #include <QTextDocument>
 #include <QTextStream>
@@ -18,17 +18,18 @@
 	setFrameShadow(QFrame::Sunken);
 
 	QPalette pal = QPalette();
-	setAutoFillBackground(true);
+	pal.setColor(QPalette::Window, Qt::white);
 	setPalette(pal);
+	setAutoFillBackground(true);
 
-	UiUtils::LabelledTextParagraph* anime_list_paragraph = new UiUtils::LabelledTextParagraph(
+	TextWidgets::LabelledTextParagraph* anime_list_paragraph = new TextWidgets::LabelledTextParagraph(
 	    "Anime list",
 	    "Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", "\n\n\n\n\n",
 	    this);
 	anime_list_data = anime_list_paragraph->GetParagraph();
 
-	UiUtils::LabelledTextParagraph* application_paragraph =
-	    new UiUtils::LabelledTextParagraph("Minori", "Uptime:", "", this);
+	TextWidgets::LabelledTextParagraph* application_paragraph =
+	    new TextWidgets::LabelledTextParagraph("Minori", "Uptime:", "", this);
 	application_data = application_paragraph->GetParagraph();
 
 	layout()->addWidget(anime_list_paragraph);
@@ -101,11 +102,11 @@
 	ts << MinutesToDateString(Anime::db.GetTotalPlannedAmount()).c_str() << '\n';
 	ts << Anime::db.GetAverageScore() << '\n';
 	ts << Anime::db.GetScoreDeviation();
-	UiUtils::SetPlainTextEditData(anime_list_data, string);
+	TextWidgets::SetPlainTextEditData(anime_list_data, string);
 
 	/* Application */
 	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
-	UiUtils::SetPlainTextEditData(application_data, QString(SecondsToDateString(session.uptime() / 1000).c_str()));
+	TextWidgets::SetPlainTextEditData(application_data, QString(SecondsToDateString(session.uptime() / 1000).c_str()));
 }
 
 #include "gui/pages/moc_statistics.cpp"
--- a/src/gui/sidebar.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-#include "gui/sidebar.h"
-#include <QFrame>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QMessageBox>
-#include <QMouseEvent>
-
-SideBar::SideBar(QWidget* parent) : QListWidget(parent) {
-	setFrameShape(QFrame::NoFrame);
-	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setSelectionMode(QAbstractItemView::SingleSelection);
-	setSelectionBehavior(QAbstractItemView::SelectItems);
-	setMouseTracking(true);
-	viewport()->setAutoFillBackground(false);
-
-	QFont font;
-	font.setPointSize(9);
-	setFont(font);
-
-	connect(this, &QListWidget::currentRowChanged, this,
-	        [this](int index) { emit CurrentItemChanged(RemoveSeparatorsFromIndex(index)); });
-}
-
-QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
-	QListWidgetItem* item = new QListWidgetItem(this);
-	item->setText(name);
-	if (!icon.isNull())
-		item->setIcon(icon);
-	return item;
-}
-
-QIcon SideBar::CreateIcon(const char* file) {
-	QPixmap pixmap(file, "PNG");
-	QIcon result;
-	result.addPixmap(pixmap, QIcon::Normal);
-	result.addPixmap(pixmap, QIcon::Selected);
-	return result;
-}
-
-QListWidgetItem* SideBar::AddSeparator() {
-	QListWidgetItem* item = new QListWidgetItem(this);
-	QFrame* line = new QFrame(this);
-	line->setFrameShape(QFrame::HLine);
-	line->setFrameShadow(QFrame::Sunken);
-	line->setMouseTracking(true);
-	line->setEnabled(false);
-
-	QPalette pal;
-	pal.setColor(QPalette::Window, QColor(0, 0, 0, 0));
-	line->setPalette(pal);
-
-	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*) const {
-	if (IndexIsSeparator(index))
-		return QItemSelectionModel::NoUpdate;
-	return QItemSelectionModel::ClearAndSelect;
-}
-
-void SideBar::mouseMoveEvent(QMouseEvent* event) {
-	if (!IndexIsSeparator(indexAt(event->pos())))
-		setCursor(Qt::PointingHandCursor);
-	else
-		unsetCursor();
-	QListView::mouseMoveEvent(event);
-}
-
-#include "gui/moc_sidebar.cpp"
--- a/src/gui/translate/anilist.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/translate/anilist.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -48,4 +48,4 @@
 	return map[format];
 }
 
-} // namespace Translate::AniList
\ No newline at end of file
+} // namespace Translate::AniList
--- a/src/gui/translate/anime.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/translate/anime.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -52,4 +52,4 @@
 	}
 }
 
-} // namespace Translate
\ No newline at end of file
+} // namespace Translate
--- a/src/gui/ui_utils.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,216 +0,0 @@
-/**
- * FIXME: most of these can actually be rerouted to *separate* files.
- * Please do this! It makes everything cleaner :)
- **/
-#include "gui/ui_utils.h"
-#include "core/session.h"
-#include <QFrame>
-#include <QLabel>
-#include <QPixmap>
-#include <QTextBlock>
-#include <QVBoxLayout>
-#ifdef MACOSX
-#	include "sys/osx/dark_theme.h"
-#else
-#	include "sys/win32/dark_theme.h"
-#endif
-
-namespace UiUtils {
-
-bool IsInDarkMode() {
-	if (session.config.theme != Themes::OS)
-		return (session.config.theme == Themes::DARK);
-#ifdef MACOSX
-	if (osx::DarkThemeAvailable()) {
-		if (osx::IsInDarkTheme()) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-#elif defined(WIN32)
-	if (win32::DarkThemeAvailable()) {
-		if (win32::IsInDarkTheme()) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-#endif
-	return (session.config.theme == Themes::DARK);
-}
-
-Header::Header(QString title, QWidget* parent) : QWidget(parent) {
-	setLayout(new QVBoxLayout);
-	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-
-	static_text_title = new QLabel(title, this);
-	static_text_title->setTextFormat(Qt::PlainText);
-	QFont font = static_text_title->font();
-	font.setWeight(QFont::Bold);
-	static_text_title->setFont(font);
-	static_text_title->setFixedHeight(16);
-
-	static_text_line = new QFrame(this);
-	static_text_line->setFrameShape(QFrame::HLine);
-	static_text_line->setFrameShadow(QFrame::Sunken);
-	static_text_line->setFixedHeight(2);
-
-	layout()->addWidget(static_text_title);
-	layout()->addWidget(static_text_line);
-	layout()->setSpacing(0);
-	layout()->setMargin(0);
-}
-
-void Header::SetTitle(QString title) {
-	static_text_title->setText(title);
-}
-
-TextParagraph::TextParagraph(QString title, QString data, QWidget* parent) : QWidget(parent) {
-	setLayout(new QVBoxLayout);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	content->setLayout(new QHBoxLayout);
-
-	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrapMode(QTextOption::NoWrap);
-
-	content->layout()->addWidget(paragraph);
-	content->layout()->setSpacing(0);
-	content->layout()->setMargin(0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout()->addWidget(header);
-	layout()->addWidget(paragraph);
-	layout()->setSpacing(0);
-	layout()->setMargin(0);
-}
-
-Header* TextParagraph::GetHeader() {
-	return header;
-}
-
-Paragraph* TextParagraph::GetParagraph() {
-	return paragraph;
-}
-
-LabelledTextParagraph::LabelledTextParagraph(QString title, QString label, QString data, QWidget* parent)
-    : QWidget(parent) {
-	setLayout(new QVBoxLayout);
-
-	header = new Header(title, this);
-
-	// this is not accessible from the object because there's really
-	// no reason to make it accessible...
-	QWidget* content = new QWidget(this);
-	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
-
-	labels = new Paragraph(label, this);
-	labels->setTextInteractionFlags(Qt::NoTextInteraction);
-	labels->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	labels->setWordWrapMode(QTextOption::NoWrap);
-	labels->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
-	labels->setFixedWidth(123);
-
-	paragraph = new Paragraph(data, this);
-	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->setWordWrapMode(QTextOption::NoWrap);
-
-	QHBoxLayout* content_layout = new QHBoxLayout;
-	content_layout->addWidget(labels, 0, Qt::AlignTop);
-	content_layout->addWidget(paragraph, 0, Qt::AlignTop);
-	content_layout->setSpacing(0);
-	content_layout->setMargin(0);
-	content->setLayout(content_layout);
-
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout()->addWidget(header);
-	layout()->addWidget(content);
-	layout()->setSpacing(0);
-	layout()->setMargin(0);
-}
-
-Header* LabelledTextParagraph::GetHeader() {
-	return header;
-}
-
-Paragraph* LabelledTextParagraph::GetLabels() {
-	return labels;
-}
-
-Paragraph* LabelledTextParagraph::GetParagraph() {
-	return paragraph;
-}
-
-SelectableTextParagraph::SelectableTextParagraph(QString title, QString data, QWidget* parent) : QWidget(parent) {
-	setLayout(new QVBoxLayout);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	content->setLayout(new QHBoxLayout);
-
-	paragraph = new Paragraph(data, content);
-
-	content->layout()->addWidget(paragraph);
-	content->layout()->setSpacing(0);
-	content->layout()->setMargin(0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout()->addWidget(header);
-	layout()->addWidget(content);
-	layout()->setSpacing(0);
-	layout()->setMargin(0);
-}
-
-Header* SelectableTextParagraph::GetHeader() {
-	return header;
-}
-
-Paragraph* SelectableTextParagraph::GetParagraph() {
-	return paragraph;
-}
-
-void SetPlainTextEditData(QPlainTextEdit* text_edit, QString data) {
-	QTextDocument* document = new QTextDocument(text_edit);
-	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
-	document->setPlainText(data);
-	text_edit->setDocument(document);
-}
-
-/* inherits QPlainTextEdit and gives a much more reasonable minimum size */
-Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
-	setReadOnly(true);
-	setTextInteractionFlags(Qt::TextBrowserInteraction);
-	setFrameShape(QFrame::NoFrame);
-	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-	QPalette pal;
-	pal.setColor(QPalette::Window, QColor(0, 0, 0, 0));
-	setPalette(pal);
-
-	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
-}
-
-/* highly based upon... some stackoverflow answer for PyQt */
-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(), h);
-}
-
-QSize Paragraph::sizeHint() const {
-	return minimumSizeHint();
-}
-
-} // namespace UiUtils
-
-#include "gui/moc_ui_utils.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/sidebar.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,89 @@
+#include "gui/widgets/sidebar.h"
+#include <QFrame>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QMessageBox>
+#include <QMouseEvent>
+
+SideBar::SideBar(QWidget* parent) : QListWidget(parent) {
+	setFrameShape(QFrame::NoFrame);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setSelectionMode(QAbstractItemView::SingleSelection);
+	setSelectionBehavior(QAbstractItemView::SelectItems);
+	setMouseTracking(true);
+	/* FIXME: is there an easy way to do this with palettes? */
+	setStyleSheet("QListWidget::item:disabled { background: transparent }");
+	viewport()->setAutoFillBackground(false);
+
+	QFont font;
+	font.setPointSize(9);
+	setFont(font);
+
+	connect(this, &QListWidget::currentRowChanged, this,
+	        [this](int index) { emit CurrentItemChanged(RemoveSeparatorsFromIndex(index)); });
+}
+
+QListWidgetItem* SideBar::AddItem(QString name, QIcon icon) {
+	QListWidgetItem* item = new QListWidgetItem(this);
+	item->setText(name);
+	if (!icon.isNull())
+		item->setIcon(icon);
+	return item;
+}
+
+QIcon SideBar::CreateIcon(const char* file) {
+	QPixmap pixmap(file, "PNG");
+	QIcon result;
+	result.addPixmap(pixmap, QIcon::Normal);
+	result.addPixmap(pixmap, QIcon::Selected);
+	return result;
+}
+
+QListWidgetItem* SideBar::AddSeparator() {
+	QListWidgetItem* item = new QListWidgetItem(this);
+	QFrame* line = new QFrame(this);
+	line->setFrameShape(QFrame::HLine);
+	line->setFrameShadow(QFrame::Sunken);
+	line->setMouseTracking(true);
+	line->setEnabled(false);
+
+	/*
+	QPalette pal;
+	pal.setColor(QPalette::Window, Qt::transparent);
+	line->setPalette(pal);
+	*/
+
+	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*) const {
+	if (IndexIsSeparator(index))
+		return QItemSelectionModel::NoUpdate;
+	return QItemSelectionModel::ClearAndSelect;
+}
+
+void SideBar::mouseMoveEvent(QMouseEvent* event) {
+	if (!IndexIsSeparator(indexAt(event->pos())))
+		setCursor(Qt::PointingHandCursor);
+	else
+		unsetCursor();
+	QListView::mouseMoveEvent(event);
+}
+
+#include "gui/widgets/moc_sidebar.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/text.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -0,0 +1,185 @@
+#include "gui/widgets/text.h"
+#include "core/session.h"
+#include <QFrame>
+#include <QLabel>
+#include <QPixmap>
+#include <QTextBlock>
+#include <QVBoxLayout>
+
+namespace TextWidgets {
+
+Header::Header(QString title, QWidget* parent) : QWidget(parent) {
+	setLayout(new QVBoxLayout);
+	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+
+	static_text_title = new QLabel(title, this);
+	static_text_title->setTextFormat(Qt::PlainText);
+	QFont font = static_text_title->font();
+	font.setWeight(QFont::Bold);
+	static_text_title->setFont(font);
+	static_text_title->setFixedHeight(16);
+
+	static_text_line = new QFrame(this);
+	static_text_line->setFrameShape(QFrame::HLine);
+	static_text_line->setFrameShadow(QFrame::Sunken);
+	static_text_line->setFixedHeight(2);
+
+	layout()->addWidget(static_text_title);
+	layout()->addWidget(static_text_line);
+	layout()->setSpacing(0);
+	layout()->setMargin(0);
+}
+
+void Header::SetTitle(QString title) {
+	static_text_title->setText(title);
+}
+
+TextParagraph::TextParagraph(QString title, QString data, QWidget* parent) : QWidget(parent) {
+	setLayout(new QVBoxLayout);
+
+	header = new Header(title, this);
+
+	QWidget* content = new QWidget(this);
+	content->setLayout(new QHBoxLayout);
+
+	paragraph = new Paragraph(data, this);
+	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	paragraph->setWordWrapMode(QTextOption::NoWrap);
+
+	content->layout()->addWidget(paragraph);
+	content->layout()->setSpacing(0);
+	content->layout()->setMargin(0);
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout()->addWidget(header);
+	layout()->addWidget(paragraph);
+	layout()->setSpacing(0);
+	layout()->setMargin(0);
+}
+
+Header* TextParagraph::GetHeader() {
+	return header;
+}
+
+Paragraph* TextParagraph::GetParagraph() {
+	return paragraph;
+}
+
+LabelledTextParagraph::LabelledTextParagraph(QString title, QString label, QString data, QWidget* parent)
+    : QWidget(parent) {
+	setLayout(new QVBoxLayout);
+
+	header = new Header(title, this);
+
+	// this is not accessible from the object because there's really
+	// no reason to make it accessible...
+	QWidget* content = new QWidget(this);
+	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+
+	labels = new Paragraph(label, this);
+	labels->setTextInteractionFlags(Qt::NoTextInteraction);
+	labels->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	labels->setWordWrapMode(QTextOption::NoWrap);
+	labels->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+	labels->setFixedWidth(123);
+
+	paragraph = new Paragraph(data, this);
+	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
+	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
+	paragraph->setWordWrapMode(QTextOption::NoWrap);
+
+	QHBoxLayout* content_layout = new QHBoxLayout;
+	content_layout->addWidget(labels, 0, Qt::AlignTop);
+	content_layout->addWidget(paragraph, 0, Qt::AlignTop);
+	content_layout->setSpacing(0);
+	content_layout->setMargin(0);
+	content->setLayout(content_layout);
+
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout()->addWidget(header);
+	layout()->addWidget(content);
+	layout()->setSpacing(0);
+	layout()->setMargin(0);
+}
+
+Header* LabelledTextParagraph::GetHeader() {
+	return header;
+}
+
+Paragraph* LabelledTextParagraph::GetLabels() {
+	return labels;
+}
+
+Paragraph* LabelledTextParagraph::GetParagraph() {
+	return paragraph;
+}
+
+SelectableTextParagraph::SelectableTextParagraph(QString title, QString data, QWidget* parent) : QWidget(parent) {
+	setLayout(new QVBoxLayout);
+
+	header = new Header(title, this);
+
+	QWidget* content = new QWidget(this);
+	content->setLayout(new QHBoxLayout);
+
+	paragraph = new Paragraph(data, content);
+
+	content->layout()->addWidget(paragraph);
+	content->layout()->setSpacing(0);
+	content->layout()->setMargin(0);
+	content->setContentsMargins(12, 0, 0, 0);
+
+	layout()->addWidget(header);
+	layout()->addWidget(content);
+	layout()->setSpacing(0);
+	layout()->setMargin(0);
+}
+
+Header* SelectableTextParagraph::GetHeader() {
+	return header;
+}
+
+Paragraph* SelectableTextParagraph::GetParagraph() {
+	return paragraph;
+}
+
+/* inherits QPlainTextEdit and gives a much more reasonable minimum size */
+Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
+	setReadOnly(true);
+	setTextInteractionFlags(Qt::TextBrowserInteraction);
+	setFrameShape(QFrame::NoFrame);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+	QPalette pal;
+	pal.setColor(QPalette::Window, QColor(0, 0, 0, 0));
+	setPalette(pal);
+
+	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+}
+
+/* highly based upon... some stackoverflow answer for PyQt */
+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(), h);
+}
+
+QSize Paragraph::sizeHint() const {
+	return minimumSizeHint();
+}
+
+/* this is still actually useful, so we'll keep it */
+void SetPlainTextEditData(QPlainTextEdit* text_edit, QString data) {
+	QTextDocument* document = new QTextDocument(text_edit);
+	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
+	document->setPlainText(data);
+	text_edit->setDocument(document);
+}
+
+} // namespace TextWidgets
+
+#include "gui/widgets/moc_text.cpp"
--- a/src/gui/window.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/gui/window.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -1,12 +1,12 @@
 #include "gui/window.h"
 #include "core/config.h"
 #include "core/session.h"
+#include "gui/dark_theme.h"
 #include "gui/dialog/settings.h"
 #include "gui/pages/anime_list.h"
 #include "gui/pages/now_playing.h"
 #include "gui/pages/statistics.h"
-#include "gui/sidebar.h"
-#include "gui/ui_utils.h"
+#include "gui/widgets/sidebar.h"
 #include "services/services.h"
 #include <QApplication>
 #include <QFile>
@@ -17,7 +17,7 @@
 #include <QTextStream>
 #if MACOSX
 #	include "sys/osx/dark_theme.h"
-#elif WIN32
+#elif defined(WIN32)
 #	include "sys/win32/dark_theme.h"
 #endif
 
@@ -157,9 +157,7 @@
 	});
 
 	menu = menubar->addMenu("&Help");
-	action = menu->addAction("About &Qt", qApp, [this]{
-		qApp->aboutQt();
-	});
+	action = menu->addAction("About &Qt", qApp, [this] { qApp->aboutQt(); });
 	action->setMenuRole(QAction::AboutQtRole);
 
 	setMenuBar(menubar);
@@ -169,67 +167,7 @@
 	layout->addWidget(stack);
 	setCentralWidget(main_widget);
 
-	ThemeChanged();
-}
-
-void MainWindow::SetStyleSheet(enum Themes theme) {
-	switch (theme) {
-		case Themes::DARK: {
-			QFile f(":qdarkstyle/dark/darkstyle.qss");
-			if (!f.exists())
-				return; // fail
-			f.open(QFile::ReadOnly | QFile::Text);
-			QTextStream ts(&f);
-			setStyleSheet(ts.readAll());
-			break;
-		}
-		default: setStyleSheet(""); break;
-	}
-}
-
-void MainWindow::ThemeChanged() {
-	switch (session.config.theme) {
-		case Themes::LIGHT: {
-#if MACOSX
-			if (osx::DarkThemeAvailable())
-				osx::SetToLightTheme();
-			else
-#else
-			SetStyleSheet(Themes::LIGHT);
-#endif
-				break;
-		}
-		case Themes::DARK: {
-#if MACOSX
-			if (osx::DarkThemeAvailable())
-				osx::SetToDarkTheme();
-			else
-#else
-			SetStyleSheet(Themes::DARK);
-#endif
-				break;
-		}
-		case Themes::OS: {
-#if MACOSX
-			if (osx::DarkThemeAvailable())
-				osx::SetToAutoTheme();
-			else
-#elif defined(WIN32)
-			if (win32::DarkThemeAvailable()) {
-				if (win32::IsInDarkTheme()) {
-					SetStyleSheet(Themes::DARK);
-				} else {
-					SetStyleSheet(Themes::LIGHT);
-				}
-			} else
-#endif
-				/* Currently OS detection only supports Windows and macOS.
-				   Please don't be shy if you're willing to port it to other OSes
-				   (or desktop environments, or window managers) */
-				SetStyleSheet(Themes::LIGHT);
-			break;
-		}
-	}
+	DarkTheme::SetTheme(session.config.theme);
 }
 
 void MainWindow::SetActivePage(QWidget* page) {
--- a/src/main.cpp	Fri Sep 22 15:21:55 2023 -0400
+++ b/src/main.cpp	Sat Sep 23 01:02:15 2023 -0400
@@ -1,6 +1,7 @@
 #include "core/session.h"
 #include "gui/window.h"
 #include <QApplication>
+#include <QStyleFactory>
 
 Session session;
 
@@ -16,4 +17,4 @@
 	window.show();
 
 	return app.exec();
-}
\ No newline at end of file
+}