changeset 365:f81bed4e04ac

*: megacommit that probably breaks things
author Paper <paper@paper.us.eu.org>
date Wed, 02 Oct 2024 23:06:43 -0400
parents 99c961c91809
children 886f66775f31
files dep/animone/src/fd/proc.cc dep/animone/src/fd/xnu.cc include/gui/widgets/anime_button.h include/gui/widgets/graph.h include/gui/widgets/text.h src/core/strings.cc src/gui/dialog/licenses.cc src/gui/pages/seasons.cc src/gui/widgets/anime_button.cc src/gui/widgets/text.cc src/services/anilist.cc
diffstat 11 files changed, 271 insertions(+), 183 deletions(-) [+]
line wrap: on
line diff
--- a/dep/animone/src/fd/proc.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/dep/animone/src/fd/proc.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -21,7 +21,34 @@
 
 namespace animone::internal::proc {
 
-static bool IsRegularFile(std::string link) {
+struct Fdinfo {
+	bool Parse(std::istream& istr);
+	std::unordered_map<std::string, std::string> data;
+};
+
+bool Fdinfo::Parse(std::istream& istr) {
+	/* shift to the start of the stream */
+	istr.seekg(0);
+
+	for (std::string line; std::getline(istr, line); ) {
+		if (line.empty()) /* huh? */
+			continue;
+
+		std::size_t colon = line.find(':');
+		if (colon == std::string::npos)
+			return false;
+
+		std::string key = line.substr(0, colon);
+		std::string value = line.substr(colon + 1);
+		util::TrimLeft(value, " ");
+
+		data[key] = value;
+	}
+
+	return true;
+}
+
+static bool IsRegularFile(const std::string& link) {
 	struct stat sb;
 	if (stat(link.c_str(), &sb) == -1)
 		return false;
@@ -37,15 +64,22 @@
 	if (!file)
 		return false;
 
-	int flags = 0;
-	for (std::string line; std::getline(file, line);)
-		if (line.find("flags:", 0) == 0)
-			flags = util::StringToInt(line.substr(line.find_last_not_of("0123456789") + 1));
+	Fdinfo fdinfo;
+	if (!fdinfo.Parse(file))
+		return false;
+
+	if (fdinfo.data.find("flags") == fdinfo.data.end())
+		return false;
 
 	/* check if the file was opened in a write mode */
-	int accflags = flags & O_ACCMODE;
-	if (accflags == O_WRONLY || accflags == O_RDWR)
-		return false;
+	try {
+		const long long accflags = std::stoll(fdinfo.data["flags"]) & O_ACCMODE;
+
+		if (accflags == O_WRONLY || accflags == O_RDWR)
+			return false;
+	} catch (const std::exception& ex) {
+		return false; /* huh ? */
+	}
 
 	return true;
 }
--- a/dep/animone/src/fd/xnu.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/dep/animone/src/fd/xnu.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -35,21 +35,28 @@
 namespace animone::internal::xnu {
 
 bool EnumerateOpenProcesses(process_proc_t process_proc) {
-	size_t pids_size = 256;
-	std::unique_ptr<pid_t[]> pids;
+	/* pre-allocate 256 pids */
+	std::vector<pid_t> pids(256);
+	int returned_size_bytes = 0;
+
+	for (;;) {
+		returned_size_bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), pids.size() * sizeof(pid_t));
+		if (returned_size_bytes <= 0) /* probably an error ? */
+			return false;
 
-	int returned_size = 0;
-	do {
-		pids.reset(new pid_t[pids_size *= 2]);
-		returned_size = proc_listpids(PROC_ALL_PIDS, 0, pids.get(), pids_size * sizeof(pid_t));
-		if (returned_size == -1)
-			return false;
-	} while ((pids_size * sizeof(size_t)) < returned_size);
+		/* break out of the loop if we have everything */
+		if ((pids.size() * sizeof(pid_t)) > returned_size_bytes)
+			break;
+
+		pids.resize(pids.size() * 2);
+	}
 
-	for (int i = 0; i < pids_size; i++) {
+	pids.resize(returned_size_bytes);
+
+	for (const auto& pid : pids) {
 		std::string result;
-		GetProcessName(pids[i], result);
-		if (!process_proc({.platform = ExecutablePlatform::Xnu, .pid = pids[i], .comm = result}))
+		GetProcessName(pid, result);
+		if (!process_proc({.platform = ExecutablePlatform::Xnu, .pid = pid, .comm = result}))
 			return false;
 	}
 
@@ -61,36 +68,41 @@
 		return false;
 
 	for (const auto& pid : pids) {
-		const int bufsz = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0);
-		if (bufsz < 0)
-			return false;
+		/* most processes probably don't even have that many files opened! */
+		std::vector<struct proc_fdinfo> fds(4);
+		int returned_size_bytes = 0;
+
+		for (;;) {
+			returned_size_bytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds.data(), fds.size());
+			if (returned_size_bytes <= 0) /* probably an error ? */
+				return false;
 
-		const size_t info_len = bufsz / sizeof(struct proc_fdinfo);
-		if (info_len < 1)
-			return false;
+			/* break out of the loop if we have everything */
+			if ((fds.size() * sizeof(struct proc_fdinfo)) > returned_size_bytes)
+				break;
 
-		std::unique_ptr<struct proc_fdinfo[]> info(new struct proc_fdinfo[info_len]);
-		if (!info)
-			return false;
+			fds.resize(fds.size() * 2);
+		}
 
-		proc_pidinfo(pid, PROC_PIDLISTFDS, 0, info.get(), bufsz);
+		fds.resize(returned_size_bytes / sizeof(struct proc_fdinfo));
 
-		for (size_t i = 0; i < info_len; i++) {
-			if (info[i].proc_fdtype == PROX_FDTYPE_VNODE) {
-				struct vnode_fdinfowithpath vnodeInfo;
+		for (const auto& fd : fds) {
+			if (fd.proc_fdtype != PROX_FDTYPE_VNODE)
+				continue;
+
+			struct vnode_fdinfowithpath vnodeInfo;
 
-				int sz = proc_pidfdinfo(pid, info[i].proc_fd, PROC_PIDFDVNODEPATHINFO, &vnodeInfo,
-				                        PROC_PIDFDVNODEPATHINFO_SIZE);
-				if (sz != PROC_PIDFDVNODEPATHINFO_SIZE)
-					return false;
+			int sz = proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDVNODEPATHINFO, &vnodeInfo,
+			                        PROC_PIDFDVNODEPATHINFO_SIZE);
+			if (sz != PROC_PIDFDVNODEPATHINFO_SIZE)
+				return false;
 
-				/* why would a media player open a file in write mode? */
-				if (vnodeInfo.pfi.fi_openflags & FWRITE)
-					continue;
+			/* why would a media player open a file in write mode? */
+			if (vnodeInfo.pfi.fi_openflags & FWRITE)
+				continue;
 
-				if (!open_file_proc({pid, vnodeInfo.pvip.vip_path}))
-					return false;
-			}
+			if (!open_file_proc({pid, vnodeInfo.pvip.vip_path}))
+				return false;
 		}
 	}
 
@@ -100,7 +112,7 @@
 static bool GetProcessNameFromProcPidPath(pid_t pid, std::string& result) {
 	result.assign(PROC_PIDPATHINFO_MAXSIZE, '\0');
 
-	int ret = proc_pidpath(pid, result.data(), result.size() * sizeof(char));
+	int ret = proc_pidpath(pid, result.data(), result.size());
 	if (ret <= 0)
 		return false;
 
@@ -116,7 +128,7 @@
 static bool GetProcessNameFromProcName(pid_t pid, std::string& result) {
 	result.assign(2 * MAXCOMLEN, '\0');
 
-	int size = proc_name(pid, &result.front(), result.length());
+	int size = proc_name(pid, result.data(), result.length());
 
 	/* if size is MAXCOMLEN or 2 * MAXCOMLEN, assume
 	 * this method won't work and our result is truncated */
--- a/include/gui/widgets/anime_button.h	Tue Jul 16 21:15:59 2024 -0400
+++ b/include/gui/widgets/anime_button.h	Wed Oct 02 23:06:43 2024 -0400
@@ -13,7 +13,7 @@
 class Anime;
 }
 
-class AnimeButton : public QFrame {
+class AnimeButton final : public QFrame {
 	Q_OBJECT
 
 public:
@@ -21,6 +21,9 @@
 	AnimeButton(const Anime::Anime& anime, QWidget* parent = nullptr);
 	void SetAnime(const Anime::Anime& anime);
 
+	bool hasHeightForWidth() const override;
+	int heightForWidth(int w) const override;
+
 protected:
 	Poster _poster;
 	QLabel _title;
--- a/include/gui/widgets/graph.h	Tue Jul 16 21:15:59 2024 -0400
+++ b/include/gui/widgets/graph.h	Wed Oct 02 23:06:43 2024 -0400
@@ -103,6 +103,8 @@
 
 			/* only draw this if we actually have any data */
 			if (total) {
+				const int rect_width = (static_cast<double>(value) / total) * (width - offset - HORIZ_SPACING - value_width);
+
 				painter.save();
 
 				QPen pen(painter.pen());
@@ -110,12 +112,11 @@
 				painter.setPen(pen);
 
 				QPainterPath path;
-				path.addRect(x + offset, y,
-				             (static_cast<double>(value) / total) * (width - offset - HORIZ_SPACING - value_width), each_height);
+				path.addRect(x + offset, y, rect_width, each_height);
 				painter.fillPath(path, Qt::darkGreen);
 				painter.drawPath(path);
 
-				offset += (static_cast<double>(value) / total) * (width - offset - HORIZ_SPACING - value_width);
+				offset += rect_width;
 
 				painter.restore();
 			}
--- a/include/gui/widgets/text.h	Tue Jul 16 21:15:59 2024 -0400
+++ b/include/gui/widgets/text.h	Wed Oct 02 23:06:43 2024 -0400
@@ -30,6 +30,21 @@
 	QPointer<QFrame> separator_;
 };
 
+class Label final : public QLabel {
+	Q_OBJECT
+
+public:
+	Label(QWidget *parent = nullptr);
+	Label(const QString &string, QWidget *parent = nullptr);
+	void SetElidingMode(bool elide);
+
+protected:
+	void paintEvent(QPaintEvent *event) override;
+
+private:
+	bool elide_;
+};
+
 /* This is a nice clean wrapper around Label suitable for our needs. */
 class Paragraph : public QWidget {
 	Q_OBJECT
@@ -65,7 +80,7 @@
 	QPointer<QWidget> contents_;
 	QPointer<QGridLayout> contents_layout_;
 
-	std::vector<std::pair<QSharedPointer<QLabel>, QSharedPointer<QLabel>>> data_;
+	std::vector<std::pair<QSharedPointer<Label>, QSharedPointer<Label>>> data_;
 };
 
 /* this is just a generic QLabel with a specific font and foreground role,
--- a/src/core/strings.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/core/strings.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -82,9 +82,9 @@
 /* this also performs case folding, so our string is lowercase after this */
 void NormalizeUnicode(std::string& string) {
 	static constexpr utf8proc_option_t options = static_cast<utf8proc_option_t>(
-		UTF8PROC_COMPAT | UTF8PROC_COMPOSE | UTF8PROC_STABLE |
-		UTF8PROC_IGNORE | UTF8PROC_STRIPCC | UTF8PROC_STRIPMARK |
-		UTF8PROC_LUMP | UTF8PROC_CASEFOLD | UTF8PROC_NLF2LS
+		UTF8PROC_COMPAT | UTF8PROC_COMPOSE  | UTF8PROC_STABLE |
+		UTF8PROC_IGNORE | UTF8PROC_STRIPCC  | UTF8PROC_STRIPMARK |
+		UTF8PROC_LUMP   | UTF8PROC_CASEFOLD | UTF8PROC_NLF2LS
 	);
 
 	/* ack */
@@ -97,11 +97,12 @@
 		options
 	);
 
-	if (size)
-		string = std::string(reinterpret_cast<const char*>(buf), size);
+	if (buf) {
+		if (size)
+			string.assign(reinterpret_cast<const char*>(buf), size);
 
-	if (buf)
-		free(buf);
+		std::free(buf);
+	}
 }
 
 void NormalizeAnimeTitle(std::string& string) {
@@ -190,14 +191,20 @@
 	return def;
 }
 
+template<typename T>
+constexpr T ipow(T num, unsigned int pow) {
+    return (pow >= sizeof(unsigned int)*8) ? 0 :
+        pow == 0 ? 1 : num * ipow(num, pow-1);
+}
+
 /* util funcs */
 uint64_t HumanReadableSizeToBytes(const std::string& str) {
 	static const std::unordered_map<std::string, uint64_t> bytes_map = {
-		{"KB", 1000ull},
-		{"MB", 1000000ull},
-		{"GB", 1000000000ull},
-		{"TB", 1000000000000ull},
-		{"PB", 1000000000000000ull},
+		{"KB",  1e3},
+		{"MB",  1e6},
+		{"GB",  1e9},
+		{"TB",  1e12},
+		{"PB",  1e15},
 	    {"KiB", 1ull << 10},
 	    {"MiB", 1ull << 20},
 	    {"GiB", 1ull << 30},
--- a/src/gui/dialog/licenses.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/gui/dialog/licenses.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -24,14 +24,49 @@
 #	include "sys/win32/dark_theme.h"
 #endif
 
-static QWidget *create_license_widget(QWidget *parent, const std::string& license) {
+static QWidget *create_license_widget(QWidget *parent, const QString& license) {
 	QTextBrowser* paragraph = new QTextBrowser(parent);
 	paragraph->setFrameShape(QFrame::NoFrame);
-	paragraph->setPlainText(Strings::ToQString(license));
+	paragraph->setPlainText(license);
 	paragraph->setFont(QFont("monospace"));
 	return paragraph;
 }
 
+static void create_basic_license(QTabWidget *tab_widget, const QString& filename, const QString& title) {
+	QFile f(filename);
+	if (!f.exists())
+		return;
+
+	f.open(QFile::ReadOnly | QFile::Text);
+	tab_widget->addTab(create_license_widget(tab_widget, f.readAll()), title);
+}
+
+static void create_dual_license(QTabWidget *tab_widget, const QString& filename1, const QString& title1, const QString& filename2, const QString& title2) {
+	QString l1, l2;
+	{
+		QFile f1(filename1), f2(filename2);
+		if (!f1.exists() || !f2.exists())
+			return;
+
+		f1.open(QFile::ReadOnly | QFile::Text);
+		f2.open(QFile::ReadOnly | QFile::Text);
+
+		l1 = f1.readAll();
+		l2 = f2.readAll();
+	}
+
+	QWidget *dual = new QWidget(tab_widget);
+	QVBoxLayout *dual_layout = new QVBoxLayout(dual);
+
+	QLabel *dual_notice = new QLabel(QCoreApplication::tr("%1 was originally forked from %2, where any changes divergent from %2 are now under a different license. Both the licenses for %1 and %2 are provided below, respectfully:").arg(title1, title2), dual);
+	dual_notice->setWordWrap(true);
+	dual_layout->addWidget(dual_notice);
+	dual_layout->addWidget(create_license_widget(dual, l1));
+	dual_layout->addWidget(create_license_widget(dual, l2));
+
+	tab_widget->addTab(dual, title1);
+}
+
 LicensesWindow::LicensesWindow(QWidget* parent) : QDialog(parent) {
 	resize(641, 500);
 	setWindowTitle(tr("About Minori"));
@@ -46,101 +81,17 @@
 
 	layout->addWidget(tab_widget);
 
-	do {
-		QFile f(":/licenses/LICENSE.minori");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("Minori"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.MIT.animone");
-		QFile b(":/licenses/LICENSE.BSD.animone");
-		if (!f.exists() || !b.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		b.open(QFile::ReadOnly | QFile::Text);
-
-		std::string mit = Strings::ToUtf8String(f.readAll());
-		std::string bsd = Strings::ToUtf8String(b.readAll());
+	create_basic_license(tab_widget, ":/licenses/LICENSE.minori", tr("Minori"));
 
-		QWidget *dual = new QWidget(this);
-		QVBoxLayout *dual_layout = new QVBoxLayout(dual);
-
-		QLabel *dual_notice = new QLabel(tr("Animone was originally forked from Anisthesia, where any changes divergent from Anisthesia are now under a different license. Both the licenses for Animone and Anisthesia are provided below, respectfully:"), dual);
-		dual_notice->setWordWrap(true);
-		dual_layout->addWidget(dual_notice);
-		dual_layout->addWidget(create_license_widget(dual, bsd));
-		dual_layout->addWidget(create_license_widget(dual, mit));
-
-		tab_widget->addTab(dual, tr("Animone"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.anitomy");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("Anitomy"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.fmt");
-		if (!f.exists())
-			break;
+	create_dual_license(tab_widget, ":/licenses/LICENSE.MIT.animone", tr("Animone"), ":/licenses/LICENSE.BSD.animone", tr("Anisthesia"));
 
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("fmt"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.nlohmann");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("JSON for Modern C++"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.pugixml");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("pugixml"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.semver");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("semver"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.toml11");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("toml11"));
-	} while (0);
-
-	do {
-		QFile f(":/licenses/LICENSE.utf8proc");
-		if (!f.exists())
-			break;
-
-		f.open(QFile::ReadOnly | QFile::Text);
-		tab_widget->addTab(create_license_widget(this, Strings::ToUtf8String(f.readAll())), tr("utf8proc"));
-	} while (0);
+	create_basic_license(tab_widget, ":/licenses/LICENSE.anitomy", tr("Anitomy"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.fmt", tr("fmt"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.nlohmann", tr("JSON for Modern C++"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.pugixml", tr("pugixml"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.semver", tr("semver"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.toml11", tr("toml11"));
+	create_basic_license(tab_widget, ":/licenses/LICENSE.utf8proc", tr("utf8proc"));
 }
 
 void LicensesWindow::showEvent(QShowEvent* event) {
--- a/src/gui/pages/seasons.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/gui/pages/seasons.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -67,7 +67,7 @@
 		QListWidgetItem* item = new QListWidgetItem;
 		AnimeButton* button = new AnimeButton(this);
 		button->SetAnime(Anime::db.items[id]);
-		item->setSizeHint(button->sizeHint());
+		item->setSizeHint(QSize(500, 200));
 		buttons->addItem(item);
 		buttons->setItemWidget(item, button);
 	}
@@ -232,6 +232,7 @@
 		buttons->setContentsMargins(4, 4, 4, 4);
 		buttons->setSpacing(2);
 		buttons->setResizeMode(QListView::Adjust);
+		buttons->setUniformItemSizes(true);
 
 		full_layout->addWidget(buttons);
 	}
--- a/src/gui/widgets/anime_button.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/gui/widgets/anime_button.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -35,8 +35,7 @@
 	QHBoxLayout* ly = new QHBoxLayout(this);
 
 	_poster.SetClickable(false);
-	_poster.setFixedSize(120, 170);
-	ly->addWidget(&_poster, 0, Qt::AlignTop);
+	ly->addWidget(&_poster, 1, Qt::AlignTop);
 
 	const std::vector<std::pair<std::string, std::string>> imap = {
 		{Strings::Translate("Aired:"), ""},
@@ -51,7 +50,6 @@
 
 	{
 		QWidget* misc_section = new QWidget(this);
-		misc_section->setFixedSize(354, 180);
 
 		QVBoxLayout* misc_layout = new QVBoxLayout(misc_section);
 		misc_layout->setContentsMargins(0, 0, 0, 0);
@@ -79,13 +77,12 @@
 			dummy_layout->setSpacing(0);
 			dummy_layout->setContentsMargins(0, 0, 0, 0);
 
-			_synopsis.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
 			_synopsis.SetSelectable(false);
 			dummy_layout->addWidget(&_synopsis);
 			misc_layout->addWidget(dummy);
 		}
 
-		ly->addWidget(misc_section, 0, Qt::AlignTop);
+		ly->addWidget(misc_section, 3, Qt::AlignTop);
 	}
 }
 
@@ -112,3 +109,11 @@
 
 	_synopsis.SetText(anime.GetSynopsis());
 }
+
+bool AnimeButton::hasHeightForWidth() const {
+	return true;
+}
+
+int AnimeButton::heightForWidth(int w) const {
+	return static_cast<int>(static_cast<double>(w) / 2.5);
+}
--- a/src/gui/widgets/text.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/gui/widgets/text.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -9,6 +9,7 @@
 #include <QVBoxLayout>
 #include <QScrollArea>
 #include <QDebug>
+#include <QPainter>
 
 namespace TextWidgets {
 
@@ -16,8 +17,8 @@
 
 Header::Header(QWidget* parent)
 	: QWidget(parent)
-	, title_(new QLabel(this))
-	, separator_(new QFrame(this)) {
+	, title_(new QLabel)
+	, separator_(new QFrame) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
 	setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
 
@@ -44,6 +45,69 @@
 	updateGeometry();
 }
 
+Label::Label(QWidget *parent) : QLabel(parent) {}
+Label::Label(const QString &string, QWidget *parent) : QLabel(string, parent) {}
+
+void Label::SetElidingMode(bool elide) {
+	elide_ = elide;
+	update();
+}
+
+void Label::paintEvent(QPaintEvent *event) {
+	if (elide_) {
+		/* bruh */
+		if (wordWrap()) {
+			QFrame::paintEvent(event);
+
+			const QString content = text();
+
+		    QPainter painter(this);
+		    QFontMetrics fontMetrics = painter.fontMetrics();
+
+		    bool didElide = false;
+		    int lineSpacing = fontMetrics.lineSpacing();
+		    int y = 0;
+
+		    QTextLayout textLayout(content, painter.font());
+		    textLayout.beginLayout();
+		    for (;;) {
+		        QTextLine line = textLayout.createLine();
+
+		        if (!line.isValid())
+		            break;
+
+		        line.setLineWidth(width());
+		        int nextLineY = y + lineSpacing;
+
+		        if (height() >= nextLineY + lineSpacing) {
+		            line.draw(&painter, QPoint(0, y));
+		            y = nextLineY;
+				} else {
+					QString lastLine = content.mid(line.textStart());
+					QString elidedLastLine = fontMetrics.elidedText(lastLine, Qt::ElideRight, width());
+					painter.drawText(QPoint(0, y + fontMetrics.ascent()), elidedLastLine);
+					line = textLayout.createLine();
+					didElide = line.isValid();
+					break;
+				}
+			}
+			textLayout.endLayout();
+		} else {
+			QString backup_text = QLabel::text();
+
+			QFontMetrics metric(fontMetrics());
+			QString elided_text = metric.elidedText(backup_text, Qt::ElideRight, width());
+
+			QLabel::setText(elided_text);
+			QLabel::paintEvent(event);
+			QLabel::setText(backup_text);
+		}
+	} else {
+		/* QLabel can handle everything... */
+		QLabel::paintEvent(event);
+	}
+}
+
 /* ---------------------------------------------------------------------------------- */
 /* "Paragraph" widgets, as in widgets meant to hold a bunch of text. */
 
@@ -111,8 +175,8 @@
 
 	data_.reserve(data.size());
 	for (std::size_t i = 0; i < data.size(); i++) {
-		QSharedPointer<QLabel> first(new QLabel);
-		QSharedPointer<QLabel> second(new QLabel);
+		QSharedPointer<Label> first(new Label);
+		QSharedPointer<Label> second(new Label);
 
 		first->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
 
@@ -121,7 +185,7 @@
 
 		data_.push_back({first, second});
 
-		contents_layout_->addWidget(first.data(), i, 0);
+		contents_layout_->addWidget(first.data(),  i, 0);
 		contents_layout_->addWidget(second.data(), i, 1);
 	}
 }
@@ -131,7 +195,12 @@
 	for (auto& [label, data] : data_)
 		label->setStyleSheet(style_sheet);
 
-	// TODO ElidedData
+	if (style & LabelledParagraph::ElidedData) {
+		for (auto& [label, data] : data_) {
+			data->setWordWrap(false);
+			data->SetElidingMode(true);
+		}
+	}
 }
 
 } // namespace TextWidgets
--- a/src/services/anilist.cc	Tue Jul 16 21:15:59 2024 -0400
+++ b/src/services/anilist.cc	Wed Oct 02 23:06:43 2024 -0400
@@ -70,25 +70,15 @@
 
 /* FIXME: why is this here */
 
-static bool AccountIsValid() {
-	const auto& auth = session.config.auth.anilist;
-	return (auth.user_id && !auth.auth_token.empty());
-}
-
 static std::optional<nlohmann::json> SendJSONRequest(const nlohmann::json& data) {
-	if (!AccountIsValid()) {
-		session.SetStatusBar(Strings::Translate("AniList: Account isn't valid! (unauthorized?)"));
-		return std::nullopt;
-	}
-
-	const auto& auth = session.config.auth.anilist;
-
-	const std::vector<std::string> headers = {
-		"Authorization: Bearer " + auth.auth_token,
+	std::vector<std::string> headers = {
 		"Accept: application/json",
 		"Content-Type: application/json",
 	};
 
+	if (!session.config.auth.anilist.auth_token.empty())
+		headers.push_back("Authorization: Bearer " + session.config.auth.anilist.auth_token);
+
 	const std::string response = Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data.dump(), HTTP::Type::Post));
 	if (response.empty()) {
 		session.SetStatusBar(Strings::Translate("AniList: JSON request returned an empty result!"));