changeset 273:f31305b9f60a

*: various code safety changes this also makes the code build on Qt 5.7. I can't test it though because I don't have it working... FAIL!
author Paper <paper@paper.us.eu.org>
date Thu, 18 Apr 2024 16:53:17 -0400
parents 5437009cb10e
children f6a756c19bfb
files configure.ac include/core/strings.h include/gui/pages/anime_list.h include/gui/pages/search.h include/gui/widgets/anime_button.h include/gui/widgets/graph.h include/gui/widgets/poster.h include/gui/window.h scripts/osx/deploy_build.sh src/core/strings.cc src/gui/pages/anime_list.cc src/gui/pages/search.cc src/gui/pages/torrents.cc src/gui/widgets/anime_button.cc src/gui/widgets/elided_label.cc src/gui/widgets/poster.cc src/gui/window.cc src/main.cc src/sys/osx/dark_theme.cc
diffstat 19 files changed, 348 insertions(+), 216 deletions(-) [+]
line wrap: on
line diff
--- a/configure.ac	Thu Apr 18 16:51:35 2024 -0400
+++ b/configure.ac	Thu Apr 18 16:53:17 2024 -0400
@@ -13,6 +13,7 @@
 AC_PROG_CC
 
 dnl Do we have a C++17 compiler
+: ${CXXFLAGS=""}
 AC_PROG_CXX
 AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
 
--- a/include/core/strings.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/core/strings.h	Thu Apr 18 16:53:17 2024 -0400
@@ -67,6 +67,7 @@
 std::string ToUtf8String(bool b);
 
 uint64_t HumanReadableSizeToBytes(const std::string& str);
+std::string BytesToHumanReadableSize(uint64_t bytes, int precision = 2);
 
 void RemoveLeadingChars(std::string& s,  const char c);
 void RemoveTrailingChars(std::string& s, const char c);
--- a/include/gui/pages/anime_list.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/pages/anime_list.h	Thu Apr 18 16:53:17 2024 -0400
@@ -6,10 +6,27 @@
 #include <QSortFilterProxyModel>
 #include <QStyledItemDelegate>
 #include <QWidget>
+#include <QThread>
 #include <vector>
+#include <queue>
 
 class QTreeView;
 class QTabBar;
+class AnimeListPage;
+
+class AnimeListPageUpdateEntryThread final : public QThread {
+public:
+	AnimeListPageUpdateEntryThread(AnimeListPage* parent);
+
+	void AddToQueue(int id);
+
+protected:
+	void run() override;
+
+private:
+	AnimeListPage* page_ = nullptr;
+	std::queue<int> queue_;
+};
 
 class AnimeListPageSortFilter final : public QSortFilterProxyModel {
 	Q_OBJECT
@@ -87,6 +104,8 @@
 	QTabBar* tab_bar;
 	QTreeView* tree_view;
 	QRect panelRect;
+
+	AnimeListPageUpdateEntryThread update_entry_thread_;
 	std::array<AnimeListPageSortFilter*, 5> sort_models;
 };
 
--- a/include/gui/pages/search.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/pages/search.h	Thu Apr 18 16:53:17 2024 -0400
@@ -7,9 +7,27 @@
 #include <QFrame>
 #include <QItemSelection>
 #include <QSortFilterProxyModel>
+#include <QThread>
 
 class QTreeView;
 
+class SearchPageSearchThread : public QThread {
+	Q_OBJECT
+
+public:
+	SearchPageSearchThread(QObject* parent = nullptr);
+	void SetSearch(const std::string& search);
+
+protected:
+	void run() override;
+
+private:
+	std::string search_;
+
+signals:
+	void GotResults(const std::vector<int>& search);
+};
+
 class SearchPageListSortFilter final : public QSortFilterProxyModel {
 	Q_OBJECT
 
@@ -62,5 +80,7 @@
 	SearchPageListModel* model = nullptr;
 	SearchPageListSortFilter* sort_model = nullptr;
 	QTreeView* treeview = nullptr;
+
+	SearchPageSearchThread thread_;
 };
 #endif // MINORI_GUI_PAGES_SEARCH_H_
--- a/include/gui/widgets/anime_button.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/widgets/anime_button.h	Thu Apr 18 16:53:17 2024 -0400
@@ -2,16 +2,12 @@
 #define MINORI_GUI_WIDGETS_ANIME_BUTTON_H_
 
 #include <QFrame>
-
-class QWidget;
-class QLabel;
+#include <QWidget>
+#include <QLabel>
 
-class Poster;
-class ElidedLabel;
-
-namespace TextWidgets {
-class LabelledParagraph;
-}
+#include "gui/widgets/poster.h"
+#include "gui/widgets/elided_label.h"
+#include "gui/widgets/text.h"
 
 namespace Anime {
 class Anime;
@@ -24,10 +20,10 @@
 	void SetAnime(const Anime::Anime& anime);
 
 protected:
-	Poster* _poster = nullptr;
-	QLabel* _title = nullptr;
-	TextWidgets::LabelledParagraph* _info = nullptr;
-	ElidedLabel* _synopsis = nullptr;
+	Poster _poster;
+	QLabel _title;
+	TextWidgets::LabelledParagraph _info;
+	ElidedLabel _synopsis;
 };
 
-#endif // MINORI_GUI_WIDGETS_ANIME_BUTTON_H_
\ No newline at end of file
+#endif // MINORI_GUI_WIDGETS_ANIME_BUTTON_H_
--- a/include/gui/widgets/graph.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/widgets/graph.h	Thu Apr 18 16:53:17 2024 -0400
@@ -56,7 +56,7 @@
 		QFontMetrics metric(font());
 
 		for (const auto& item : map) {
-			unsigned long width = metric.horizontalAdvance(QString::number(item.first), -1);
+			unsigned long width = metric.boundingRect(QString::number(item.first)).width();
 			if (width > ret)
 				ret = width;
 		}
@@ -69,7 +69,7 @@
 		QFontMetrics metric(font());
 
 		for (const auto& item : map) {
-			unsigned long width = metric.horizontalAdvance(QString::number(item.second), -1);
+			unsigned long width = metric.boundingRect(QString::number(item.second)).width();
 			if (width > ret)
 				ret = width;
 		}
--- a/include/gui/widgets/poster.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/widgets/poster.h	Thu Apr 18 16:53:17 2024 -0400
@@ -3,8 +3,9 @@
 #include <QFrame>
 #include <QImage>
 
+#include "gui/widgets/clickable_label.h"
+
 class QWidget;
-class ClickableLabel;
 namespace Anime {
 class Anime;
 }
@@ -24,10 +25,10 @@
 	void RenderToLabel();
 
 private:
-	QImage img;
-	QString service_url;
-	ClickableLabel* label;
-	bool clickable = true;
+	QImage img_;
+	QString service_url_;
+	ClickableLabel label_;
+	bool clickable_ = true;
 };
 
-#endif // MINORI_GUI_WIDGETS_POSTER_H_
\ No newline at end of file
+#endif // MINORI_GUI_WIDGETS_POSTER_H_
--- a/include/gui/window.h	Thu Apr 18 16:51:35 2024 -0400
+++ b/include/gui/window.h	Thu Apr 18 16:53:17 2024 -0400
@@ -11,25 +11,43 @@
 #include <QCloseEvent>
 #include <QStackedWidget>
 #include <QThread>
+#include <QTimer>
 #include <QWidget>
 
 class QMenu;
+class AnimeListPage;
 
 Q_DECLARE_METATYPE(std::vector<std::string>);
 
-class PlayingThread : public QThread {
+class MainWindowPlayingThread final : public QThread {
 	Q_OBJECT
 
 public:
-	PlayingThread(QObject* object = nullptr) : QThread(object) {}
+	MainWindowPlayingThread(QObject* object = nullptr) : QThread(object) {}
 
-private:
+protected:
 	void run() override;
 
 signals:
 	void Done(const std::vector<std::string>& files);
 };
 
+class MainWindowAsyncSynchronizeThread final : public QThread {
+	Q_OBJECT
+
+public:
+	MainWindowAsyncSynchronizeThread(QAction* action, AnimeListPage* page, QObject* object = nullptr);
+	void SetAction(QAction* action);
+	void SetPage(AnimeListPage* page);
+
+protected:
+	void run() override;
+
+private:
+	QAction* action_ = nullptr;
+	AnimeListPage* page_ = nullptr;
+};
+
 class MainWindow final : public QMainWindow {
 	Q_OBJECT
 
@@ -58,11 +76,14 @@
 	void closeEvent(QCloseEvent* event) override;
 
 private:
-	std::unique_ptr<QWidget> main_widget = nullptr;
-	std::unique_ptr<QStackedWidget> stack = nullptr;
-	std::unique_ptr<SideBar> sidebar = nullptr;
+	QWidget main_widget_;
+	QStackedWidget stack_;
+	SideBar sidebar_;
 
-	std::unique_ptr<PlayingThread> thread = nullptr;
+	MainWindowPlayingThread playing_thread_;
+	QTimer playing_thread_timer_;
+
+	MainWindowAsyncSynchronizeThread async_synchronize_thread_;
 
 	QMenu* folder_menu = nullptr;
 };
--- a/scripts/osx/deploy_build.sh	Thu Apr 18 16:51:35 2024 -0400
+++ b/scripts/osx/deploy_build.sh	Thu Apr 18 16:53:17 2024 -0400
@@ -4,6 +4,11 @@
 # run this in your build dir to get a usable app bundle
 
 SCRIPT_DIR=$(dirname -- "$0")
+FRAMEWORK_MODE=false
+if test "x$1x" = "x--frameworksx"; then
+	echo "framework mode enabled"
+	FRAMEWORK_MODE=true
+fi
 BUNDLE_NAME="Minori"
 
 cp -r "$SCRIPT_DIR/../../rc/sys/osx/$BUNDLE_NAME.app" .
@@ -12,9 +17,15 @@
 cp ".libs/minori" "$BUNDLE_NAME.app/Contents/MacOS/minori"
 
 mkdir -p "$BUNDLE_NAME.app/Contents/Frameworks"
-for i in animia pugixml anitomy; do
+for i in animone pugixml anitomy; do
 	cp "dep/$i/.libs/lib$i.0.dylib" "$BUNDLE_NAME.app/Contents/Frameworks"
 	install_name_tool -change "/usr/local/lib/lib$i.0.dylib" "@executable_path/../Frameworks/lib$i.0.dylib" "$BUNDLE_NAME.app/Contents/MacOS/minori"
 done
 
 macdeployqt "$BUNDLE_NAME.app"
+if $FRAMEWORK_MODE; then
+	for i in QtCore QtGui QtWidgets; do
+		install_name_tool -id @executable_path/../Frameworks/$i.framework/Versions/5/$i $BUNDLE_NAME.app/Contents/Frameworks/$i.framework/Versions/5/$i
+		install_name_tool -change @rpath/$i.framework/Versions/5/$i @executable_path/../Frameworks/$i.framework/Versions/5/$i $BUNDLE_NAME.app/Contents/MacOS/minori
+	done
+fi
--- a/src/core/strings.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/core/strings.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -13,6 +13,7 @@
 #include <cctype>
 #include <codecvt>
 #include <iostream>
+#include <iomanip>
 #include <locale>
 #include <string>
 #include <unordered_map>
@@ -243,11 +244,16 @@
 /* util funcs */
 uint64_t HumanReadableSizeToBytes(const std::string& str) {
 	static const std::unordered_map<std::string, uint64_t> bytes_map = {
-	    {"KB", 1ull << 10},
-	    {"MB", 1ull << 20},
-	    {"GB", 1ull << 30},
-	    {"TB", 1ull << 40},
-	    {"PB", 1ull << 50}  /* surely we won't need more than this */
+		{"KB", 1000ull},
+		{"MB", 1000000ull},
+		{"GB", 1000000000ull},
+		{"TB", 1000000000000ull},
+		{"PB", 1000000000000000ull},
+	    {"KiB", 1ull << 10},
+	    {"MiB", 1ull << 20},
+	    {"GiB", 1ull << 30},
+	    {"TiB", 1ull << 40},
+	    {"PiB", 1ull << 50}  /* surely we won't need more than this */
 	};
 
 	for (const auto& suffix : bytes_map) {
@@ -264,6 +270,35 @@
 	return ToInt(str, 0);
 }
 
+std::string BytesToHumanReadableSize(uint64_t bytes, int precision) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
+	/* QLocale in Qt >= 5.10.0 has a function for this */
+	return Strings::ToUtf8String(session.config.locale.GetLocale().formattedDataSize(bytes), precision);
+#else
+	static const std::unordered_map<uint64_t, std::string> map = {
+		{1ull << 10, "KiB"},
+		{1ull << 20, "MiB"},
+		{1ull << 30, "GiB"},
+		{1ull << 40, "TiB"},
+		{1ull << 50, "PiB"}
+	};
+
+	for (const auto& suffix : map) {
+		if (bytes / suffix.first < 1)
+			continue;
+
+		std::stringstream ss;
+		ss << std::setprecision(precision)
+		   << (static_cast<double>(bytes) / suffix.first) << " "
+		   << suffix.second;
+		return ss.str();
+	}
+
+	/* better luck next time */
+	return "0 bytes";
+#endif
+}
+
 void RemoveLeadingChars(std::string& s, const char c) {
 	s.erase(0, std::min(s.find_first_not_of(c), s.size() - 1));
 }
--- a/src/gui/pages/anime_list.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/pages/anime_list.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -27,11 +27,32 @@
 #include <QShortcut>
 #include <QStylePainter>
 #include <QStyledItemDelegate>
-#include <QThread>
+#include <QThreadPool>
+#include <QRunnable>
 #include <QTreeView>
 
 #include <set>
 
+AnimeListPageUpdateEntryThread::AnimeListPageUpdateEntryThread(AnimeListPage* parent) : QThread(parent) {
+	page_ = parent;
+}
+
+void AnimeListPageUpdateEntryThread::AddToQueue(int id) {
+	if (isRunning())
+		return; /* don't let us fuck ourselves */
+
+	queue_.push(id);
+}
+
+/* processes the queue... */
+void AnimeListPageUpdateEntryThread::run() {
+	while (!queue_.empty() && !isInterruptionRequested()) {
+		Services::UpdateAnimeEntry(queue_.front());
+		queue_.pop();
+	}
+	page_->Refresh();
+}
+
 AnimeListPageSortFilter::AnimeListPageSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
 }
 
@@ -111,7 +132,7 @@
 				case AL_TITLE: return Strings::ToQString(list[index.row()].GetUserPreferredTitle());
 				case AL_PROGRESS:
 					return QString::number(list[index.row()].GetUserProgress()) + "/" +
-					       QString::number(list[index.row()].GetEpisodes());
+						   QString::number(list[index.row()].GetEpisodes());
 				case AL_EPISODES: return list[index.row()].GetEpisodes();
 				case AL_SCORE: return Strings::ToQString(list[index.row()].GetUserPresentableScore());
 				case AL_TYPE: return Strings::ToQString(Translate::ToString(list[index.row()].GetFormat()));
@@ -120,7 +141,7 @@
 					if (!year)
 						return "Unknown Unknown";
 					return Strings::ToQString(Translate::ToLocalString(list[index.row()].GetSeason()) + " " +
-					                          Strings::ToUtf8String(year.value()));
+											  Strings::ToUtf8String(year.value()));
 				}
 				case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
 				case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
@@ -219,12 +240,14 @@
 }
 
 void AnimeListPage::UpdateAnime(int id) {
-	QThread* thread = QThread::create([this, id] { Services::UpdateAnimeEntry(id); });
+	/* this ought to just add to the thread's buffer. */
+	if (update_entry_thread_.isRunning()) {
+		update_entry_thread_.requestInterruption();
+		update_entry_thread_.wait();
+	}
 
-	connect(thread, &QThread::finished, this, &AnimeListPage::Refresh);
-	connect(thread, &QThread::finished, thread, &QThread::deleteLater);
-
-	thread->start();
+	update_entry_thread_.AddToQueue(id);
+	update_entry_thread_.start();
 }
 
 void AnimeListPage::RemoveAnime(int id) {
@@ -243,7 +266,7 @@
 		if (i == AnimeListPageModel::AL_TITLE)
 			continue;
 		const auto column_name =
-		    sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
+			sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
 
 		QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
 			if (!checked && (VisibleColumnsCount() <= 1))
@@ -277,9 +300,9 @@
 	menu->setToolTipsVisible(true);
 
 	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+		reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
 	const QItemSelection selection =
-	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
 
 	std::set<Anime::Anime*> animes;
 	for (const auto& index : selection.indexes()) {
@@ -293,7 +316,7 @@
 	menu->addAction(tr("Information"), [this, animes] {
 		for (auto& anime : animes) {
 			InformationDialog* dialog = new InformationDialog(
-			    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MAIN_INFO, this);
+				*anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MAIN_INFO, this);
 
 			dialog->show();
 			dialog->raise();
@@ -304,7 +327,7 @@
 	menu->addAction(tr("Edit"), [this, animes] {
 		for (auto& anime : animes) {
 			InformationDialog* dialog = new InformationDialog(
-			    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MY_LIST, this);
+				*anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MY_LIST, this);
 
 			dialog->show();
 			dialog->raise();
@@ -322,19 +345,19 @@
 void AnimeListPage::ItemDoubleClicked() {
 	/* throw out any other garbage */
 	const QItemSelection selection =
-	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
 	if (!selection.indexes().first().isValid()) {
 		return;
 	}
 
 	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+		reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
 
 	const QModelIndex index = source_model->index(selection.indexes().first().row());
 	Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
 
 	InformationDialog* dialog = new InformationDialog(
-	    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MAIN_INFO, this);
+		*anime, [this, anime] { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MAIN_INFO, this);
 
 	dialog->show();
 	dialog->raise();
@@ -349,7 +372,7 @@
 void AnimeListPage::RefreshTabs() {
 	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])) + ")");
+								   QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 }
 
 void AnimeListPage::Refresh() {
@@ -424,7 +447,7 @@
 
 /* --------- QTabWidget replication end ---------- */
 
-AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent) {
+AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent), update_entry_thread_(this) {
 	/* Tab bar */
 	tab_bar = new QTabBar(this);
 	tab_bar->setExpanding(false);
@@ -445,7 +468,7 @@
 
 	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])) + ")");
+						QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 		sort_models[i] = new AnimeListPageSortFilter(tree_view);
 		sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i]));
 		sort_models[i]->setSortRole(Qt::UserRole);
@@ -476,10 +499,10 @@
 
 	/* Enter & return keys */
 	connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
+			&AnimeListPage::ItemDoubleClicked);
 
 	connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
+			&AnimeListPage::ItemDoubleClicked);
 
 	tree_view->header()->setStretchLastSection(false);
 	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
--- a/src/gui/pages/search.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/pages/search.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -26,6 +26,17 @@
 #include "anitomy/anitomy.h"
 #include "pugixml.hpp"
 
+SearchPageSearchThread::SearchPageSearchThread(QObject* parent) : QThread(parent) {
+}
+
+void SearchPageSearchThread::SetSearch(const std::string& search) {
+	search_ = search;
+}
+
+void SearchPageSearchThread::run() {
+	emit GotResults(Services::Search(search_));
+}
+
 SearchPageListSortFilter::SearchPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
 }
 
@@ -139,7 +150,7 @@
 					const QString d = data(index, Qt::DisplayRole).toString();
 					const QFontMetrics metric = QFontMetrics(QFont());
 
-					return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height());
+					return QSize(std::max(metric.boundingRect(d).width(), 100), metric.height());
 				}
 			}
 			break;
@@ -282,21 +293,15 @@
 			QLineEdit* line_edit = new QLineEdit("", toolbar);
 			connect(line_edit, &QLineEdit::returnPressed, this, [this, line_edit] {
 				/* static thread here. */
-				static QThread* thread = nullptr;
+				if (thread_.isRunning())
+					thread_.exit(1); /* fail */
 
-				if (thread)
-					return;
+				thread_.SetSearch(Strings::ToUtf8String(line_edit->text()));
 
-				thread = QThread::create([this, line_edit] {
-					model->ParseSearch(Services::Search(Strings::ToUtf8String(line_edit->text())));
-				});
-
-				connect(thread, &QThread::finished, this, [] {
-					thread->deleteLater();
-					thread = nullptr;
-				});
-
-				thread->start();
+				thread_.start();
+			});
+			connect(&thread_, &SearchPageSearchThread::GotResults, this, [this](const std::vector<int>& search) {
+				model->ParseSearch(search);
 			});
 			toolbar->addWidget(line_edit);
 		}
--- a/src/gui/pages/torrents.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/pages/torrents.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -14,6 +14,7 @@
 #include <QToolBar>
 #include <QTreeView>
 #include <QVBoxLayout>
+#include <QtGlobal>
 
 #include <algorithm>
 #include <fstream>
@@ -256,7 +257,7 @@
 				case TL_TITLE: return Strings::ToQString(item.GetTitle());
 				case TL_EPISODE: return Strings::ToQString(item.GetEpisode());
 				case TL_GROUP: return Strings::ToQString(item.GetGroup());
-				case TL_SIZE: return session.config.locale.GetLocale().formattedDataSize(item.GetSize());
+				case TL_SIZE: return Strings::ToQString(Strings::BytesToHumanReadableSize(item.GetSize()));
 				case TL_RESOLUTION: return Strings::ToQString(item.GetResolution());
 				case TL_SEEDERS: return item.GetSeeders();
 				case TL_LEECHERS: return item.GetLeechers();
@@ -284,7 +285,7 @@
 					const QString d = data(index, Qt::DisplayRole).toString();
 					const QFontMetrics metric = QFontMetrics(QFont());
 
-					return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height());
+					return QSize(std::max(metric.boundingRect(d).width(), 100), metric.height());
 				}
 			}
 			break;
--- a/src/gui/widgets/anime_button.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/widgets/anime_button.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -26,15 +26,17 @@
 *|_________| Synopsis               *
 \***********************************/
 
-AnimeButton::AnimeButton(QWidget* parent) : QFrame(parent) {
+AnimeButton::AnimeButton(QWidget* parent)
+	: QFrame(parent)
+	, _info(tr("Aired:\nEpisodes:\nGenres:\nProducers:\nScore:\nPopularity:"), "\n\n\n\n\n", nullptr)
+	, _synopsis("", nullptr) {
 	setFrameShadow(QFrame::Plain);
 	setFrameShape(QFrame::Box);
 	QHBoxLayout* ly = new QHBoxLayout(this);
 
-	_poster = new Poster(this);
-	_poster->setFixedSize(120, 170);
-	_poster->SetClickable(false);
-	ly->addWidget(_poster, 0, Qt::AlignTop);
+	_poster.setFixedSize(120, 170);
+	_poster.SetClickable(false);
+	ly->addWidget(&_poster, 0, Qt::AlignTop);
 
 	{
 		QWidget* misc_section = new QWidget(this);
@@ -43,39 +45,37 @@
 		QVBoxLayout* misc_layout = new QVBoxLayout(misc_section);
 		misc_layout->setContentsMargins(0, 0, 0, 0);
 
-		_title = new QLabel("", misc_section);
-		_title->setAutoFillBackground(true);
-		_title->setContentsMargins(4, 4, 4, 4);
-		_title->setStyleSheet("background-color: rgba(0, 245, 25, 50);");
+		_title.setAutoFillBackground(true);
+		_title.setContentsMargins(4, 4, 4, 4);
+		_title.setStyleSheet("background-color: rgba(0, 245, 25, 50);");
 		{
-			QFont fnt(_title->font());
+			QFont fnt(_title.font());
 			fnt.setWeight(QFont::Bold);
-			_title->setFont(fnt);
+			_title.setFont(fnt);
 		}
-		misc_layout->addWidget(_title);
+		misc_layout->addWidget(&_title);
 
-		_info = new TextWidgets::LabelledParagraph(tr("Aired:\nEpisodes:\nGenres:\nProducers:\nScore:\nPopularity:"),
-		                                           "\n\n\n\n\n", misc_section);
 		{
-			QFont fnt(_info->GetLabels()->font());
+			QFont fnt(_info.GetLabels()->font());
 			fnt.setWeight(QFont::Bold);
-			_info->GetLabels()->setFont(fnt);
+			_info.GetLabels()->setFont(fnt);
 		}
-		_info->setContentsMargins(4, 0, 4, 0);
-		_info->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
-		misc_layout->addWidget(_info);
+
+		_info.setContentsMargins(4, 0, 4, 0);
+		_info.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
+		misc_layout->addWidget(&_info);
 
-		QWidget* dummy = new QWidget(misc_section);
-		dummy->setContentsMargins(4, 0, 4, 0);
-		QVBoxLayout* dummy_layout = new QVBoxLayout(dummy);
-		dummy_layout->setSpacing(0);
-		// dummy_layout->setContentsMargins(0, 0, 0, 0);
-		dummy_layout->setContentsMargins(0, 0, 0, 0);
+		{
+			QWidget* dummy = new QWidget(misc_section);
+			dummy->setContentsMargins(4, 0, 4, 0);
+			QVBoxLayout* dummy_layout = new QVBoxLayout(dummy);
+			dummy_layout->setSpacing(0);
+			dummy_layout->setContentsMargins(0, 0, 0, 0);
 
-		_synopsis = new ElidedLabel("", dummy);
-		_synopsis->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
-		dummy_layout->addWidget(_synopsis);
-		misc_layout->addWidget(dummy);
+			_synopsis.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+			dummy_layout->addWidget(&_synopsis);
+			misc_layout->addWidget(dummy);
+		}
 
 		ly->addWidget(misc_section, 0, Qt::AlignTop);
 	}
@@ -86,20 +86,16 @@
 }
 
 void AnimeButton::SetAnime(const Anime::Anime& anime) {
-	_poster->SetAnime(anime);
-	_title->setText(Strings::ToQString(anime.GetUserPreferredTitle()));
+	_poster.SetAnime(anime);
+	_title.setText(Strings::ToQString(anime.GetUserPreferredTitle()));
 
 	{
 		const QLocale& locale = session.config.locale.GetLocale();
-		_info->GetParagraph()->SetText(locale.toString(anime.GetAirDate().GetAsQDate(), "dd MMM yyyy") + "\n" +
-		                               QString::number(anime.GetEpisodes()) + "\n" +
-		                               Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) + "\n" + "...\n" +
-		                               QString::number(anime.GetAudienceScore()) + "%\n" + "...");
+		_info.GetParagraph()->SetText(locale.toString(anime.GetAirDate().GetAsQDate(), "dd MMM yyyy") + "\n" +
+		                              QString::number(anime.GetEpisodes()) + "\n" +
+		                              Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) + "\n" + "...\n" +
+		                              QString::number(anime.GetAudienceScore()) + "%\n" + "...");
 	}
 
-	{
-		QString synopsis = Strings::ToQString(anime.GetSynopsis());
-		QFontMetrics metrics(_synopsis->font());
-		_synopsis->SetText(Strings::ToQString(anime.GetSynopsis()));
-	}
-}
\ No newline at end of file
+	_synopsis.SetText(Strings::ToQString(anime.GetSynopsis()));
+}
--- a/src/gui/widgets/elided_label.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/widgets/elided_label.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -49,32 +49,30 @@
 	QFrame::paintEvent(event);
 
 	QPainter painter(this);
-	QFontMetrics fontMetrics = painter.fontMetrics();
+	QFontMetrics metrics = painter.fontMetrics();
 
-	int line_spacing = fontMetrics.lineSpacing();
+	const int line_spacing = metrics.lineSpacing();
 	int y = 0;
 
-	QTextLayout textLayout(content, painter.font());
-	textLayout.beginLayout();
+	QTextLayout text_layout(content, painter.font());
+	text_layout.beginLayout();
 	for (;;) {
-		QTextLine line = textLayout.createLine();
-
+		QTextLine line = text_layout.createLine();
 		if (!line.isValid())
 			break;
 
 		line.setLineWidth(width());
-		int nextLineY = y + line_spacing;
 
-		if (height() >= nextLineY + line_spacing) {
+		if (height() >= y + (2 * line_spacing)) {
 			line.draw(&painter, QPoint(0, y));
-			y = nextLineY;
+			y += line_spacing;
 		} else {
 			QString last_line = content.mid(line.textStart());
-			QString elided_last_line = fontMetrics.elidedText(last_line, Qt::ElideRight, width());
-			painter.drawText(QPoint(0, y + fontMetrics.ascent()), elided_last_line);
-			line = textLayout.createLine();
+			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;
 		}
 	}
-	textLayout.endLayout();
+	text_layout.endLayout();
 }
--- a/src/gui/widgets/poster.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/widgets/poster.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -25,9 +25,8 @@
 	setFrameShape(QFrame::Box);
 	setFrameShadow(QFrame::Plain);
 
-	label = new ClickableLabel(this);
-	label->setAlignment(Qt::AlignCenter);
-	layout->addWidget(label);
+	label_.setAlignment(Qt::AlignCenter);
+	layout->addWidget(&label_);
 }
 
 Poster::Poster(const Anime::Anime& anime, QWidget* parent) : Poster(parent) {
@@ -45,37 +44,37 @@
 		thread->start();
 	}
 
-	service_url = Strings::ToQString(anime.GetServiceUrl());
+	service_url_ = Strings::ToQString(anime.GetServiceUrl());
 
-	if (clickable) {
-		label->disconnect();
-		connect(label, &ClickableLabel::clicked, this, [this] { QDesktopServices::openUrl(service_url); });
+	if (clickable_) {
+		label_.disconnect();
+		connect(&label_, &ClickableLabel::clicked, this, [this] { QDesktopServices::openUrl(service_url_); });
 	}
 }
 
 void Poster::SetClickable(bool enabled) {
-	clickable = enabled;
+	clickable_ = enabled;
 
-	if (clickable && !service_url.isEmpty()) {
+	if (clickable_ && !service_url_.isEmpty()) {
 		setCursor(Qt::PointingHandCursor);
-		label->disconnect();
-		connect(label, &ClickableLabel::clicked, this, [this] { QDesktopServices::openUrl(service_url); });
+		label_.disconnect();
+		connect(&label_, &ClickableLabel::clicked, this, [this] { QDesktopServices::openUrl(service_url_); });
 	} else {
 		setCursor(Qt::ArrowCursor);
-		label->disconnect();
+		label_.disconnect();
 	}
 }
 
 void Poster::ImageDownloadFinished(const QByteArray& arr) {
-	img.loadFromData(arr);
+	img_.loadFromData(arr);
 	RenderToLabel();
 }
 
 void Poster::RenderToLabel() {
-	const QPixmap pixmap = QPixmap::fromImage(img);
+	const QPixmap pixmap = QPixmap::fromImage(img_);
 	if (pixmap.isNull())
 		return;
-	label->setPixmap(pixmap.scaled(label->size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
+	label_.setPixmap(pixmap.scaled(label_.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
 }
 
 void Poster::resizeEvent(QResizeEvent*) {
--- a/src/gui/window.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/gui/window.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -48,34 +48,50 @@
 #	include "sys/win32/dark_theme.h"
 #endif
 
-void PlayingThread::run() {
+void MainWindowPlayingThread::run() {
 	std::vector<std::string> files;
 	Track::Media::GetCurrentlyPlaying(files);
 	emit Done(files);
 }
 
-MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
+MainWindowAsyncSynchronizeThread::MainWindowAsyncSynchronizeThread(QAction* action, AnimeListPage* page, QObject* parent) : QThread(parent) {
+	SetAction(action);
+	SetPage(page);
+}
+
+void MainWindowAsyncSynchronizeThread::SetAction(QAction* action) {
+	action_ = action;
+}
+
+void MainWindowAsyncSynchronizeThread::SetPage(AnimeListPage* page) {
+	page_ = page;
+}
+
+void MainWindowAsyncSynchronizeThread::run() {
+	action_->setEnabled(false);
+	Services::Synchronize();
+	page_->Refresh();
+	action_->setEnabled(true);
+}
+
+MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), async_synchronize_thread_(nullptr, nullptr) {
 	setWindowIcon(QIcon(":/icons/favicon.png"));
 
-	main_widget.reset(new QWidget(this));
-	new QHBoxLayout(main_widget.get());
+	sidebar_.setFixedWidth(128);
+	sidebar_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+
+	connect(&sidebar_, &SideBar::CurrentItemChanged, &stack_, &QStackedWidget::setCurrentIndex);
+
+	new QHBoxLayout(&main_widget_);
 
 	AddMainWidgets();
-	setCentralWidget(main_widget.get());
+	setCentralWidget(&main_widget_);
 
 	CreateBars();
 
-	NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack->widget(static_cast<int>(Pages::NOW_PLAYING)));
-
-	qRegisterMetaType<std::vector<std::string>>();
+	NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack_.widget(static_cast<int>(Pages::NOW_PLAYING)));
 
-	/* This thread will be destroyed on
-	 * close of the program OR on the destruction
-	 * of MainWindow
-	 */
-	thread.reset(new PlayingThread(this));
-
-	connect(thread.get(), &PlayingThread::Done, this, [page](const std::vector<std::string>& files) {
+	connect(&playing_thread_, &MainWindowPlayingThread::Done, this, [page](const std::vector<std::string>& files) {
 		for (const auto& file : files) {
 			anitomy::Anitomy anitomy;
 			anitomy.Parse(Strings::ToWstring(file));
@@ -92,13 +108,11 @@
 		}
 	});
 
-	QTimer* timer = new QTimer(this);
-
-	connect(timer, &QTimer::timeout, this, [this, page] {
-		if (!thread.get() || thread->isRunning())
+	connect(&playing_thread_timer_, &QTimer::timeout, this, [this] {
+		if (playing_thread_.isRunning())
 			return;
 
-		thread->start();
+		playing_thread_.start();
 	});
 
 #ifdef MACOSX
@@ -106,53 +120,42 @@
 		return;
 #endif
 
-	timer->start(5000);
+	playing_thread_timer_.start(5000);
 }
 
 void MainWindow::AddMainWidgets() {
 	int page = static_cast<int>(Pages::ANIME_LIST);
 
-	if (sidebar.get()) {
-		main_widget->layout()->removeWidget(sidebar.get());
-		sidebar.reset();
-	}
+	sidebar_.clear();
 
-	if (stack.get()) {
-		page = stack->currentIndex();
-		main_widget->layout()->removeWidget(stack.get());
-	}
+	sidebar_.AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
+	sidebar_.AddSeparator();
+	sidebar_.AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
+	sidebar_.AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
+	sidebar_.AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
+	sidebar_.AddSeparator();
+	sidebar_.AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
+	sidebar_.AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
+	sidebar_.AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
 
-	sidebar.reset(new SideBar(main_widget.get()));
-	sidebar->setFixedWidth(128);
-	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
+	while (stack_.count())
+		stack_.removeWidget(stack_.widget(0));
 
-	sidebar->AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
-	sidebar->AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
-	sidebar->AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
-	sidebar->AddSeparator();
-	sidebar->AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
-	sidebar->AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
-	sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
-
-	stack.reset(new QStackedWidget(main_widget.get()));
-
-	stack->addWidget(new NowPlayingPage(main_widget.get()));
+	/* can we allocate these on the stack? */
+	stack_.addWidget(new NowPlayingPage(&main_widget_));
+	/* ---- */
+	stack_.addWidget(new AnimeListPage(&main_widget_));
+	stack_.addWidget(new HistoryPage(&main_widget_));
+	stack_.addWidget(new StatisticsPage(&main_widget_));
 	/* ---- */
-	stack->addWidget(new AnimeListPage(main_widget.get()));
-	stack->addWidget(new HistoryPage(main_widget.get()));
-	stack->addWidget(new StatisticsPage(main_widget.get()));
-	/* ---- */
-	stack->addWidget(new SearchPage(main_widget.get()));
-	stack->addWidget(new SeasonsPage(main_widget.get()));
-	stack->addWidget(new TorrentsPage(main_widget.get()));
+	stack_.addWidget(new SearchPage(&main_widget_));
+	stack_.addWidget(new SeasonsPage(&main_widget_));
+	stack_.addWidget(new TorrentsPage(&main_widget_));
 
-	connect(sidebar.get(), &SideBar::CurrentItemChanged, stack.get(), &QStackedWidget::setCurrentIndex);
-	sidebar->SetCurrentItem(page);
+	sidebar_.SetCurrentItem(page);
 
-	main_widget->layout()->addWidget(sidebar.get());
-	main_widget->layout()->addWidget(stack.get());
+	main_widget_.layout()->addWidget(&sidebar_);
+	main_widget_.layout()->addWidget(&stack_);
 }
 
 void MainWindow::CreateBars() {
@@ -201,7 +204,7 @@
 				sync_action = menu->addAction(tr("Synchronize &list"));
 
 				connect(sync_action, &QAction::triggered, this,
-				        [this, sync_action] { AsyncSynchronize(sync_action, stack.get()); });
+				        [this, sync_action] { AsyncSynchronize(sync_action, &stack_); });
 
 				sync_action->setIcon(QIcon(":/icons/24x24/arrow-circle-double-135.png"));
 				sync_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
@@ -290,48 +293,48 @@
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(0); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(0); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Anime List")));
 				action->setCheckable(true);
 				action->setChecked(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(1); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(1); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&History")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(2); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(2); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Statistics")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(3); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(3); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("S&earch")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(4); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(4); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("Se&asons")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(5); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(5); });
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Torrents")));
 				action->setCheckable(true);
-				connect(action, &QAction::toggled, this, [this] { sidebar->SetCurrentItem(6); });
+				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(6); });
 			}
 
-			/* pain in my ass */
-			connect(sidebar.get(), &SideBar::CurrentItemChanged, this, [pages_group](int index) {
+			/* pain in the ass */
+			connect(&sidebar_, &SideBar::CurrentItemChanged, this, [pages_group](int index) {
 				QAction* checked = pages_group->checkedAction();
 
 				const QList<QAction*>& actions = pages_group->actions();
@@ -495,12 +498,13 @@
 		}
 	}
 
-	QThreadPool::globalInstance()->start([stack, action] {
-		action->setEnabled(false);
-		Services::Synchronize();
-		reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)))->Refresh();
-		action->setEnabled(true);
-	});
+	/* FIXME: make this use a QThread; this is *very* unsafe */
+	AnimeListPage* page = reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)));
+	if (!async_synchronize_thread_.isRunning()) {
+		async_synchronize_thread_.SetAction(action);
+		async_synchronize_thread_.SetPage(page);
+		async_synchronize_thread_.start();
+	}
 }
 
 void MainWindow::RetranslateUI() {
--- a/src/main.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/main.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -19,6 +19,9 @@
 	app.setApplicationDisplayName("Minori");
 	app.setAttribute(Qt::AA_DontShowIconsInMenus, true);
 
+	qRegisterMetaType<std::vector<std::string>>(); /* window.cc */
+	qRegisterMetaType<std::vector<int>>(); /* search.cc */
+
 	session.config.Load();
 	Anime::db.LoadDatabaseFromDisk();
 
--- a/src/sys/osx/dark_theme.cc	Thu Apr 18 16:51:35 2024 -0400
+++ b/src/sys/osx/dark_theme.cc	Thu Apr 18 16:53:17 2024 -0400
@@ -5,8 +5,6 @@
 
 #include <CoreFoundation/CoreFoundation.h>
 
-#include <QOperatingSystemVersion>
-
 namespace osx {
 
 typedef id (*object_message_send)(id, SEL, ...);
@@ -39,7 +37,7 @@
 }
 
 bool DarkThemeAvailable() {
-	return (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave);
+	return (__builtin_available(macOS 10.14, *));
 }
 
 bool IsInDarkTheme() {