changeset 348:6b0768158dcd

text: redesign almost every widget i.e. Paragraph is now a QLabel, etc etc, some things will probably break, idc
author Paper <paper@paper.us.eu.org>
date Tue, 25 Jun 2024 11:19:54 -0400
parents a0aa8c8c4307
children 7e97c566cce4 daa03aa2262d
files include/core/anime_season.h include/core/strings.h include/gui/pages/now_playing.h include/gui/pages/seasons.h include/gui/pages/statistics.h include/gui/widgets/anime_button.h include/gui/widgets/elided_label.h include/gui/widgets/poster.h include/gui/widgets/text.h src/core/anime_season.cc src/gui/dialog/information.cc src/gui/pages/now_playing.cc src/gui/pages/seasons.cc src/gui/pages/statistics.cc src/gui/widgets/anime_button.cc src/gui/widgets/anime_info.cc src/gui/widgets/elided_label.cc src/gui/widgets/poster.cc src/gui/widgets/text.cc
diffstat 19 files changed, 458 insertions(+), 560 deletions(-) [+]
line wrap: on
line diff
--- a/include/core/anime_season.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/core/anime_season.h	Tue Jun 25 11:19:54 2024 -0400
@@ -36,6 +36,9 @@
 	bool operator<=(const Season& o) const;
 	bool operator>=(const Season& o) const;
 
+	Season& operator++();
+	Season& operator--();
+
 	Name season = Name::Unknown;
 	Date::Year year = 0;
 };
--- a/include/core/strings.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/core/strings.h	Tue Jun 25 11:19:54 2024 -0400
@@ -53,10 +53,10 @@
 	return def;
 }
 
-template<typename T, std::enable_if_t<std::is_integral<T>::value && !std::is_same<T, bool>::value, bool> = true>
+template<typename T, std::enable_if_t<std::is_arithmetic<T>::value && !std::is_same<T, bool>::value, bool> = true>
 std::string ToUtf8String(T i) {
 	std::ostringstream s;
-	s << i;
+	s << std::noboolalpha << i;
 	return s.str();
 }
 
--- a/include/gui/pages/now_playing.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/pages/now_playing.h	Tue Jun 25 11:19:54 2024 -0400
@@ -3,13 +3,15 @@
 
 #include "gui/widgets/anime_info.h"
 #include "gui/widgets/poster.h"
+#include "gui/widgets/sidebar.h"
 #include "gui/widgets/text.h"
 
 #include <QFrame>
+#include <QStackedWidget>
+
 #include <memory>
 #include <unordered_map>
 
-class QStackedWidget;
 
 namespace Anime {
 class Anime;
@@ -19,6 +21,9 @@
 class Elements;
 }
 
+/* -------------------------------------------------------------- */
+/* separate pages */
+
 namespace NowPlayingPages {
 
 class Default : public QWidget {
@@ -26,6 +31,9 @@
 
 public:
 	Default(QWidget* parent = nullptr);
+
+private:
+	TextWidgets::Title title_;
 };
 
 class Playing : public QWidget {
@@ -39,15 +47,20 @@
 private:
 	int _id = 0;
 	int _episode = 0;
-	std::unique_ptr<QWidget> _main = nullptr;
-	std::unique_ptr<TextWidgets::Title> _title = nullptr;
-	std::unique_ptr<AnimeInfoWidget> _info = nullptr;
-	std::unique_ptr<QWidget> _sidebar = nullptr;
-	std::unique_ptr<Poster> _poster = nullptr;
+
+	QWidget _main;
+	TextWidgets::Title _title;
+	AnimeInfoWidget _info;
+
+	QWidget _sidebar;
+	Poster _poster;
 };
 
 } // namespace NowPlayingPages
 
+/* -------------------------------------------------------------- */
+/* the full page */
+
 class NowPlayingPage final : public QFrame {
 	Q_OBJECT
 
@@ -58,7 +71,15 @@
 	int GetPlayingId();
 
 private:
-	QStackedWidget* stack;
+	enum class Subpages {
+		Default = 0,
+		Playing = 1,
+	};
+
+	QStackedWidget stack_;
+
+	NowPlayingPages::Default default_;
+	NowPlayingPages::Playing playing_;
 };
 
 #endif // MINORI_GUI_PAGES_NOW_PLAYING_H_
--- a/include/gui/pages/seasons.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/pages/seasons.h	Tue Jun 25 11:19:54 2024 -0400
@@ -3,6 +3,7 @@
 
 #include <QFrame>
 #include <QThread>
+#include <QToolButton>
 
 #include "core/anime.h"
 #include "core/date.h"
@@ -43,7 +44,7 @@
 
 protected:
 	QListWidget* buttons = nullptr;
-	QToolButton* season_button = nullptr;
+	QToolButton season_button;
 
 	Anime::Season season_;
 };
--- a/include/gui/pages/statistics.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/pages/statistics.h	Tue Jun 25 11:19:54 2024 -0400
@@ -4,12 +4,8 @@
 #include <QFrame>
 #include <QWidget>
 
-template<typename T>
-class Graph;
-
-namespace TextWidgets {
-class LabelledSection;
-}
+#include "gui/widgets/graph.h"
+#include "gui/widgets/text.h"
 
 class StatisticsPage final : public QFrame {
 	Q_OBJECT
@@ -22,9 +18,10 @@
 	void showEvent(QShowEvent*) override;
 
 private:
-	std::shared_ptr<TextWidgets::LabelledSection> _anime_list;
-	std::shared_ptr<Graph<int>> _score_distribution_graph;
-	std::shared_ptr<TextWidgets::LabelledSection> _application;
+	TextWidgets::LabelledSection _anime_list;
+	TextWidgets::LabelledSection _application;
+
+	Graph<int> _score_distribution_graph;
 };
 
 #endif // MINORI_GUI_PAGES_STATISTICS_H_
\ No newline at end of file
--- a/include/gui/widgets/anime_button.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/widgets/anime_button.h	Tue Jun 25 11:19:54 2024 -0400
@@ -14,6 +14,8 @@
 }
 
 class AnimeButton : public QFrame {
+	Q_OBJECT
+
 public:
 	AnimeButton(QWidget* parent = nullptr);
 	AnimeButton(const Anime::Anime& anime, QWidget* parent = nullptr);
@@ -23,7 +25,7 @@
 	Poster _poster;
 	QLabel _title;
 	TextWidgets::LabelledParagraph _info;
-	ElidedLabel _synopsis;
+	TextWidgets::Paragraph _synopsis;
 };
 
 #endif // MINORI_GUI_WIDGETS_ANIME_BUTTON_H_
--- a/include/gui/widgets/elided_label.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/widgets/elided_label.h	Tue Jun 25 11:19:54 2024 -0400
@@ -7,15 +7,4 @@
 class QPaintEvent;
 class QWidget;
 
-class ElidedLabel : public QFrame {
-public:
-	ElidedLabel(const QString& text, QWidget* parent = nullptr);
-	void SetText(const QString& text);
-
-protected:
-	QString content;
-
-	void paintEvent(QPaintEvent* event) override;
-};
-
 #endif // MINORI_GUI_WIDGETS_ELIDED_LABEL_H_
--- a/include/gui/widgets/poster.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/widgets/poster.h	Tue Jun 25 11:19:54 2024 -0400
@@ -20,6 +20,9 @@
 	void SetAnime(const Anime::Anime& anime);
 	void SetClickable(bool clickable);
 
+	bool hasHeightForWidth(void) const override;
+	int heightForWidth(int w) const override;
+
 protected:
 	void showEvent(QShowEvent*) override;
 	void resizeEvent(QResizeEvent*) override;
@@ -27,6 +30,9 @@
 	void RenderToLabel();
 	void DownloadPoster();
 
+	QSize sizeHint() const override;
+	QSize minimumSizeHint() const override;
+
 private:
 	/* stored as a pointer to prevent blocking */
 	HTTP::RequestThread* get_thread_;
--- a/include/gui/widgets/text.h	Sun Jun 23 10:32:09 2024 -0400
+++ b/include/gui/widgets/text.h	Tue Jun 25 11:19:54 2024 -0400
@@ -4,131 +4,136 @@
 #include <QLineEdit>
 #include <QPlainTextEdit>
 #include <QSize>
-#include <QString>
 #include <QWidget>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QPointer>
 
-class QFrame;
-
-#include <QLabel>
+#include <memory>
+#include <unordered_map>
 
 namespace TextWidgets {
 
+/* These used to have getter methods to get the real Qt widgets;
+ * don't make those anymore. Instead, add new methods that are
+ * wrappers around the Qt methods instead. */
+
 class Header : public QWidget {
 	Q_OBJECT
 
 public:
-	Header(const QString& title, QWidget* parent = nullptr);
-	void SetText(const QString& title);
+	Header(QWidget* parent = nullptr);
+	void SetText(const std::string& title);
 
-private:
-	QLabel static_text_title;
-	QFrame static_text_line;
+protected:
+	QPointer<QLabel> title_;
+	QPointer<QFrame> separator_;
 };
 
+/* This is a nice clean wrapper around Label suitable for our needs. */
 class Paragraph : public QWidget {
 	Q_OBJECT
 
 public:
-	Paragraph(const QString& text, QWidget* parent = nullptr);
-	void SetText(const QString& text);
-	QPlainTextEdit* GetLabel();
+	Paragraph(QWidget* parent = nullptr);
+	void SetText(const std::string& text);
+	void SetSelectable(bool enable);
+	void SetWordWrap(bool enable);
+	void SetElidingMode(bool enable);
 
 protected:
-	bool hasHeightForWidth() const override;
-	int heightForWidth(int w) const override;
-
-private:
-	QPlainTextEdit text_edit;
+	QPointer<QLabel> label_;
 };
 
+/* This aligns data with labels */
 class LabelledParagraph final : public QWidget {
 	Q_OBJECT
 
 public:
-	LabelledParagraph(const QString& label, const QString& data, QWidget* parent = nullptr);
-	QLabel* GetLabels();
-	QLabel* GetData();
+	enum Style {
+		BoldedLabels = (1 << 1),
+		ElidedData = (1 << 2), /* does nothing for now */
+	};
+
+	LabelledParagraph(QWidget* parent = nullptr);
+	~LabelledParagraph();
+	void SetData(const std::vector<std::pair<std::string, std::string>>& data);
+	void SetStyle(int style); /* bit-flags from Style enum */
+	void Clear(void);
+
+protected:
+	QPointer<QWidget> contents_;
+	QPointer<QGridLayout> contents_layout_;
 
-	/* synonymous with GetData(), kept for compatibility. don't use in new code!!! */
-	QLabel* GetParagraph();
+	std::vector<std::pair<QSharedPointer<QLabel>, QSharedPointer<QLabel>>> data_;
+};
 
-private:
-	QLabel labels_;
-	QLabel data_;
+/* this is just a generic QLabel with a specific font and foreground role,
+ * which is why it's defined inline */
+class Title final : public Paragraph {
+public:
+	Title(QWidget* parent = nullptr) : Paragraph(parent) {
+		QFont fnt(label_->font());
+		fnt.setPixelSize(16);
+		label_->setFont(fnt);
+
+		label_->setForegroundRole(QPalette::Highlight);
+	}
 };
 
-class Line : public QWidget {
-	Q_OBJECT
+/* ----------------------------------------------------------------------- */
+/* Generic "Section" widget */
 
+template<typename T>
+class Section final : public QWidget {
 public:
-	Line(QWidget* parent = nullptr);
-	Line(const QString& text, QWidget* parent = nullptr);
-	void SetText(const QString& text);
+	Section(QWidget* parent = nullptr) : QWidget(parent) {
+		header_ = new Header(this);
 
-protected:
-	QLineEdit line_edit_;
-};
+		content_container_ = new QWidget(this);
+		content_ = new T(content_container_);
+
+		QVBoxLayout* content_layout = new QVBoxLayout(content_container_);
 
-class Title final : public Line {
-	Q_OBJECT
+		content_layout->addWidget(content_);
+		content_layout->setSpacing(0);
+		content_layout->setContentsMargins(12, 0, 0, 0);
 
-public:
-	Title(const QString& title, QWidget* parent = nullptr);
-};
+		content_container_->setContentsMargins(0, 0, 0, 0);
+
+		content_container_->setLayout(content_layout);
+
+		QVBoxLayout* layout = new QVBoxLayout(this);
 
-class Section final : public QWidget {
-	Q_OBJECT
+		layout->addWidget(header_);
+		layout->addWidget(content_container_);
+		layout->setSpacing(0);
+		layout->setContentsMargins(0, 0, 0, 0);
 
-public:
-	Section(const QString& title, const QString& data, QWidget* parent = nullptr);
-	Header* GetHeader();
-	Paragraph* GetParagraph();
+		setLayout(layout);
+	}
+
+	Header& GetHeader() { return *header_; }
+	T& GetContent() { return *content_; }
 
 private:
-	Header* header;
-	Paragraph* paragraph;
-};
-
-class LabelledSection final : public QWidget {
-	Q_OBJECT
+	Header* header_;
+	T* content_;
 
-public:
-	LabelledSection(const QString& title, const QString& label, const QString& data, QWidget* parent = nullptr);
-	Header* GetHeader();
-	QLabel* GetLabels();
-	QLabel* GetData();
-	QLabel* GetParagraph();
-
-private:
-	Header* header;
-	LabelledParagraph* content;
+	/* I don't think making a separate container
+	 * widget is necessary anymore, but I'm paranoid */
+	QWidget* content_container_;
 };
 
-class SelectableSection final : public QWidget {
-	Q_OBJECT
-
-public:
-	SelectableSection(const QString& title, const QString& data, QWidget* parent = nullptr);
-	Header* GetHeader();
-	Paragraph* GetParagraph();
-
-private:
-	Header* header;
-	Paragraph* paragraph;
-};
-
-class OneLineSection final : public QWidget {
-	Q_OBJECT
-
-public:
-	OneLineSection(const QString& title, const QString& data, QWidget* parent = nullptr);
-	Header* GetHeader();
-	Line* GetLine();
-
-private:
-	Header* header;
-	Line* line;
-};
+/* Old aliases used when the sections weren't templateized.
+ *
+ * These are kept to keep old code working and can largely
+ * be ignored for anything new.
+ *
+ * SelectableSection is actually just a generic "long text" */
+using LabelledSection = Section<LabelledParagraph>;
+using SelectableSection = Section<Paragraph>;
+using OneLineSection = Section<Paragraph>;
 
 } // namespace TextWidgets
 
--- a/src/core/anime_season.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/core/anime_season.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -79,4 +79,28 @@
 	return !(*this < o);
 }
 
+Season& Season::operator++() {
+	switch (season) {
+		case Season::Name::Winter: season = Season::Name::Spring; break;
+		case Season::Name::Spring: season = Season::Name::Summer; break;
+		case Season::Name::Summer: season = Season::Name::Autumn; break;
+		case Season::Name::Autumn: season = Season::Name::Winter; year++; break;
+		default: season = Season::Name::Unknown; break;
+	}
+
+	return *this;
 }
+
+Season& Season::operator--() {
+	switch (season) {
+		case Season::Name::Winter: season = Season::Name::Autumn; year--; break;
+		case Season::Name::Spring: season = Season::Name::Winter; break;
+		case Season::Name::Summer: season = Season::Name::Spring; break;
+		case Season::Name::Autumn: season = Season::Name::Summer; break;
+		default: season = Season::Name::Unknown; break;
+	}
+
+	return *this;
+}
+
+}
--- a/src/gui/dialog/information.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/dialog/information.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -46,7 +46,7 @@
 }
 
 InformationDialog::InformationDialog(Anime::Anime* anime, std::function<void(Anime::Anime*)> accept, enum Pages page,
-                                     QWidget* parent)
+									 QWidget* parent)
 	: QDialog(parent) {
 	/* ack. lots of brackets here, but MUCH, MUCH MUCH better than what it used to be */
 	setFixedSize(842, 613);
@@ -89,8 +89,8 @@
 
 			{
 				/* Anime title */
-				TextWidgets::Title* anime_title =
-				    new TextWidgets::Title(Strings::ToQString(anime->GetUserPreferredTitle()), main_widget);
+				TextWidgets::Title* anime_title = new TextWidgets::Title(main_widget);
+				anime_title->SetText(anime->GetUserPreferredTitle());
 				main_layout->addWidget(anime_title);
 			}
 
@@ -111,7 +111,10 @@
 					settings_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
 					QVBoxLayout* settings_layout = new QVBoxLayout(settings_widget);
-					settings_layout->addWidget(new TextWidgets::Header(tr("Anime list"), settings_widget));
+
+					TextWidgets::Header* header = new TextWidgets::Header(settings_widget);
+					header->SetText(Strings::Translate("Anime list"));
+					settings_layout->addWidget(header);
 
 					{
 						/* Anime List */
@@ -143,7 +146,7 @@
 
 								QSpinBox* spin_box = new QSpinBox(section);
 								connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this,
-								        [this](int i) { _progress = i; });
+										[this](int i) { _progress = i; });
 								spin_box->setRange(0, anime->GetEpisodes());
 								spin_box->setSingleStep(1);
 								spin_box->setValue(_progress = anime->GetUserProgress());
@@ -155,9 +158,9 @@
 								/* Rewatching? */
 								QCheckBox* checkbox = new QCheckBox(tr("Rewatching"));
 								connect(checkbox, QOverload<int>::of(&QCheckBox::stateChanged), this,
-								        [this](int state) { _rewatching = (state == Qt::Checked); });
+										[this](int state) { _rewatching = (state == Qt::Checked); });
 								checkbox->setCheckState((_rewatching = anime->GetUserIsRewatching()) ? Qt::Checked
-								                                                                     : Qt::Unchecked);
+																									 : Qt::Unchecked);
 								checkbox->setFixedWidth(LAYOUT_ITEM_WIDTH);
 								layout->addWidget(checkbox, 1, 1);
 							}
@@ -174,15 +177,15 @@
 								_status = anime->GetUserStatus();
 								for (unsigned int i = 0; i < Anime::ListStatuses.size(); i++) {
 									combo_box->addItem(Strings::ToQString(Translate::ToLocalString(Anime::ListStatuses[i])),
-									                   static_cast<int>(Anime::ListStatuses[i]));
+													   static_cast<int>(Anime::ListStatuses[i]));
 									if (Anime::ListStatuses[i] == _status)
 										combo_box->setCurrentIndex(i);
 								}
 
 								connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
-								        [this, combo_box](int) {
-									        _status = static_cast<Anime::ListStatus>(combo_box->currentData().toInt());
-								        });
+										[this, combo_box](int) {
+											_status = static_cast<Anime::ListStatus>(combo_box->currentData().toInt());
+										});
 
 								/* this should NEVER, EVER, be NOT_IN_LIST */
 								combo_box->setFixedWidth(LAYOUT_ITEM_WIDTH);
@@ -195,7 +198,7 @@
 
 								QSpinBox* spin_box = new QSpinBox(section);
 								connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this,
-								        [this](int i) { _score = i; });
+										[this](int i) { _score = i; });
 								spin_box->setRange(0, 100);
 								spin_box->setSingleStep(5);
 								spin_box->setValue(_score = anime->GetUserScore());
@@ -225,7 +228,7 @@
 
 								OptionalDate* date = new OptionalDate(true, section);
 								connect(date, &OptionalDate::DataChanged, this,
-								        [this](bool enabled, Date date) { _started = enabled ? date : Date(); });
+										[this](bool enabled, Date date) { _started = enabled ? date : Date(); });
 								date->setFixedWidth(LAYOUT_ITEM_WIDTH);
 								_started = anime->GetUserDateStarted();
 								if (!_started.IsValid()) {
@@ -242,7 +245,7 @@
 
 								OptionalDate* date = new OptionalDate(true, section);
 								connect(date, &OptionalDate::DataChanged, this,
-								        [this](bool enabled, Date date) { _completed = enabled ? date : Date(); });
+										[this](bool enabled, Date date) { _completed = enabled ? date : Date(); });
 								date->setFixedWidth(LAYOUT_ITEM_WIDTH);
 								_completed = anime->GetUserDateCompleted();
 								if (!_completed.IsValid()) {
@@ -260,28 +263,28 @@
 
 					/*
 					{
-					    // commenting this out until it actually gets implemented :)
+						// commenting this out until it actually gets implemented :)
 
-					    settings_layout->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
+						settings_layout->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
 
-					    QWidget* sg_local_content = new QWidget(settings_widget);
-					    QVBoxLayout* sg_local_layout = new QVBoxLayout(sg_local_content);
-					    sg_local_layout->setSpacing(5);
-					    sg_local_layout->setContentsMargins(12, 0, 0, 0);
+						QWidget* sg_local_content = new QWidget(settings_widget);
+						QVBoxLayout* sg_local_layout = new QVBoxLayout(sg_local_content);
+						sg_local_layout->setSpacing(5);
+						sg_local_layout->setContentsMargins(12, 0, 0, 0);
 
-					    CREATE_SECTION(sg_local_content, [this, &anime](QWidget* section, QGridLayout* layout){
-					        layout->addWidget(new QLabel(tr("Alternative titles:"), section), 0, 0);
+						CREATE_SECTION(sg_local_content, [this, &anime](QWidget* section, QGridLayout* layout){
+							layout->addWidget(new QLabel(tr("Alternative titles:"), section), 0, 0);
 
-					        QLineEdit* line_edit = new QLineEdit("", section);
-					        line_edit->setPlaceholderText(
-					            tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
-					        layout->addWidget(line_edit, 1, 0);
+							QLineEdit* line_edit = new QLineEdit("", section);
+							line_edit->setPlaceholderText(
+								tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
+							layout->addWidget(line_edit, 1, 0);
 
-					        QCheckBox* checkbox = new QCheckBox(tr("Use the first alternative title to search for
+							QCheckBox* checkbox = new QCheckBox(tr("Use the first alternative title to search for
 					torrents")); layout->addWidget(checkbox, 2, 0);
-					    });
+						});
 
-					    settings_layout->addWidget(sg_local_content);
+						settings_layout->addWidget(sg_local_content);
 					}
 					*/
 
--- a/src/gui/pages/now_playing.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/pages/now_playing.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -19,8 +19,8 @@
 	QVBoxLayout* layout = new QVBoxLayout(this);
 	layout->setContentsMargins(0, 0, 0, 0);
 
-	TextWidgets::Title* title = new TextWidgets::Title(tr("Now Playing"), this);
-	layout->addWidget(title);
+	title_.SetText(Strings::Translate("Now Playing"));
+	layout->addWidget(&title_);
 
 	layout->addStretch();
 }
@@ -28,32 +28,27 @@
 Playing::Playing(QWidget* parent) : QWidget(parent) {
 	QHBoxLayout* layout = new QHBoxLayout(this);
 
-	_main.reset(new QWidget(this));
-	_main->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	_main.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
 
-	QVBoxLayout* main_layout = new QVBoxLayout(_main.get());
+	QVBoxLayout* main_layout = new QVBoxLayout(&_main);
 	main_layout->setContentsMargins(0, 0, 0, 0);
 
-	_title.reset(new TextWidgets::Title("", _main.get()));
-	main_layout->addWidget(_title.get());
+	main_layout->addWidget(&_title);
 
-	_info.reset(new AnimeInfoWidget(_main.get()));
-	_info->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-	_info->layout()->setContentsMargins(0, 0, 0, 0);
-	main_layout->addWidget(_info.get());
+	_info.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	_info.layout()->setContentsMargins(0, 0, 0, 0);
+	main_layout->addWidget(&_info);
 
 	/* "sidebar", includes... just the anime image :) */
-	_sidebar.reset(new QWidget(this));
-	QVBoxLayout* sidebar_layout = new QVBoxLayout(_sidebar.get());
+	QVBoxLayout* sidebar_layout = new QVBoxLayout(&_sidebar);
 	sidebar_layout->setContentsMargins(0, 0, 0, 0);
 
-	_poster.reset(new Poster(_sidebar.get()));
-	sidebar_layout->addWidget(_poster.get());
+	sidebar_layout->addWidget(&_poster);
 
 	sidebar_layout->addStretch();
 
-	layout->addWidget(_sidebar.get());
-	layout->addWidget(_main.get());
+	layout->addWidget(&_sidebar);
+	layout->addWidget(&_main);
 	layout->setSpacing(10);
 	layout->setContentsMargins(0, 0, 0, 0);
 }
@@ -68,9 +63,9 @@
 		return;
 	_id = anime.GetId();
 	_episode = Strings::ToInt(Strings::ToUtf8String(info.get(anitomy::kElementEpisodeNumber)));
-	_title->SetText(Strings::ToQString(anime.GetUserPreferredTitle()));
-	_info->SetAnime(anime);
-	_poster->SetAnime(anime);
+	_title.SetText(anime.GetUserPreferredTitle());
+	_info.SetAnime(anime);
+	_poster.SetAnime(anime);
 
 	updateGeometry();
 }
@@ -85,24 +80,23 @@
 	setFrameShadow(QFrame::Sunken);
 	setAutoFillBackground(true);
 
-	stack = new QStackedWidget(this);
-	stack->addWidget(new NowPlayingPages::Default(stack));
-	stack->addWidget(new NowPlayingPages::Playing(stack));
-	layout->addWidget(stack);
+	stack_.addWidget(&default_);
+	stack_.addWidget(&playing_);
+	layout->addWidget(&stack_);
 
 	SetDefault();
 }
 
 void NowPlayingPage::SetDefault() {
-	stack->setCurrentIndex(0);
+	stack_.setCurrentIndex(static_cast<int>(Subpages::Default));
 }
 
 int NowPlayingPage::GetPlayingId() {
-	return reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->GetPlayingAnime();
+	return playing_.GetPlayingAnime();
 }
 
 void NowPlayingPage::SetPlaying(const Anime::Anime& anime, const anitomy::Elements& info) {
-	reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->SetPlayingAnime(anime, info);
-	stack->setCurrentIndex(1);
+	playing_.SetPlayingAnime(anime, info);
+	stack_.setCurrentIndex(static_cast<int>(Subpages::Playing));
 	updateGeometry();
 }
--- a/src/gui/pages/seasons.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/pages/seasons.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -15,6 +15,8 @@
 #include <QToolButton>
 #include <QVBoxLayout>
 
+#include <iostream>
+
 SeasonsPageSearchThread::SeasonsPageSearchThread(QObject* parent) : QThread(parent) {
 }
 
@@ -56,7 +58,7 @@
 void SeasonsPage::Refresh() {
 	setUpdatesEnabled(false);
 
-	if (!buttons || !season_button)
+	if (!buttons)
 		return;
 
 	buttons->clear();
@@ -70,7 +72,7 @@
 		buttons->setItemWidget(item, button);
 	}
 
-	season_button->setText(Strings::ToQString(Translate::ToLocalString(season_)));
+	season_button.setText(Strings::ToQString(Translate::ToLocalString(season_)));
 
 	setUpdatesEnabled(true);
 }
@@ -96,47 +98,50 @@
 		toolbar->setMovable(false);
 
 		{
-			/* currently this is VERY hardcoded to en_US */
-			static constexpr Date::Year last_year = 1960;
+			/* XXX this last year probably shouldn't be hardcoded */
+			static const Anime::Season last_season(Anime::Season::Name::Winter, 1960);
+			Anime::Season current_season(Date(QDate::currentDate()));
+			const Date::Year year_before_collapse = GetClosestDecade(current_season.year) - 10;
 
-			auto create_year_menu = [this](QWidget* parent, QMenu* parent_menu, Date::Year year){
-				const QString year_s = QString::number(year);
+			/* year -> menu for that year */
+			std::map<Date::Year, QMenu*> menu_map;
 
-				QMenu* menu = new QMenu(year_s, parent);
-				for (const auto& season : Anime::Season::Names) {
-					QAction* action = menu->addAction(Strings::ToQString(Translate::ToLocalString(Anime::Season(season, year))));
-					connect(action, &QAction::triggered, this, [this, season, year] {
-						SetSeason({season, year});
-					});
-				}
-				parent_menu->addMenu(menu);
+			auto create_season_menu = [&](QWidget* parent, Anime::Season season){
+				QMenu*& menu = menu_map[season.year];
+				if (!menu)
+					menu = new QMenu(QString::number(season.year), parent);
+
+				QAction* action = menu->addAction(Strings::ToQString(Translate::ToLocalString(season)));
+				connect(action, &QAction::triggered, this, [this, season] {
+					SetSeason(season);
+				});
 			};
 
-			auto create_decade_menu = [create_year_menu](QWidget* parent, QMenu* parent_menu, Date::Year decade) {
-				QMenu* menu = new QMenu(QString::number(decade) + "s", parent);
-				for (int i = 9; i >= 0; i--)
-					create_year_menu(parent, menu, decade + i);
-				parent_menu->addMenu(menu);
-			};
+			for (Anime::Season s = current_season; s >= last_season; --s)
+				create_season_menu(&season_button, s);
+
+			/* ------------------------------------------------------- */
+			/* now actually generate the full menu */
 
-			/* we'll be extinct by the time this code breaks, so I guess it's fine :) */
-			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);
+			QMenu* full_menu = new QMenu(&season_button);
+
+			for (Date::Year c = current_season.year; c >= year_before_collapse; c--)
+				full_menu->addMenu(menu_map[c]);
+
+			full_menu->addSeparator();
 
-			for (Date::Year c = current_year; c >= year_before_collapse; c--)
-				create_year_menu(season_button, full_season_menu, c);
-
-			full_season_menu->addSeparator();
+			/* collapse each menu into a decade */
+			for (Date::Year c = year_before_collapse - 10; c >= last_season.year; c -= 10) {
+				QMenu* decade_menu = new QMenu(tr("%1s").arg(QString::number(c)), parent);
+				for (Date::Year i = c + 9; i >= c; i--)
+					decade_menu->addMenu(menu_map[i]);
+				full_menu->addMenu(decade_menu);
+			}
 
-			for (Date::Year c = year_before_collapse - 10; c >= last_year; c -= 10)
-				create_decade_menu(season_button, full_season_menu, c);
+			season_button.setMenu(full_menu);
+			season_button.setPopupMode(QToolButton::InstantPopup);
 
-			season_button->setMenu(full_season_menu);
-			season_button->setPopupMode(QToolButton::InstantPopup);
-
-			toolbar->addWidget(season_button);
+			toolbar->addWidget(&season_button);
 		}
 
 		toolbar->addSeparator();
--- a/src/gui/pages/statistics.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/pages/statistics.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -17,7 +17,8 @@
 #include <cmath>
 #include <sstream>
 
-StatisticsPage::StatisticsPage(QWidget* parent) : QFrame(parent) {
+StatisticsPage::StatisticsPage(QWidget* parent)
+	: QFrame(parent) {
 	setBackgroundRole(QPalette::Base);
 
 	QVBoxLayout* layout = new QVBoxLayout(this);
@@ -27,25 +28,34 @@
 
 	setAutoFillBackground(true);
 
-	_anime_list.reset(new TextWidgets::LabelledSection(
-	    tr("Anime list"),
-	    tr("Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:"),
-	    "", this));
-	layout->addWidget(_anime_list.get());
+	const std::vector<std::pair<std::string, std::string>> al_data_template = {
+		{Strings::Translate("Anime count:"), ""},
+		{Strings::Translate("Episode count:"), ""},
+		{Strings::Translate("Time spent watching:"), ""},
+		{Strings::Translate("Time to complete:"), ""},
+		{Strings::Translate("Average score:"), ""},
+		{Strings::Translate("Score deviation:"), ""},
+	};
+
+	_anime_list.GetHeader().SetText(Strings::Translate("Anime List"));
+	_anime_list.GetContent().SetData(al_data_template);
+
+	layout->addWidget(&_anime_list);
 
 	{
 		QWidget* score_dist_widget = new QWidget(this);
 		QVBoxLayout* score_dist_layout = new QVBoxLayout(score_dist_widget);
 
-		score_dist_layout->addWidget(new TextWidgets::Header(tr("Score distribution"), score_dist_widget));
+		TextWidgets::Header* hdr = new TextWidgets::Header(score_dist_widget);
+		hdr->SetText(Strings::Translate("Score distribution"));
+		score_dist_layout->addWidget(hdr);
 
 		/* Ew */
 		{
 			QWidget* score_graph_parent = new QWidget(score_dist_widget);
 			QVBoxLayout* score_parent_layout = new QVBoxLayout(score_graph_parent);
 
-			_score_distribution_graph.reset(new Graph<int>(score_graph_parent));
-			score_parent_layout->addWidget(_score_distribution_graph.get());
+			score_parent_layout->addWidget(&_score_distribution_graph);
 
 			score_parent_layout->setSpacing(0);
 			score_parent_layout->setContentsMargins(12, 0, 0, 0);
@@ -58,8 +68,15 @@
 		layout->addWidget(score_dist_widget);
 	}
 
-	_application.reset(new TextWidgets::LabelledSection(tr("Minori"), tr("Uptime:\nRequests made:"), "\n\n", this));
-	layout->addWidget(_application.get());
+	const std::vector<std::pair<std::string, std::string>> app_data_template = {
+		{Strings::Translate("Uptime:"), ""},
+		{Strings::Translate("Requests made:"), ""},
+	};
+
+	_application.GetHeader().SetText(Strings::Translate("Minori"));
+	_application.GetContent().SetData(app_data_template);
+
+	layout->addWidget(&_application);
 
 	layout->addStretch();
 
@@ -84,25 +101,26 @@
 }
 
 void StatisticsPage::UpdateStatistics() {
-	/* Anime list */
-	QString string = "";
-	QTextStream ts(&string);
-	ts << Anime::db.GetTotalAnimeAmount() << '\n';
-	ts << Anime::db.GetTotalEpisodeAmount() << '\n';
-	ts << Strings::ToQString(Time::GetSecondsAsAbsoluteString(Time::Units::Minutes, Anime::db.GetTotalWatchedAmount(), 60.0)) << '\n';
-	ts << Strings::ToQString(Time::GetSecondsAsAbsoluteString(Time::Units::Minutes, Anime::db.GetTotalPlannedAmount(), 60.0)) << '\n';
-	ts << Anime::db.GetAverageScore() << '\n';
-	ts << Anime::db.GetScoreDeviation();
-	_anime_list->GetData()->setText(string);
+	const std::vector<std::pair<std::string, std::string>> al_data = {
+		{Strings::Translate("Anime count:"), Strings::ToUtf8String(Anime::db.GetTotalAnimeAmount())},
+		{Strings::Translate("Episode count:"), Strings::ToUtf8String(Anime::db.GetTotalEpisodeAmount())},
+		{Strings::Translate("Time spent watching:"), Time::GetSecondsAsAbsoluteString(Time::Units::Minutes, Anime::db.GetTotalWatchedAmount(), 60.0)},
+		{Strings::Translate("Time to complete:"), Time::GetSecondsAsAbsoluteString(Time::Units::Minutes, Anime::db.GetTotalPlannedAmount(), 60.0)},
+		{Strings::Translate("Average score:"), Strings::ToUtf8String(Anime::db.GetAverageScore())},
+		{Strings::Translate("Score deviation:"), Strings::ToUtf8String(Anime::db.GetScoreDeviation())},
+	};
+
+	_anime_list.GetContent().SetData(al_data);
 
-	_score_distribution_graph->Clear();
+	_score_distribution_graph.Clear();
 	for (int i = 10; i <= 100; i += 10)
-		_score_distribution_graph->AddItem(i, GetTotalWithScore(i));
+		_score_distribution_graph.AddItem(i, GetTotalWithScore(i));
 
-	string = "";
-	ts << Strings::ToQString(Time::GetSecondsAsAbsoluteString(Time::Units::Seconds, session.uptime() / 1000)) << '\n';
-	ts << session.GetRequests();
 	/* Application */
-	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
-	_application->GetData()->setText(string);
+	const std::vector<std::pair<std::string, std::string>> app_data_template = {
+		{Strings::Translate("Uptime:"), Time::GetSecondsAsAbsoluteString(Time::Units::Seconds, session.uptime() / 1000)},
+		{Strings::Translate("Requests made:"), Strings::ToUtf8String(session.GetRequests())},
+	};
+
+	_application.GetContent().SetData(app_data_template);
 }
--- a/src/gui/widgets/anime_button.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/widgets/anime_button.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -12,6 +12,8 @@
 #include <QVBoxLayout>
 #include <QWidget>
 
+#include <iostream>
+
 /* This widget is only used on the Seasons page. */
 
 /***********************************\
@@ -26,19 +28,27 @@
 *|_________| Synopsis               *
 \***********************************/
 
-AnimeButton::AnimeButton(QWidget* parent)
-	: QFrame(parent)
-	, _info(tr("Aired:\nEpisodes:\nGenres:\nProducers:\nScore:\nPopularity:"), "\n\n\n\n\n", nullptr)
-	, _synopsis("", nullptr) {
+AnimeButton::AnimeButton(QWidget* parent) : QFrame(parent) {
 	setFrameShadow(QFrame::Plain);
 	setFrameShape(QFrame::Box);
+
 	QHBoxLayout* ly = new QHBoxLayout(this);
 
-	/* XXX does Qt have a "fixed ratio"? */
+	_poster.SetClickable(false);
 	_poster.setFixedSize(120, 170);
-	_poster.SetClickable(false);
 	ly->addWidget(&_poster, 0, Qt::AlignTop);
 
+	const std::vector<std::pair<std::string, std::string>> imap = {
+		{Strings::Translate("Aired:"), ""},
+		{Strings::Translate("Episodes:"), ""},
+		{Strings::Translate("Genres:"), ""},
+		{Strings::Translate("Producers:"), ""},
+		{Strings::Translate("Score:"), ""},
+		{Strings::Translate("Popularity:"), ""},
+	};
+
+	_info.SetData(imap);
+
 	{
 		QWidget* misc_section = new QWidget(this);
 		misc_section->setFixedSize(354, 180);
@@ -56,11 +66,7 @@
 		}
 		misc_layout->addWidget(&_title);
 
-		{
-			QFont fnt(_info.GetLabels()->font());
-			fnt.setWeight(QFont::Bold);
-			_info.GetLabels()->setFont(fnt);
-		}
+		_info.SetStyle(TextWidgets::LabelledParagraph::Style::BoldedLabels);
 
 		_info.setContentsMargins(4, 0, 4, 0);
 		_info.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
@@ -74,6 +80,7 @@
 			dummy_layout->setContentsMargins(0, 0, 0, 0);
 
 			_synopsis.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+			_synopsis.SetSelectable(false);
 			dummy_layout->addWidget(&_synopsis);
 			misc_layout->addWidget(dummy);
 		}
@@ -90,13 +97,18 @@
 	_poster.SetAnime(anime);
 	_title.setText(Strings::ToQString(anime.GetUserPreferredTitle()));
 
-	{
-		const QLocale& locale = session.config.locale.GetLocale();
-		_info.GetData()->setText(locale.toString(anime.GetStartedDate().GetAsQDate(), "dd MMM yyyy") + "\n" +
-		                         QString::number(anime.GetEpisodes()) + "\n" +
-		                         Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) + "\n" + "...\n" +
-		                         QString::number(anime.GetAudienceScore()) + "%\n" + "...");
-	}
+	const QLocale& locale = session.config.locale.GetLocale();
 
-	_synopsis.SetText(Strings::ToQString(anime.GetSynopsis()));
+	const std::vector<std::pair<std::string, std::string>> imap = {
+		{Strings::Translate("Aired:"), Strings::ToUtf8String(locale.toString(anime.GetStartedDate().GetAsQDate(), "dd MMM yyyy"))},
+		{Strings::Translate("Episodes:"), Strings::ToUtf8String(anime.GetEpisodes())},
+		{Strings::Translate("Genres:"), Strings::Implode(anime.GetGenres(), ", ")},
+		{Strings::Translate("Producers:"), "..."},
+		{Strings::Translate("Score:"), Strings::ToUtf8String(anime.GetAudienceScore()) + "%"},
+		{Strings::Translate("Popularity:"), "..."},
+	};
+
+	_info.SetData(imap);
+
+	_synopsis.SetText(anime.GetSynopsis());
 }
--- a/src/gui/widgets/anime_info.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/widgets/anime_info.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -36,17 +36,33 @@
 /* all widgets share this thread */
 static AnimeInfoWidgetGetMetadataThread get_metadata_thread;
 
-AnimeInfoWidget::AnimeInfoWidget(QWidget* parent)
-	: QWidget(parent)
-	, _title(tr("Alternative titles"), "")
-	, _details(tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nProducers:\nScore:"), "")
-	, _synopsis(tr("Synopsis"), "") {
+AnimeInfoWidget::AnimeInfoWidget(QWidget* parent) : QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
+	_title.GetHeader().SetText(Strings::Translate("Alternative titles"));
 	layout->addWidget(&_title);
+
+	_details.GetHeader().SetText(Strings::Translate("Details"));
+
+	const std::vector<std::pair<std::string, std::string>> items = {
+		{Strings::Translate("Type:"), ""},
+		{Strings::Translate("Episodes:"), ""},
+		{Strings::Translate("Status:"), ""},
+		{Strings::Translate("Season:"), ""},
+		{Strings::Translate("Genres:"), ""},
+		{Strings::Translate("Producers:"), ""},
+		{Strings::Translate("Score:"), ""},
+	};
+
+	_details.GetContent().SetData(items);
+
 	layout->addWidget(&_details);
+
+	_synopsis.GetHeader().SetText(Strings::Translate("Synopsis"));
 	layout->addWidget(&_synopsis);
 
+	layout->addStretch();
+
 	/* ... */
 	connect(&get_metadata_thread, &AnimeInfoWidgetGetMetadataThread::NeedRefresh, this, [this](int id) {
 		setUpdatesEnabled(false);
@@ -72,11 +88,11 @@
 		get_metadata_thread.start();
 
 	/* alt titles */
-	_title.GetLine()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	_title.GetContent().SetText(Strings::Implode(anime.GetTitleSynonyms(), ", "));
 
 	RefreshGenres(anime);
 
-	_synopsis.GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis()));
+	_synopsis.GetContent().SetText(anime.GetSynopsis());
 
 	setUpdatesEnabled(true);
 
@@ -84,23 +100,21 @@
 }
 
 void AnimeInfoWidget::RefreshGenres(const Anime::Anime& anime) {
-	/* 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
 	 * Latin-1 (on Windows?) */
 	const auto genres = anime.GetGenres();
 	const auto producers = anime.GetProducers();
 
-	details_data_s << Strings::ToQString(Translate::ToLocalString(anime.GetFormat())) << "\n"
-	               << anime.GetEpisodes() << "\n"
-	               << Strings::ToQString(Translate::ToLocalString(anime.GetAiringStatus())) << "\n"
-	               << Strings::ToQString(Translate::ToLocalString(anime.GetSeason())) << "\n"
-	               << Strings::ToQString((genres.size() > 1)    ? Strings::Implode(genres, ", ")    : "-") << "\n"
-	               << Strings::ToQString((producers.size() > 1) ? Strings::Implode(producers, ", ") : "-") << "\n"
-	               << anime.GetAudienceScore() << "%";
+	const std::vector<std::pair<std::string, std::string>> items = {
+		{Strings::Translate("Type:"), Translate::ToLocalString(anime.GetFormat())},
+		{Strings::Translate("Episodes:"), Strings::ToUtf8String(anime.GetEpisodes())},
+		{Strings::Translate("Status:"), Translate::ToLocalString(anime.GetAiringStatus())},
+		{Strings::Translate("Season:"), Translate::ToLocalString(anime.GetSeason())},
+		{Strings::Translate("Genres:"), (genres.size() > 1)    ? Strings::Implode(genres, ", ")    : "-"},
+		{Strings::Translate("Producers:"), (producers.size() > 1) ? Strings::Implode(producers, ", ") : "-"},
+		{Strings::Translate("Score:"), Strings::ToUtf8String(anime.GetAudienceScore()) + "%"},
+	};
 
-	_details.GetData()->setText(details_data);
+	_details.GetContent().SetData(items);
 }
--- a/src/gui/widgets/elided_label.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/widgets/elided_label.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -1,78 +1,7 @@
-/*
- * Copyright (C) 2016 The Qt Company Ltd.
- * Contact: https://www.qt.io/licensing/
- *
- * This file is part of the QtCore module of the Qt Toolkit.
- *
- * "Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *   * Redistributions of source code must retain the above copyright
- *     notice, this list of conditions and the following disclaimer.
- *   * Redistributions in binary form must reproduce the above copyright
- *     notice, this list of conditions and the following disclaimer in
- *     the documentation and/or other materials provided with the
- *     distribution.
- *   * Neither the name of The Qt Company Ltd nor the names of its
- *     contributors may be used to endorse or promote products derived
- *     from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
- */
+
 
 #include "gui/widgets/elided_label.h"
 
 #include <QPainter>
 #include <QSizePolicy>
 #include <QTextLayout>
-
-ElidedLabel::ElidedLabel(const QString& text, QWidget* parent) : QFrame(parent), content(text) {
-	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
-}
-
-void ElidedLabel::SetText(const QString& text) {
-	content = text;
-	update();
-}
-
-void ElidedLabel::paintEvent(QPaintEvent* event) {
-	QFrame::paintEvent(event);
-
-	QPainter painter(this);
-	QFontMetrics metrics = painter.fontMetrics();
-
-	const int line_spacing = metrics.lineSpacing();
-	int y = 0;
-
-	QTextLayout text_layout(content, painter.font());
-	text_layout.beginLayout();
-	for (;;) {
-		QTextLine line = text_layout.createLine();
-		if (!line.isValid())
-			break;
-
-		line.setLineWidth(width());
-
-		if (height() >= y + (2 * line_spacing)) {
-			line.draw(&painter, QPoint(0, y));
-			y += line_spacing;
-		} else {
-			QString last_line = content.mid(line.textStart());
-			QString elided_last_line = metrics.elidedText(last_line, Qt::ElideRight, width());
-			painter.drawText(QPoint(0, y + metrics.ascent()), elided_last_line);
-			line = text_layout.createLine();
-			break;
-		}
-	}
-	text_layout.endLayout();
-}
--- a/src/gui/widgets/poster.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/widgets/poster.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -16,12 +16,14 @@
 #include <QThread>
 #include <QUrl>
 
+#include <iostream>
+
 Poster::Poster(QWidget* parent) : QFrame(parent) {
 	QHBoxLayout* layout = new QHBoxLayout(this);
 	layout->setContentsMargins(1, 1, 1, 1);
 
 	setCursor(Qt::PointingHandCursor);
-	setFixedSize(150, 225);
+	setFixedSize(150, 225); // FIXME need to kill this
 	setFrameShape(QFrame::Box);
 	setFrameShadow(QFrame::Plain);
 
@@ -101,6 +103,22 @@
 	label_.setPixmap(pixmap.scaled(label_.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
 }
 
+bool Poster::hasHeightForWidth(void) const {
+	return true;
+}
+
+int Poster::heightForWidth(int w) const {
+	return static_cast<int>(static_cast<double>(w) * 225 / 150);
+}
+
 void Poster::resizeEvent(QResizeEvent*) {
 	RenderToLabel();
 }
+
+QSize Poster::minimumSizeHint() const {
+	return QSize(120, heightForWidth(120));
+}
+
+QSize Poster::sizeHint() const {
+	return QSize(150, heightForWidth(150));
+}
--- a/src/gui/widgets/text.cc	Sun Jun 23 10:32:09 2024 -0400
+++ b/src/gui/widgets/text.cc	Tue Jun 25 11:19:54 2024 -0400
@@ -1,5 +1,6 @@
 #include "gui/widgets/text.h"
 #include "core/session.h"
+#include "core/strings.h"
 
 #include <QDebug>
 #include <QFrame>
@@ -9,272 +10,128 @@
 #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)
+/* Generic header meant to be used in conjunction with Section<T> */
+
+Header::Header(QWidget* parent)
 	: QWidget(parent)
-	, static_text_title(title) {
+	, title_(new QLabel(this))
+	, separator_(new QFrame(this)) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
-	static_text_title.setTextFormat(Qt::PlainText);
+	title_->setTextFormat(Qt::PlainText);
 
 	{
-		QFont font = static_text_title.font();
+		QFont font = title_->font();
 		font.setWeight(QFont::Bold);
-		static_text_title.setFont(font);
+		title_->setFont(font);
 	}
 
-	static_text_line.setFrameShape(QFrame::HLine);
-	static_text_line.setFrameShadow(QFrame::Sunken);
-	static_text_line.setFixedHeight(2);
+	separator_->setFrameShape(QFrame::HLine);
+	separator_->setFrameShadow(QFrame::Sunken);
+	separator_->setFixedHeight(2);
 
-	layout->addWidget(&static_text_title);
-	layout->addWidget(&static_text_line);
+	layout->addWidget(title_.data());
+	layout->addWidget(separator_.data());
 	layout->setSpacing(0);
 	layout->setContentsMargins(0, 0, 0, 0);
 }
 
-void Header::SetText(const QString& text) {
-	static_text_title.setText(text);
+void Header::SetText(const std::string& text) {
+	title_->setText(Strings::ToQString(text));
 	updateGeometry();
 }
 
-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.setFrameStyle(QFrame::NoFrame);
-
-	text_edit.document()->setDocumentMargin(0);
-
-	SetText(text);
-
-	layout->addWidget(&text_edit);
-}
-
-void Paragraph::SetText(const QString& text) {
-	text_edit.document()->setPlainText(text);
+/* ---------------------------------------------------------------------------------- */
+/* "Paragraph" widgets, as in widgets meant to hold a bunch of text. */
 
-	/* return the view to the start */
-	QTextCursor cursor = text_edit.textCursor();
-	cursor.setPosition(0);
-	text_edit.setTextCursor(cursor);
-}
-
-bool Paragraph::hasHeightForWidth() const {
-	return true;
-}
-
-int Paragraph::heightForWidth(int w) const {
-	QTextDocument* doc = text_edit.document();
-	doc->setTextWidth(w);
-
-	return doc->size().toSize().height();
-}
-
-QPlainTextEdit* Paragraph::GetLabel() {
-	return &text_edit;
-}
-
-Line::Line(QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
+Paragraph::Paragraph(QWidget *parent) : QWidget(parent), label_(new QLabel) {
+	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;");
+	label_->setTextInteractionFlags(Qt::TextBrowserInteraction);
 
-	layout->addWidget(&line_edit_);
-}
+	/* defaults */
+	SetWordWrap(true);
+	SetSelectable(true);
 
-Line::Line(const QString& text, QWidget* parent) : Line(parent) {
-	SetText(text);
+	layout->addWidget(label_.data());
 }
 
-void Line::SetText(const QString& text) {
-	line_edit_.setText(text);
-	line_edit_.setCursorPosition(0);
+void Paragraph::SetText(const std::string& text) {
+	label_->setText(Strings::ToQString(text));
 }
 
-Title::Title(const QString& title, QWidget* parent) : Line(title, parent) {
-	QFont fnt(line_edit_.font());
-	fnt.setPixelSize(16);
-	line_edit_.setFont(fnt);
-
-	line_edit_.setForegroundRole(QPalette::Highlight);
+void Paragraph::SetSelectable(bool enable) {
+	label_->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents, !enable);
+	label_->setCursor(enable ? Qt::IBeamCursor : Qt::ArrowCursor);
 }
 
-Section::Section(const QString& title, const QString& data, QWidget* parent)
-	: QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	paragraph = new Paragraph(data, this);
-	paragraph->GetLabel()->setTextInteractionFlags(Qt::NoTextInteraction);
-	paragraph->GetLabel()->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
-	paragraph->GetLabel()->setWordWrapMode(QTextOption::NoWrap);
-
-	content_layout->addWidget(paragraph);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(paragraph);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
+void Paragraph::SetWordWrap(bool enable) {
+	label_->setWordWrap(enable);
 }
 
-Header* Section::GetHeader() {
-	return header;
-}
+/* LabelledParagraph implementation */
 
-Paragraph* Section::GetParagraph() {
-	return paragraph;
-}
-
-/* despite being named a "labelled paragraph" this uses QLabels for simplicity */
-LabelledParagraph::LabelledParagraph(const QString& label, const QString& data, QWidget* parent)
+LabelledParagraph::LabelledParagraph(QWidget* parent)
 	: QWidget(parent)
-	, labels_(label)
-	, data_(data) {
+	, contents_(new QWidget)
+	, contents_layout_(new QGridLayout) {
 	QHBoxLayout* ly = new QHBoxLayout(this);
 
-	labels_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-	data_.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+	contents_layout_->setVerticalSpacing(1);
+	contents_layout_->setHorizontalSpacing(20);
+	contents_layout_->setContentsMargins(0, 0, 0, 0);
+	contents_layout_->setColumnStretch(1, 0);
 
-	ly->addWidget(&labels_, 0, Qt::AlignTop);
-	ly->addWidget(&data_, 0, Qt::AlignTop);
-	ly->setSpacing(20);
+	contents_->setLayout(contents_layout_.data());
+
+	ly->addWidget(contents_.data());
 	ly->setContentsMargins(0, 0, 0, 0);
 }
 
-QLabel* LabelledParagraph::GetLabels() {
-	return &labels_;
-}
-
-QLabel* LabelledParagraph::GetData() {
-	return &data_;
-}
-
-QLabel* LabelledParagraph::GetParagraph() {
-	return GetData();
+LabelledParagraph::~LabelledParagraph() {
+	data_.clear();
 }
 
-LabelledSection::LabelledSection(const QString& title, const QString& label, const QString& data, QWidget* parent)
-    : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	// this is not accessible from the object because there's really
-	// no reason to make it accessible...
-	content = new LabelledParagraph(label, data, this);
-	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
-	content->setContentsMargins(12, 0, 0, 0);
+void LabelledParagraph::Clear(void) {
+	for (auto& [label, data] : data_) {
+		contents_layout_->removeWidget(label.data());
+		contents_layout_->removeWidget(data.data());
+	}
 
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* LabelledSection::GetHeader() {
-	return header;
-}
-
-QLabel* LabelledSection::GetLabels() {
-	return content->GetLabels();
-}
-
-QLabel* LabelledSection::GetData() {
-	return content->GetData();
-}
-
-QLabel* LabelledSection::GetParagraph() {
-	return content->GetParagraph();
+	data_.clear();
 }
 
-SelectableSection::SelectableSection(const QString& title, const QString& data, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
+void LabelledParagraph::SetData(const std::vector<std::pair<std::string, std::string>>& data) {
+	Clear();
 
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	paragraph = new Paragraph(data, content);
+	data_.reserve(data.size());
+	for (std::size_t i = 0; i < data.size(); i++) {
+		QSharedPointer<QLabel> first(new QLabel);
+		QSharedPointer<QLabel> second(new QLabel);
 
-	content_layout->addWidget(paragraph);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(12, 0, 0, 0);
-	content->setContentsMargins(0, 0, 0, 0);
+		first->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+		first->setText(Strings::ToQString(data[i].first));
+		second->setText(Strings::ToQString(data[i].second));
 
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
+		data_.push_back({first, second});
 
-Header* SelectableSection::GetHeader() {
-	return header;
+		contents_layout_->addWidget(first.data(), i, 0);
+		contents_layout_->addWidget(second.data(), i, 1);
+	}
 }
 
-Paragraph* SelectableSection::GetParagraph() {
-	return paragraph;
-}
-
-OneLineSection::OneLineSection(const QString& title, const QString& text, QWidget* parent) : QWidget(parent) {
-	QVBoxLayout* layout = new QVBoxLayout(this);
-
-	header = new Header(title, this);
-
-	QWidget* content = new QWidget(this);
-	QHBoxLayout* content_layout = new QHBoxLayout(content);
-
-	line = new Line(text, content);
+void LabelledParagraph::SetStyle(int style) {
+	const QString style_sheet = (style & LabelledParagraph::BoldedLabels) ? "font-weight: bold;" : "";
+	for (auto& [label, data] : data_)
+		label->setStyleSheet(style_sheet);
 
-	content_layout->addWidget(line);
-	content_layout->setSpacing(0);
-	content_layout->setContentsMargins(0, 0, 0, 0);
-	content->setContentsMargins(12, 0, 0, 0);
-
-	layout->addWidget(header);
-	layout->addWidget(content);
-	layout->setSpacing(0);
-	layout->setContentsMargins(0, 0, 0, 0);
-}
-
-Header* OneLineSection::GetHeader() {
-	return header;
-}
-
-Line* OneLineSection::GetLine() {
-	return line;
+	// TODO ElidedData
 }
 
 } // namespace TextWidgets