changeset 83:d02fdf1d6708

*: huuuge update 1. make the now playing page function correctly 2. de-constructorfy many of our custom widgets, allowing them to be changed on-the-fly from the Now Playing page 3. ... :)
author Paper <mrpapersonic@gmail.com>
date Tue, 24 Oct 2023 22:01:02 -0400
parents 8b65c417c225
children eab9e623eb84
files .clang-format include/core/anime_db.h include/core/array.h include/gui/dialog/information.h include/gui/pages/anime_list.h include/gui/pages/now_playing.h include/gui/widgets/poster.h include/gui/widgets/text.h src/core/anime_db.cc src/core/json.cc src/gui/dialog/information.cc src/gui/pages/anime_list.cc src/gui/pages/now_playing.cc src/gui/pages/statistics.cc src/gui/widgets/anime_info.cc src/gui/widgets/poster.cc src/gui/widgets/text.cc src/gui/window.cc src/track/constants.cc
diffstat 19 files changed, 214 insertions(+), 130 deletions(-) [+]
line wrap: on
line diff
--- a/.clang-format	Mon Oct 23 13:37:42 2023 -0400
+++ b/.clang-format	Tue Oct 24 22:01:02 2023 -0400
@@ -11,7 +11,6 @@
 IndentAccessModifiers: true
 IndentPPDirectives: AfterHash
 
-BreakArrays: true
 BreakBeforeBraces: Attach
 BreakStringLiterals: true
 
--- a/include/core/anime_db.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/core/anime_db.h	Tue Oct 24 22:01:02 2023 -0400
@@ -16,7 +16,7 @@
 		double GetAverageScore();
 		double GetScoreDeviation();
 		int GetListsAnimeAmount(ListStatus status);
-		int GetAnimeFromTitle(std::string title);
+		int GetAnimeFromTitle(const std::string& title);
 };
 
 extern Database db;
--- a/include/core/array.h	Mon Oct 23 13:37:42 2023 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-#ifndef __core__array_h
-#define __core__array_h
-
-#ifndef ARRAYSIZE
-#	define ARRAYSIZE(x) (sizeof(x) / sizeof(x[0]))
-#endif
-
-#endif // __core__array_h
\ No newline at end of file
--- a/include/gui/dialog/information.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/gui/dialog/information.h	Tue Oct 24 22:01:02 2023 -0400
@@ -9,17 +9,16 @@
 		Q_OBJECT
 
 	public:
-		InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
+		InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
 
 	private:
-		unsigned int id;
-		unsigned int progress;
-		unsigned int score;
-		bool rewatching;
-		Anime::ListStatus status;
-		std::string notes;
-		Date started;
-		Date completed;
-		void SaveData();
+		void SaveData(Anime::Anime& anime);
+		unsigned int _progress;
+		unsigned int _score;
+		bool _rewatching;
+		Anime::ListStatus _status;
+		std::string _notes;
+		Date _started;
+		Date _completed;
 };
 #endif // __gui__dialog__information_h
--- a/include/gui/pages/anime_list.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/gui/pages/anime_list.h	Tue Oct 24 22:01:02 2023 -0400
@@ -95,6 +95,6 @@
 		QTabBar* tab_bar;
 		QTreeView* tree_view;
 		QRect panelRect;
-		AnimeListPageSortFilter* sort_models[5];
+		std::array<AnimeListPageSortFilter*, 5> sort_models;
 };
 #endif // __gui__pages__anime_list_h
\ No newline at end of file
--- a/include/gui/pages/now_playing.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/gui/pages/now_playing.h	Tue Oct 24 22:01:02 2023 -0400
@@ -4,6 +4,9 @@
 #include <unordered_map>
 
 class QStackedWidget;
+namespace Anime {
+class Anime;
+}
 
 class NowPlayingPage : public QFrame {
 		Q_OBJECT
@@ -11,7 +14,7 @@
 	public:
 		NowPlayingPage(QWidget* parent = nullptr);
 		void SetDefault();
-		void SetPlaying(int id, const std::unordered_map<std::string, std::string>& episodes);
+		void SetPlaying(const Anime::Anime& anime, const std::unordered_map<std::string, std::string>& episodes);
 		int GetPlayingId();
 
 	private:
--- a/include/gui/widgets/poster.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/gui/widgets/poster.h	Tue Oct 24 22:01:02 2023 -0400
@@ -1,21 +1,25 @@
 #ifndef __gui__widgets__poster_h
 #define __gui__widgets__poster_h
-#include <QByteArray>
 #include <QFrame>
 #include <QImage>
 
 class QWidget;
 class ClickableLabel;
+namespace Anime {
+class Anime;
+}
 
 class Poster : public QFrame {
 		Q_OBJECT
 
 	public:
-		Poster(int id, QWidget* parent = nullptr);
+		Poster(QWidget* parent = nullptr);
+		Poster(const Anime::Anime& anime, QWidget* parent = nullptr);
+		void SetAnime(const Anime::Anime& anime);
 
 	protected:
 		void resizeEvent(QResizeEvent*) override;
-		void ImageDownloadFinished(QByteArray arr);
+		void ImageDownloadFinished(const QByteArray& arr);
 		void RenderToLabel();
 
 	private:
--- a/include/gui/widgets/text.h	Mon Oct 23 13:37:42 2023 -0400
+++ b/include/gui/widgets/text.h	Tue Oct 24 22:01:02 2023 -0400
@@ -16,8 +16,8 @@
 		Q_OBJECT
 
 	public:
-		Header(QString title, QWidget* parent = nullptr);
-		void SetText(QString title);
+		Header(const QString& title, QWidget* parent = nullptr);
+		void SetText(const QString& title);
 
 	private:
 		QLabel* static_text_title;
@@ -28,8 +28,8 @@
 		Q_OBJECT
 
 	public:
-		Paragraph(QString text, QWidget* parent = nullptr);
-		void SetText(QString text);
+		Paragraph(const QString& text, QWidget* parent = nullptr);
+		void SetText(const QString& text);
 		QSize minimumSizeHint() const override;
 		QSize sizeHint() const override;
 };
@@ -38,21 +38,23 @@
 		Q_OBJECT
 
 	public:
-		Line(QString text, QWidget* parent = nullptr);
+		Line(QWidget* parent = nullptr);
+		Line(const QString& text, QWidget* parent = nullptr);
+		void SetText(const QString& text);
 };
 
 class Title : public Line {
 		Q_OBJECT
 
 	public:
-		Title(QString title, QWidget* parent = nullptr);
+		Title(const QString& title, QWidget* parent = nullptr);
 };
 
 class Section : public QWidget {
 		Q_OBJECT
 
 	public:
-		Section(QString title, QString data, QWidget* parent = nullptr);
+		Section(const QString& title, const QString& data, QWidget* parent = nullptr);
 		Header* GetHeader();
 		Paragraph* GetParagraph();
 
@@ -65,7 +67,7 @@
 		Q_OBJECT
 
 	public:
-		LabelledSection(QString title, QString label, QString data, QWidget* parent = nullptr);
+		LabelledSection(const QString& title, const QString& label, const QString& data, QWidget* parent = nullptr);
 		Header* GetHeader();
 		Paragraph* GetLabels();
 		Paragraph* GetParagraph();
@@ -80,7 +82,7 @@
 		Q_OBJECT
 
 	public:
-		SelectableSection(QString title, QString data, QWidget* parent = nullptr);
+		SelectableSection(const QString& title, const QString& data, QWidget* parent = nullptr);
 		Header* GetHeader();
 		Paragraph* GetParagraph();
 
@@ -93,7 +95,7 @@
 		Q_OBJECT
 
 	public:
-		OneLineSection(QString title, QString data, QWidget* parent = nullptr);
+		OneLineSection(const QString& title, const QString& data, QWidget* parent = nullptr);
 		Header* GetHeader();
 		Line* GetLine();
 
--- a/src/core/anime_db.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/core/anime_db.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -62,8 +62,9 @@
 	return total;
 }
 
-/* I'm sure many will appreciate this being called an
-   "average" instead of a "mean" */
+/* In Taiga this is called a mean, but "average" is
+   what's primarily used in conversation, at least
+   in the U.S. */
 double Database::GetAverageScore() {
 	double avg = 0;
 	int amt = 0;
@@ -88,19 +89,55 @@
 	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
 }
 
-int Database::GetAnimeFromTitle(std::string title) {
+template <typename T, typename U>
+static T get_lowest_in_map(const std::unordered_map<T, U>& map) {
+	if (map.size() <= 0)
+		return 0;
+	T id;
+	U ret = std::numeric_limits<U>::max();
+	for (const auto& t : map) {
+		if (t.second < ret) {
+			ret = t.second;
+			id = t.first;
+		}
+	}
+	return id;
+}
+
+/* This is really fugly but WHO CARES :P
+
+   This sort of ""advanced"" algorithm is only in effect because
+   there are some special cases, e.g. Another and Re:ZERO, where 
+   we get the wrong match, so we have to create Advanced Techniques
+   to solve this
+
+   This algorithm:
+     1. searches each anime item for a match to the preferred title
+        AND all synonyms and marks those matches with
+          `synonym.length() - (synonym.find(needle) + needle.length());`
+        which, on a title that exactly matches, will be 0
+     2. returns the id of the match that is the lowest, which will most
+        definitely match anything that exactly matches the title of the
+        filename */
+int Database::GetAnimeFromTitle(const std::string& title) {
 	if (title.empty())
 		return 0;
+	std::unordered_map<int, long long> map;
 	for (const auto& a : items) {
-		if (a.second.GetUserPreferredTitle().find(title) != std::string::npos)
-			return a.second.GetId();
-		for (const auto& t : a.second.GetTitleSynonyms()) {
-			if (t.find(title) != std::string::npos) {
-				return a.second.GetId();
+		long long ret = a.second.GetUserPreferredTitle().find(title);
+		if (ret != static_cast<long long>(std::string::npos)) {
+			map[a.second.GetId()] = a.second.GetUserPreferredTitle().length() - (ret + title.length());
+			continue;
+		}
+		for (const auto& synonym : a.second.GetTitleSynonyms()) {
+			ret = synonym.find(title);
+			if (ret != static_cast<long long>(std::string::npos)) {
+				map[a.second.GetId()] = synonym.length() - (ret + title.length());
+				continue;
 			}
 		}
 	}
-	return 0;
+	return get_lowest_in_map(map);
 }
 
 Database db;
--- a/src/core/json.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/core/json.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -2,28 +2,28 @@
 
 namespace JSON {
 
-std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, std::string def) {
+std::string GetString(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, std::string def) {
 	if (json.contains(ptr) && json[ptr].is_string())
 		return json[ptr].get<std::string>();
 	else
 		return def;
 }
 
-int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, int def) {
+int GetInt(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, int def) {
 	if (json.contains(ptr) && json[ptr].is_number())
 		return json[ptr].get<int>();
 	else
 		return def;
 }
 
-bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, bool def) {
+bool GetBoolean(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, bool def) {
 	if (json.contains(ptr) && json[ptr].is_boolean())
 		return json[ptr].get<bool>();
 	else
 		return def;
 }
 
-double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, double def) {
+double GetDouble(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, double def) {
 	if (json.contains(ptr) && json[ptr].is_number())
 		return json[ptr].get<double>();
 	else
--- a/src/gui/dialog/information.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/dialog/information.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -1,7 +1,6 @@
 #include "gui/dialog/information.h"
 #include "core/anime.h"
 #include "core/anime_db.h"
-#include "core/array.h"
 #include "core/strings.h"
 #include "gui/pages/anime_list.h"
 #include "gui/translate/anime.h"
@@ -26,18 +25,17 @@
 
 /* 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. */
-void InformationDialog::SaveData() {
-	Anime::Anime& anime = Anime::db.items[id];
-	anime.SetUserProgress(progress);
-	anime.SetUserScore(score);
-	anime.SetUserIsRewatching(rewatching);
-	anime.SetUserStatus(status);
-	anime.SetUserNotes(notes);
-	anime.SetUserDateStarted(started);
-	anime.SetUserDateCompleted(completed);
+void InformationDialog::SaveData(Anime::Anime& anime) {
+	anime.SetUserProgress(_progress);
+	anime.SetUserScore(_score);
+	anime.SetUserIsRewatching(_rewatching);
+	anime.SetUserStatus(_status);
+	anime.SetUserNotes(_notes);
+	anime.SetUserDateStarted(_started);
+	anime.SetUserDateCompleted(_completed);
 }
 
-InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
+InformationDialog::InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
     : QDialog(parent) {
 	setFixedSize(842, 613);
 	setWindowTitle(tr("Anime Information"));
@@ -54,7 +52,7 @@
 	/* "sidebar", includes... just the anime image :) */
 	QWidget* sidebar = new QWidget(widget);
 	QVBoxLayout* sidebar_layout = new QVBoxLayout(sidebar);
-	Poster* poster = new Poster(anime.GetId(), sidebar);
+	Poster* poster = new Poster(anime, sidebar);
 	sidebar_layout->addWidget(poster);
 	sidebar_layout->setContentsMargins(0, 0, 0, 0);
 	sidebar_layout->addStretch();
@@ -64,7 +62,6 @@
 
 	main_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 
-	id = anime.GetId();
 	/* anime title header text */
 	TextWidgets::Title* anime_title =
 	    new TextWidgets::Title(Strings::ToQString(anime.GetUserPreferredTitle()), main_widget);
@@ -131,10 +128,10 @@
 			subsection_layout->addWidget(new QLabel(tr("Episodes watched:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
-			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { progress = i; });
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { _progress = i; });
 			spin_box->setRange(0, anime.GetEpisodes());
 			spin_box->setSingleStep(1);
-			spin_box->setValue(progress = anime.GetUserProgress());
+			spin_box->setValue(_progress = anime.GetUserProgress());
 			subsection_layout->addWidget(spin_box);
 		});
 		CREATE_SUBSECTION({
@@ -142,8 +139,8 @@
 
 			QCheckBox* checkbox = new QCheckBox(tr("Rewatching"));
 			connect(checkbox, QOverload<int>::of(&QCheckBox::stateChanged), this,
-			        [this](int state) { rewatching = (state == Qt::Checked); });
-			checkbox->setCheckState(anime.GetUserIsRewatching() ? Qt::Checked : Qt::Unchecked);
+			        [this](int state) { _rewatching = (state == Qt::Checked); });
+			checkbox->setCheckState((_rewatching = anime.GetUserIsRewatching()) ? Qt::Checked : Qt::Unchecked);
 			subsection_layout->addWidget(checkbox);
 		});
 	});
@@ -153,24 +150,24 @@
 			subsection_layout->addWidget(new QLabel(tr("Status:"), subsection));
 
 			QStringList string_list;
-			for (unsigned int i = 0; i < ARRAYSIZE(Anime::ListStatuses); i++)
+			for (unsigned int i = 0; i < Anime::ListStatuses.size(); i++)
 				string_list.append(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])));
 
 			QComboBox* combo_box = new QComboBox(subsection);
 			combo_box->addItems(string_list);
 			connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
-			        [this](int i) { status = Anime::ListStatuses[i]; });
-			combo_box->setCurrentIndex(static_cast<int>(status = anime.GetUserStatus()) - 1);
+			        [this](int i) { _status = Anime::ListStatuses[i]; });
+			combo_box->setCurrentIndex(static_cast<int>(_status = anime.GetUserStatus()) - 1);
 			subsection_layout->addWidget(combo_box);
 		});
 		CREATE_SUBSECTION({
 			subsection_layout->addWidget(new QLabel(tr("Score:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
-			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { score = i; });
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) { _score = i; });
 			spin_box->setRange(0, 100);
 			spin_box->setSingleStep(5);
-			spin_box->setValue(score = anime.GetUserScore());
+			spin_box->setValue(_score = anime.GetUserScore());
 			subsection_layout->addWidget(spin_box);
 		});
 	});
@@ -182,9 +179,9 @@
 			QLineEdit* line_edit = new QLineEdit(subsection);
 			connect(line_edit, &QLineEdit::textChanged, this, [this](const QString& text) {
 				/* this sucks but I don't really want to implement anything smarter :) */
-				notes = Strings::ToUtf8String(text);
+				_notes = Strings::ToUtf8String(text);
 			});
-			line_edit->setText(Strings::ToQString(notes = anime.GetUserNotes()));
+			line_edit->setText(Strings::ToQString(_notes = anime.GetUserNotes()));
 			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
 			subsection_layout->addWidget(line_edit);
 		});
@@ -196,13 +193,13 @@
 
 			OptionalDate* date = new OptionalDate(true, subsection);
 			connect(date, &OptionalDate::DataChanged, this,
-			        [this](bool enabled, Date date) { started = (enabled) ? date : Date(); });
-			started = anime.GetUserDateStarted();
-			if (!started.IsValid()) {
+			        [this](bool enabled, Date date) { _started = (enabled) ? date : Date(); });
+			_started = anime.GetUserDateStarted();
+			if (!_started.IsValid()) {
 				date->SetEnabled(false);
-				started = anime.GetAirDate();
+				_started = anime.GetAirDate();
 			}
-			date->SetDate(started);
+			date->SetDate(_started);
 			subsection_layout->addWidget(date);
 		});
 		CREATE_SUBSECTION({
@@ -210,13 +207,13 @@
 
 			OptionalDate* date = new OptionalDate(true, subsection);
 			connect(date, &OptionalDate::DataChanged, this,
-			        [this](bool enabled, Date date) { completed = (enabled) ? date : Date(); });
-			completed = anime.GetUserDateCompleted();
-			if (!completed.IsValid()) {
+			        [this](bool enabled, Date date) { _completed = (enabled) ? date : Date(); });
+			_completed = anime.GetUserDateCompleted();
+			if (!_completed.IsValid()) {
 				date->SetEnabled(false);
-				completed = anime.GetAirDate();
+				_completed = anime.GetAirDate();
 			}
-			date->SetDate(completed);
+			date->SetDate(_completed);
 			subsection_layout->addWidget(date);
 		});
 	});
@@ -266,8 +263,8 @@
 	layout->setSpacing(12);
 
 	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
-	connect(button_box, &QDialogButtonBox::accepted, this, [this, accept] {
-		SaveData();
+	connect(button_box, &QDialogButtonBox::accepted, this, [this, accept, &anime] {
+		SaveData(anime);
 		accept();
 		QDialog::accept();
 	});
--- a/src/gui/pages/anime_list.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/pages/anime_list.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -11,7 +11,6 @@
 #include "gui/pages/anime_list.h"
 #include "core/anime.h"
 #include "core/anime_db.h"
-#include "core/array.h"
 #include "core/session.h"
 #include "core/strings.h"
 #include "core/time.h"
@@ -436,7 +435,7 @@
 	tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
 	tree_view->setFrameShape(QFrame::NoFrame);
 
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) {
+	for (unsigned int i = 0; i < sort_models.size(); i++) {
 		tab_bar->addTab(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
 		                QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 		sort_models[i] = new AnimeListPageSortFilter(tree_view);
@@ -476,12 +475,12 @@
 }
 
 void AnimeListPage::RefreshList() {
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+	for (unsigned int i = 0; i < sort_models.size(); i++)
 		reinterpret_cast<AnimeListPageModel*>(sort_models[i]->sourceModel())->RefreshList();
 }
 
 void AnimeListPage::RefreshTabs() {
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+	for (unsigned int i = 0; i < sort_models.size(); i++)
 		tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
 		                           QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 }
@@ -497,7 +496,7 @@
 void AnimeListPage::Reset() {
 	while (tab_bar->count())
 		tab_bar->removeTab(0);
-	for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
+	for (unsigned int i = 0; i < sort_models.size(); i++)
 		delete sort_models[i];
 }
 
--- a/src/gui/pages/now_playing.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/pages/now_playing.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -3,8 +3,10 @@
 #include "core/strings.h"
 #include "gui/widgets/anime_info.h"
 #include "gui/widgets/text.h"
+#include "gui/widgets/poster.h"
 #include <QLabel>
 #include <QStackedWidget>
+#include <QHBoxLayout>
 #include <QVBoxLayout>
 #include <QWidget>
 
@@ -27,14 +29,17 @@
 
 	public:
 		Playing(QWidget* parent = nullptr);
-		void SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info);
+		void SetPlayingAnime(const Anime::Anime& anime, const std::unordered_map<std::string, std::string>& info);
 		int GetPlayingAnime();
 
 	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;
 };
 
 Default::Default(QWidget* parent) : QWidget(parent) {
@@ -50,12 +55,32 @@
 Playing::Playing(QWidget* parent) : QWidget(parent) {
 	QHBoxLayout* layout = new QHBoxLayout(this);
 
-	_title.reset(new TextWidgets::Title("\n", this));
-	layout->addWidget(_title.get());
+	_main.reset(new QWidget(this));
+	_main->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+	QVBoxLayout* main_layout = new QVBoxLayout(_main.get());
+	main_layout->setContentsMargins(0, 0, 0, 0);
+
+	_title.reset(new TextWidgets::Title("", _main.get()));
+	main_layout->addWidget(_title.get());
+
+	_info.reset(new AnimeInfoWidget(_main.get()));
+	_info->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+	main_layout->addWidget(_info.get());
 
-	_info.reset(new AnimeInfoWidget(this));
-	layout->addWidget(_info.get());
+	/* "sidebar", includes... just the anime image :) */
+	_sidebar.reset(new QWidget(this));
+	QVBoxLayout* sidebar_layout = new QVBoxLayout(_sidebar.get());
+	sidebar_layout->setContentsMargins(0, 0, 0, 0);
 
+	_poster.reset(new Poster(_sidebar.get()));
+	sidebar_layout->addWidget(_poster.get());
+
+	sidebar_layout->addStretch();
+
+	layout->addWidget(_sidebar.get());
+	layout->addWidget(_main.get());
+	layout->setSpacing(10);
 	layout->setContentsMargins(0, 0, 0, 0);
 }
 
@@ -63,14 +88,20 @@
 	return _id;
 }
 
-void Playing::SetPlayingAnime(int id, const std::unordered_map<std::string, std::string>& info) {
-	if (id == _id || id <= 0)
+void Playing::SetPlayingAnime(const Anime::Anime& anime, const std::unordered_map<std::string, std::string>& info) {
+	if (_id == anime.GetId() && std::to_string(_episode) == info.at("episode"))
 		return;
-	if (Anime::db.items.find(id) != Anime::db.items.end()) {
-		const Anime::Anime& anime = Anime::db.items[_id = id];
-		_title->setText(Strings::ToQString(anime.GetUserPreferredTitle()));
-		_info->SetAnime(anime);
+	_id = anime.GetId();
+	try {
+		_episode = std::stoi(info.at("episode"));
+	} catch (std::invalid_argument) {
+		_episode = 0;
 	}
+	_title->SetText(Strings::ToQString(anime.GetUserPreferredTitle()));
+	_info->SetAnime(anime);
+	_poster->SetAnime(anime);
+
+	updateGeometry();
 }
 
 } // namespace NowPlayingPages
@@ -102,9 +133,10 @@
 	return reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->GetPlayingAnime();
 }
 
-void NowPlayingPage::SetPlaying(int id, const std::unordered_map<std::string, std::string>& info) {
-	reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->SetPlayingAnime(id, info);
+void NowPlayingPage::SetPlaying(const Anime::Anime& anime, const std::unordered_map<std::string, std::string>& info) {
+	reinterpret_cast<NowPlayingPages::Playing*>(stack->widget(1))->SetPlayingAnime(anime, info);
 	stack->setCurrentIndex(1);
+	updateGeometry();
 }
 
 #include "gui/pages/moc_now_playing.cpp"
--- a/src/gui/pages/statistics.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/pages/statistics.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -25,7 +25,7 @@
 	TextWidgets::LabelledSection* anime_list_paragraph = new TextWidgets::LabelledSection(
 	    tr("Anime list"),
 	    tr("Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:"),
-	    "\n\n\n\n\n\n", this);
+	    "", this);
 	anime_list_data = anime_list_paragraph->GetParagraph();
 
 	TextWidgets::LabelledSection* application_paragraph =
--- a/src/gui/widgets/anime_info.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/widgets/anime_info.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -26,7 +26,7 @@
 
 void AnimeInfoWidget::SetAnime(const Anime::Anime& anime) {
 	/* alt titles */
-	_title->GetLine()->setText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	_title->GetLine()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
 
 	/* details */
 	QString details_data;
@@ -37,9 +37,11 @@
 	               << Translate::ToString(anime.GetSeason()).c_str() << " " << anime.GetAirDate().GetYear() << "\n"
 	               << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
 	               << anime.GetAudienceScore() << "%";
-	_details->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	_details->GetParagraph()->SetText(details_data);
 
-	_synopsis->GetParagraph()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", ")));
+	_synopsis->GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis()));
+
+	updateGeometry();
 }
 
 #include "gui/widgets/moc_anime_info.cpp"
--- a/src/gui/widgets/poster.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/widgets/poster.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -16,7 +16,7 @@
 #include <QUrl>
 #include <curl/curl.h>
 
-Poster::Poster(int id, QWidget* parent) : QFrame(parent) {
+Poster::Poster(QWidget* parent) : QFrame(parent) {
 	QHBoxLayout* layout = new QHBoxLayout(this);
 	layout->setContentsMargins(1, 1, 1, 1);
 
@@ -25,23 +25,27 @@
 	setFrameShape(QFrame::Box);
 	setFrameShadow(QFrame::Plain);
 
-	const Anime::Anime& anime = Anime::db.items[id];
+	label = new ClickableLabel(this);
+	label->setAlignment(Qt::AlignCenter);
+	layout->addWidget(label);
+}
 
+Poster::Poster(const Anime::Anime& anime, QWidget* parent) : Poster(parent) {
+	SetAnime(anime);
+}
+
+void Poster::SetAnime(const Anime::Anime& anime) {
 	QThreadPool::globalInstance()->start([this, anime] {
 		QByteArray ba = HTTP::Get(anime.GetPosterUrl(), {});
 		ImageDownloadFinished(ba);
 	});
 
-	QPixmap pixmap = QPixmap::fromImage(img);
-
-	label = new ClickableLabel(this);
-	label->setAlignment(Qt::AlignCenter);
+	label->disconnect();
 	connect(label, &ClickableLabel::clicked, this,
 	        [anime] { QDesktopServices::openUrl(Strings::ToQString(anime.GetServiceUrl())); });
-	layout->addWidget(label);
 }
 
-void Poster::ImageDownloadFinished(QByteArray arr) {
+void Poster::ImageDownloadFinished(const QByteArray& arr) {
 	img.loadFromData(arr);
 	RenderToLabel();
 }
--- a/src/gui/widgets/text.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/widgets/text.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -8,7 +8,7 @@
 
 namespace TextWidgets {
 
-Header::Header(QString title, QWidget* parent) : QWidget(parent) {
+Header::Header(const QString& title, QWidget* parent) : QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
@@ -29,12 +29,13 @@
 	layout->setContentsMargins(0, 0, 0, 0);
 }
 
-void Header::SetText(QString text) {
+void Header::SetText(const QString& text) {
 	static_text_title->setText(text);
+	updateGeometry();
 }
 
 /* inherits QPlainTextEdit and gives a much more reasonable minimum size */
-Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
+Paragraph::Paragraph(const QString& text, QWidget* parent) : QPlainTextEdit(text, parent) {
 	setTextInteractionFlags(Qt::TextBrowserInteraction);
 	setFrameShape(QFrame::NoFrame);
 	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -47,11 +48,12 @@
 	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
 }
 
-void Paragraph::SetText(QString text) {
+void Paragraph::SetText(const QString& text) {
 	QTextDocument* document = new QTextDocument(this);
 	document->setDocumentLayout(new QPlainTextDocumentLayout(document));
 	document->setPlainText(text);
 	setDocument(document);
+	updateGeometry();
 }
 
 /* highly based upon... some stackoverflow answer for PyQt */
@@ -72,10 +74,9 @@
 /* Equivalent to Paragraph(), but is only capable of showing one line. Only
    exists because sometimes with SelectableSection it will let you go
    out of bounds */
-Line::Line(QString text, QWidget* parent) : QLineEdit(text, parent) {
+Line::Line(QWidget* parent) : QLineEdit(parent) {
 	setFrame(false);
 	setReadOnly(true);
-	setCursorPosition(0); /* displays left text first */
 
 	QPalette pal;
 	pal.setColor(QPalette::Window, Qt::transparent);
@@ -84,7 +85,16 @@
 	setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
 }
 
-Title::Title(QString title, QWidget* parent) : Line(title, parent) {
+Line::Line(const QString& text, QWidget* parent) : Line(parent) {
+	SetText(text);
+}
+
+void Line::SetText(const QString& text) {
+	setText(text);
+	setCursorPosition(0); /* displays left text first */
+}
+
+Title::Title(const QString& title, QWidget* parent) : Line(title, parent) {
 	QFont fnt(font());
 	fnt.setPixelSize(16);
 	setFont(fnt);
@@ -95,7 +105,7 @@
 	setPalette(pal);
 }
 
-Section::Section(QString title, QString data, QWidget* parent) : QWidget(parent) {
+Section::Section(const QString& title, const QString& data, QWidget* parent) : QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
 	header = new Header(title, this);
@@ -127,7 +137,7 @@
 	return paragraph;
 }
 
-LabelledSection::LabelledSection(QString title, QString label, QString data, QWidget* parent) : QWidget(parent) {
+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);
@@ -135,18 +145,19 @@
 	// 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);
+	content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
 
 	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->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
 
 	paragraph = new Paragraph(data, this);
 	paragraph->setTextInteractionFlags(Qt::NoTextInteraction);
 	paragraph->setAttribute(Qt::WidgetAttribute::WA_TransparentForMouseEvents);
 	paragraph->setWordWrapMode(QTextOption::NoWrap);
+	paragraph->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
 
 	QHBoxLayout* content_layout = new QHBoxLayout(content);
 	content_layout->addWidget(labels, 0, Qt::AlignTop);
@@ -174,7 +185,7 @@
 	return paragraph;
 }
 
-SelectableSection::SelectableSection(QString title, QString data, QWidget* parent) : QWidget(parent) {
+SelectableSection::SelectableSection(const QString& title, const QString& data, QWidget* parent) : QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
 	header = new Header(title, this);
@@ -203,7 +214,7 @@
 	return paragraph;
 }
 
-OneLineSection::OneLineSection(QString title, QString text, QWidget* parent) : QWidget(parent) {
+OneLineSection::OneLineSection(const QString& title, const QString& text, QWidget* parent) : QWidget(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 
 	header = new Header(title, this);
--- a/src/gui/window.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/gui/window.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -98,12 +98,12 @@
 		Filesystem::Path p = Track::Media::GetCurrentPlaying();
 		std::unordered_map<std::string, std::string> elements = Track::Media::GetFileElements(p);
 		int id = Anime::db.GetAnimeFromTitle(elements["title"]);
-		if (id == 0) {
+		if (id <= 0) {
 			page->SetDefault();
 			return;
 		}
 
-		page->SetPlaying(id, elements);
+		page->SetPlaying(Anime::db.items[id], elements);
 	});
 	timer->start(5000);
 
--- a/src/track/constants.cc	Mon Oct 23 13:37:42 2023 -0400
+++ b/src/track/constants.cc	Tue Oct 24 22:01:02 2023 -0400
@@ -1,5 +1,7 @@
 #include "track/constants.h"
 
+// clang-format off
+// https://github.com/llvm/llvm-project/issues/62676
 const std::vector<std::string> media_extensions = {
     "avi",
     "asf",
@@ -12,8 +14,9 @@
 #ifdef MACOSX
     "VLC"
 #elif WIN32
-    "vlc.exe", "mpc-hc.exe", "mpc-hc64.exe", "wmplayer.exe"
+    "vlc.exe", "mpc-hc.exe", "mpc-hc64.exe", "wmplayer.exe", "mpv.exe"
 #else // linux, unix, whatevs
     "vlc", "mpv", "mpc-qt"
 #endif
 };
+// clang-format on