changeset 250:c130f47f6f48

*: many many changes e.g. the search page is actually implemented now!
author Paper <paper@paper.us.eu.org>
date Sun, 04 Feb 2024 21:17:17 -0500
parents 6b2441c776dd
children 4d635d3e168a
files Makefile.am configure.ac include/core/anime.h include/core/config.h include/core/session.h include/gui/pages/search.h include/gui/pages/statistics.h include/gui/window.h include/services/anilist.h include/services/services.h m4/autotroll.m4 m4/m4_ax_have_qt.m4 src/core/config.cc src/core/filesystem.cc src/core/strings.cc src/gui/dialog/information.cc src/gui/pages/anime_list.cc src/gui/pages/search.cc src/gui/pages/statistics.cc src/gui/pages/torrents.cc src/gui/widgets/anime_info.cc src/gui/widgets/sidebar.cc src/gui/window.cc src/main.cc src/services/anilist.cc src/services/services.cc src/sys/glib/dark_theme.cc src/sys/osx/filesystem.cc
diffstat 28 files changed, 1502 insertions(+), 584 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.am	Wed Jan 24 20:18:59 2024 -0500
+++ b/Makefile.am	Sun Feb 04 21:17:17 2024 -0500
@@ -5,23 +5,23 @@
 	rc/locale/es.ts
 
 .ts.qm:
-	@MKDIR_P@ `dirname $@`; \
-	@QT_LRELEASE@ $< -qm $@
+	$(MKDIR_P) `dirname $@`; \
+	$(LRELEASE) $< -qm $@
 
 minori_locale_qm = $(minori_locale_ts:.ts=.qm)
 
 # this has to be in the root build folder
-translations.qrc: $(minori_locale_qm)
+rc/locale/translations.qrc: $(minori_locale_qm)
 	printf "<!DOCTYPE rcc><RCC version=\"1.0\">\n\t<qresource prefix=\"locale/\">\n" > $@; \
 	for q in $(minori_locale_qm); do \
-		printf "\t\t<file alias=\"%s\">%s</file>\n" "`basename $$q`" "$$q" >> $@; \
-	done; \
+		printf "\t\t<file>%s</file>\n" "`basename $$q`" >> $@; \
+	done;
 	printf "\t</qresource>\n</RCC>\n" >> $@;
 
 minori_qtrc = \
 	$(top_srcdir)/rc/icons/icons.qrc	\
 	$(top_srcdir)/rc/player_data.qrc	\
-	translations.qrc
+	rc/locale/translations.qrc
 
 if BUILD_WIN
 
@@ -30,7 +30,7 @@
 endif
 
 rc/final_qrc.cc: $(minori_qtrc)
-	@QT_RCC@ -o $@ $(minori_qtrc)
+	$(RCC) -o $@ $(minori_qtrc)
 
 minori_qtheaders = \
 	include/core/http.h			\
@@ -84,8 +84,8 @@
 
 if BUILD_GLIB
 files_glib = src/sys/glib/dark_theme.cc
-cflags_glib = @GIO_CFLAGS@
-libs_glib = @GIO_LIBS@
+cflags_glib = $(GLIB_CFLAGS)
+libs_glib = $(GLIB_LIBS)
 endif
 
 if BUILD_WIN
@@ -101,8 +101,9 @@
 .rc.$(OBJEXT):
 	$(WINDRES) $(WRCFLAGS) -i $< -o $@
 files_windres=rc/win32/version.rc rc/win32/resource.rc
-endif
-endif
+endif # BUILD_WINDRES
+
+endif # BUILD_WIN
 
 if BUILD_OSX
 files_osx = src/sys/osx/dark_theme.cc src/sys/osx/filesystem.cc src/sys/osx/permissions.cc
@@ -156,9 +157,11 @@
 	$(files_osx)			\
 	$(files_glib)			\
 	$(files_win)			\
+	$(files_windres)
+
+nodist_minori_SOURCES = \
 	$(minori_moc_sources)	\
-	rc/final_qrc.cc		\
-	$(files_windres)
+	rc/final_qrc.cc
 
 minori_includes = \
 	-I$(top_srcdir)/include \
@@ -167,16 +170,16 @@
 	-I$(top_srcdir)/dep/anitomy \
 	-I$(top_srcdir)/dep
 
-minori_CPPFLAGS = @LIBCURL_CPPFLAGS@ $(minori_includes)
-minori_CXXFLAGS = @QT_CXXFLAGS@ $(cflags_osx) $(cflags_glib) $(cflags_win) -std=c++17
-minori_LDFLAGS = $(ldflags_osx) $(ldflags_win)
+minori_CPPFLAGS = $(QT_CPPFLAGS) $(LIBCURL_CPPFLAGS) $(minori_includes)
+minori_CXXFLAGS = $(cflags_osx) $(cflags_glib) $(cflags_win)
+minori_LDFLAGS = $(QT_LDFLAGS) $(ldflags_osx) $(ldflags_win)
 
 minori_DEPENDENCIES = dep/pugixml/libpugixml.la dep/animia/libanimia.la dep/anitomy/libanitomy.la
-minori_LDADD = $(libs_glib) $(libs_osx) $(libs_win) @LIBCURL@ @QT_LIBS@ dep/pugixml/libpugixml.la dep/animia/libanimia.la dep/anitomy/libanitomy.la
+minori_LDADD = $(libs_glib) $(LIBCURL) $(QT_LIBS) $(libs_osx) $(libs_win) $(minori_DEPENDENCIES)
 
 .h_moc.cc:
-	@MKDIR_P@ -- `dirname $@`
-	@QT_MOC@ -o $@ $(minori_includes) $<
+	$(MKDIR_P) -- `dirname $@`
+	$(MOC) -o $@ $(minori_includes) $<
 
 SUFFIXES = .h _moc.cc .ts .qm
 SUBDIRS = $(subdirs)
--- a/configure.ac	Wed Jan 24 20:18:59 2024 -0500
+++ b/configure.ac	Sun Feb 04 21:17:17 2024 -0500
@@ -9,61 +9,59 @@
 
 AM_INIT_AUTOMAKE([-Wall -Wportability foreign subdir-objects])
 
-# Do we have a C++17 compiler
+dnl Do we have a C++17 compiler
 : ${CXXFLAGS=""}
 AC_PROG_CXX
 AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
 
-# Init libtool
+dnl Init libtool
 AM_PROG_AR
 LT_INIT
 
-# Qt?
-AX_HAVE_QT
+dnl Qt?
+AT_WITH_QT([widgets gui core], [], [], [have_qt=no], [have_qt=yes])
+
+AS_IF([test "x$have_qt" = "xno"], [AC_MSG_ERROR([*** Qt not found.])])
 
-if test "x$have_qt" = "xno"; then
-	AC_MSG_ERROR([*** Qt not found.])
-fi
+dnl need this for moc
+AC_PROG_MKDIR_P
+AC_SUBST([MKDIR_P])
 
-# need this for moc
-AC_PROG_MKDIR_P
-
-# libcurl?
+dnl libcurl?
 LIBCURL_CHECK_CONFIG([yes], [7.7.2], [have_libcurl=yes], [have_libcurl=no])
 
-if test "x$have_libcurl" = "xno"; then
-	AC_MSG_ERROR([*** libcurl not found.])
-fi
+AS_IF([test "x$have_libcurl" = "xno"], [AC_MSG_ERROR([*** libcurl not found.])])
 
 build_windows=no
 build_osx=no
+build_linux=no
 build_glib=no
 
-case "${host_os}" in
-	cygwin*|mingw*)
-		# Windows
-		build_windows=yes
-		AC_CHECK_TOOL([WINDRES], [windres])
-		AC_SUBST(WINDRES)
-		AC_PROG_SED # We need sed for version numbers in windres
-		AC_SUBST(SED)
-		AC_DEFINE(WIN32)
-		;;
-	darwin*)
-		# Mac OS X
-		build_osx=yes
-		AC_DEFINE(MACOSX)
-		;;
-	*)
-		if test "x$host_os" = "xlinux"; then
-			AC_DEFINE(LINUX)
-		fi
-		# Everything else
-		AC_SUBST([GIO_CFLAGS])
-		AC_SUBST([GIO_LIBS])
-		PKG_CHECK_MODULES([GIO], [gio-2.0], [build_glib=yes], [])
-		;;
-esac
+AS_CASE(["$host_os"],
+	[cygwin*|mingw*], [build_windows=yes],
+	[darwin*], [build_osx=yes],
+	[linux*], [build_linux=yes],
+	[])
+
+if test "x$build_windows" = "xyes"; then
+	AC_DEFINE([WIN32])
+
+	dnl Check for windres
+	AC_CHECK_TOOL([WINDRES], [windres])
+	AC_SUBST([WINDRES])
+elif test "x$build_osx" = "xyes"; then
+	AC_DEFINE([MACOSX])
+else
+	AS_IF([test "x$build_linux" = "xyes"], [AC_DEFINE([linux])])
+
+	PKG_CHECK_MODULES([GLIB], [gio-2.0 glib-2.0], [build_glib=yes], [build_glib=no])
+	if test "x$build_glib" = "xyes"; then
+		AC_DEFINE([GLIB])
+
+		AC_SUBST([GLIB_CFLAGS])
+		AC_SUBST([GLIB_LIBS])
+	fi
+fi
 
 AM_CONDITIONAL([BUILD_WIN], [test "x$build_windows" = "xyes"])
 AM_CONDITIONAL([BUILD_OSX], [test "x$build_osx" = "xyes"])
--- a/include/core/anime.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/core/anime.h	Sun Feb 04 21:17:17 2024 -0500
@@ -147,7 +147,7 @@
 		std::vector<std::string> GetProducers() const;
 		SeriesFormat GetFormat() const;
 		SeriesSeason GetSeason() const;
-		int GetAudienceScore() const; /* should be double once MAL and Kitsu are implemented */
+		int GetAudienceScore() const;
 		std::string GetSynopsis() const;
 		int GetDuration() const;
 		std::string GetPosterUrl() const;
--- a/include/core/config.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/core/config.h	Sun Feb 04 21:17:17 2024 -0500
@@ -20,7 +20,7 @@
 class Config {
 	public:
 		int Load();
-		int Save() const;
+		int Save();
 
 		Anime::Services service;
 		Theme::Theme theme;
--- a/include/core/session.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/core/session.h	Sun Feb 04 21:17:17 2024 -0500
@@ -7,6 +7,8 @@
 
 #include "semver/semver.hpp"
 
+class MainWindow;
+
 struct Session {
 	public:
 		Session() { timer.start(); }
--- a/include/gui/pages/search.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/gui/pages/search.h	Sun Feb 04 21:17:17 2024 -0500
@@ -1,12 +1,66 @@
 #ifndef __gui__pages__search_h
 #define __gui__pages__search_h
-#include <QWidget>
+
+#include "core/anime.h"
+
+#include <QFrame>
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QItemSelection>
+
+class QTreeView;
+
+class SearchPageListSortFilter final : public QSortFilterProxyModel {
+		Q_OBJECT
+
+	public:
+		SearchPageListSortFilter(QObject* parent = nullptr);
+
+	protected:
+		bool lessThan(const QModelIndex& l, const QModelIndex& r) const override;
+};
+
+class SearchPageListModel final : public QAbstractListModel {
+		Q_OBJECT
 
-class SearchPage final : public QWidget {
+	public:
+		enum columns {
+			SR_TITLE,
+			SR_TYPE,
+			SR_EPISODES,
+			SR_SCORE,
+			SR_SEASON,
+
+			NB_COLUMNS
+		};
+
+		SearchPageListModel(QObject* parent);
+		~SearchPageListModel() override = default;
+		int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+		int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+		QVariant data(const QModelIndex& index, int role) const override;
+		QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override;
+		Qt::ItemFlags flags(const QModelIndex& index) const override;
+
+		void ParseSearch(const std::vector<int>& ids);
+		Anime::Anime* GetAnimeFromIndex(const QModelIndex& index) const;
+
+	private:
+		std::vector<int> ids;
+};
+
+class SearchPage final : public QFrame {
 		Q_OBJECT
 
 	public:
 		SearchPage(QWidget* parent = nullptr);
-};
+		void Search(const std::string& search);
+		void DisplayListMenu();
+		void ItemDoubleClicked();
 
+	private:
+		SearchPageListModel* model = nullptr;
+		SearchPageListSortFilter* sort_model = nullptr;
+		QTreeView* treeview = nullptr;
+};
 #endif // __gui__pages__search_h
--- a/include/gui/pages/statistics.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/gui/pages/statistics.h	Sun Feb 04 21:17:17 2024 -0500
@@ -22,9 +22,6 @@
 		void showEvent(QShowEvent*) override;
 
 	private:
-		std::string MinutesToDateString(int minutes);
-		std::string SecondsToDateString(int seconds);
-
 		std::shared_ptr<TextWidgets::LabelledSection> _anime_list;
 		std::shared_ptr<Graph<int>> _score_distribution_graph;
 		std::shared_ptr<TextWidgets::LabelledSection> _application;
--- a/include/gui/window.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/gui/window.h	Sun Feb 04 21:17:17 2024 -0500
@@ -13,6 +13,8 @@
 #include <QThread>
 #include "gui/widgets/sidebar.h"
 
+class QMenu;
+
 Q_DECLARE_METATYPE(std::vector<std::string>);
 
 class PlayingThread : public QThread {
@@ -32,11 +34,24 @@
 		Q_OBJECT
 
 	public:
+		enum class Pages {
+			NOW_PLAYING,
+
+			ANIME_LIST,
+			HISTORY,
+			STATISTICS,
+
+			SEARCH,
+			SEASONS,
+			TORRENTS
+		};
+
 		MainWindow(QWidget* parent = nullptr);
 		void SetActivePage(QWidget* page);
 		void CreateBars();
 		void AddMainWidgets();
 		void RetranslateUI();
+		void UpdateFolderMenu();
 		void AsyncSynchronize(QAction* action, QStackedWidget* stack);
 		void changeEvent(QEvent* event) override;
 		void showEvent(QShowEvent* event) override;
@@ -47,7 +62,9 @@
 		std::unique_ptr<QStackedWidget> stack = nullptr;
 		std::unique_ptr<SideBar> sidebar = nullptr;
 
-        std::unique_ptr<PlayingThread> thread = nullptr;
+		std::unique_ptr<PlayingThread> thread = nullptr;
+
+		QMenu* folder_menu = nullptr;
 };
 
 #endif // __window_h
--- a/include/services/anilist.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/services/anilist.h	Sun Feb 04 21:17:17 2024 -0500
@@ -1,9 +1,8 @@
 #ifndef __services__anilist_h
 #define __services__anilist_h
 
-#include "core/anime.h"
-#include "core/json.h"
-#include <curl/curl.h>
+#include <vector>
+#include <string>
 
 namespace Services {
 namespace AniList {
@@ -13,6 +12,9 @@
 /* Read queries */
 int GetAnimeList();
 
+/* Search query */
+std::vector<int> Search(const std::string& search);
+
 /* Write queries (mutations) */
 int UpdateAnimeEntry(int id);
 
--- a/include/services/services.h	Wed Jan 24 20:18:59 2024 -0500
+++ b/include/services/services.h	Sun Feb 04 21:17:17 2024 -0500
@@ -1,9 +1,13 @@
 #ifndef __services__services_h
 #define __services__services_h
 
+#include <vector>
+#include <string>
+
 namespace Services {
 
 void Synchronize();
+std::vector<int> Search(const std::string& search);
 void UpdateAnimeEntry(int id);
 bool Authorize();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m4/autotroll.m4	Sun Feb 04 21:17:17 2024 -0500
@@ -0,0 +1,720 @@
+# Build Qt apps with the autotools (Autoconf/Automake).
+# M4 macros.
+#
+# This file is part of AutoTroll.
+#
+# Copyright (C) 2006-2018  Benoit Sigoure <benoit.sigoure@lrde.epita.fr>
+# Copyright (C) 2012-2023  Werner Lemberg <wl@gnu.org>
+#
+# AutoTroll is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders of
+# AutoTroll give you unlimited permission to copy, distribute and
+# modify the configure scripts that are the output of Autoconf when
+# processing the macros of AutoTroll.  You need not follow the terms
+# of the GNU General Public License when using or distributing such
+# scripts, even though portions of the text of AutoTroll appear in
+# them.  The GNU General Public License (GPL) does govern all other
+# use of the material that constitutes AutoTroll.
+#
+# This special exception to the GPL applies to versions of AutoTroll
+# released by the copyright holders of AutoTroll.  Note that people
+# who make modified versions of AutoTroll are not obligated to grant
+# this special exception for their modified versions; it is their
+# choice whether to do so.  The GNU General Public License gives
+# permission to release a modified version without this exception;
+# this exception also makes it possible to release a modified version
+# which carries forward this exception.
+
+ # ------------- #
+ # DOCUMENTATION #
+ # ------------- #
+
+# Disclaimer: Tested with Qt 4.2, 4.8, Qt 5.x, and Qt 6 only.
+# Feedback welcome.  Simply invoke AT_WITH_QT in your configure.ac.
+# AT_WITH_QT can take arguments which are documented in depth below.
+# The default arguments are equivalent to the default .pro file
+# generated by qmake.
+#
+# Invoking AT_WITH_QT will do the following:
+#
+#  - Add option `--with-qt[=ARG]' to your configure script.  Possible
+#    values for ARG are `yes' (which is the default) and `no' to
+#    enable and disable Qt support, respectively, or a path to the
+#    directory which contains the Qt binaries in case you have a
+#    non-stardard location.
+#
+#  - Add option `--without-qt', which is equivalent to `--with-qt=no'.
+#
+#  - On MacOS, add `-spec macx-g++' or `-spec macx-lang' (if `$CXX
+#    --version' output contains `clang').  This can be overridden with
+#    the QMAKESPEC environment variable, for example
+#
+#      QMAKESPEC='macx-clang' ./configure ...
+#
+#    (The QMAKESPEC variable is honoured for non-MacOS builds also.)
+#
+#  - If Qt support is enabled, define C preprocessor macro HAVE_QT.
+#
+#  - Find the programs `qmake', `moc', `uic', and `rcc' and save them
+#    in the make variables $(QMAKE), $(MOC), $(UIC), and $(RCC).
+#
+#  - Save the path to Qt binaries in $(QT_PATH).
+#
+#  - Find the flags necessary to compile and link Qt, that is:
+#
+#     * $(QT_DEFINES): -D's defined by qmake.
+#     * $(QT_CFLAGS): CFLAGS as defined by qmake (C?!)
+#     * $(QT_CXXFLAGS): CXXFLAGS as defined by qmake.
+#     * $(QT_INCPATH): -I's defined by qmake.
+#     * $(QT_CPPFLAGS): Same as $(QT_DEFINES) + $(QT_INCPATH).
+#     * $(QT_LFLAGS): LFLAGS defined by qmake.
+#     * $(QT_LDFLAGS): Same thing as $(QT_LFLAGS).
+#     * $(QT_LIBS): LIBS defined by qmake.
+#
+#  - Provide @QT_STATIC_PLUGINS@, which holds some additional C++
+#    declarations necessary for linking with static Qt plugins (for
+#    dynamic Qt builds it contains a dummy typedef declaration
+#    instead).  Use this substitution in a `foo.cpp.in' C++ template
+#    file or something similar, which must be registered in
+#    configure.ac's call to AC_CONFIG_FILES so that a proper `foo.cpp'
+#    file gets created.  Then compile and link `foo.cpp' with your
+#    program in the usual automake way.
+#
+#    NOTE: It is not possible to automatically detect whether a Qt
+#    release earlier than version 5 is built as a static library!  For
+#    this reason, @QT_STATIC_PLUGINS@ always contains the dummy
+#    typedef declaration if not using Qt5.
+#
+# You *MUST* invoke $(MOC) and/or $(UIC) by yourself where necessary.
+# AutoTroll provides you with Makerules to ease this; here is a sample
+# Makefile.am to use with AutoTroll which builds the code given in
+# chapter 7 of the Qt Tutorial
+# (http://doc.trolltech.com/4.2/tutorial-t7.html).
+#
+# -------------------------------------------------------------------------
+# include $(top_srcdir)/build-aux/autotroll.mk
+#
+# ACLOCAL_AMFLAGS = -I build-aux
+#
+# bin_PROGRAMS = lcdrange
+# lcdrange_SOURCES  = $(BUILT_SOURCES) lcdrange.cpp lcdrange.h main.cpp
+# lcdrange_CXXFLAGS = $(QT_CXXFLAGS) $(AM_CXXFLAGS)
+# lcdrange_CPPFLAGS = $(QT_CPPFLAGS) $(AM_CPPFLAGS)
+# lcdrange_LDFLAGS  = $(QT_LDFLAGS) $(LDFLAGS)
+# lcdrange_LDADD    = $(QT_LIBS) $(LDADD)
+#
+# BUILT_SOURCES = lcdrange.moc.cpp
+# -------------------------------------------------------------------------
+#
+# Note that your MOC, UIC, and RCC files *MUST* be listed explicitly
+# in BUILT_SOURCES.  If you name them properly (e.g. `.moc.cc',
+# `.qrc.cc', `.ui.cc' -- of course you can use `.cpp' or `.cxx' or
+# `.C' rather than `.cc') AutoTroll will build them automagically for
+# you, using implicit rules defined in `autotroll.mk'.
+
+m4_define([_AUTOTROLL_SERIAL],
+  [m4_translit([
+# serial 17
+], [#
+], [])])
+
+
+m4_ifdef([AX_INSTEAD_IF],
+  [],
+  [AC_DEFUN([AX_INSTEAD_IF],
+    [m4_ifval([$1],
+      [AC_MSG_WARN([$2])
+       [$1]],
+      [AC_MSG_ERROR([$2])])])])
+
+
+# AX_PATH_TOOLS(VARIABLE, PROGS-TO-CHECK-FOR, [VALUE-IF-NOT-FOUND], [PATH])
+# -------------------------------------------------------------------------
+AC_DEFUN([AX_PATH_TOOLS],
+  [for ax_tool in $2; do
+     AC_PATH_TOOL([$1], [$ax_tool], [], [$4])
+     test -n "$$1" && break
+   done
+   m4_ifval([$3], [test -n "$$1" || $1="$3"])
+  ])
+
+
+m4_pattern_forbid([^AT_])
+m4_pattern_forbid([^_AT_])
+
+
+# AT_WITH_QT([QT_modules], [QT_config], [QT_misc], [RUN-IF-FAILED], [RUN-IF-OK])
+# ------------------------------------------------------------------------------
+# Enable Qt support and add an option --with-qt to the configure
+# script.
+#
+# The QT_modules argument is optional and defines extra modules to
+# enable or disable (it's equivalent to the QT variable in .pro
+# files).  Modules can be specified as follows:
+#
+# AT_WITH_QT   =>  No argument -> No QT value.
+#                                 Qmake sets it to "core gui" by
+#                                 default.
+# AT_WITH_QT([xml])   =>  QT += xml
+# AT_WITH_QT([+xml])  =>  QT += xml
+# AT_WITH_QT([-gui])  =>  QT -= gui
+# AT_WITH_QT([xml -gui +sql svg])  =>  QT += xml sql svg
+#                                      QT -= gui
+#
+# The QT_config argument is also optional and follows the same
+# convention as QT_modules.  Instead of changing the QT variable, it
+# changes the CONFIG variable, which is used to tweak configuration
+# and compiler options.
+#
+# The last argument, QT_misc (also optional) will be copied as-is the
+# .pro file used to guess how to compile Qt apps.  You may use it to
+# further tweak the build process of Qt apps if tweaking the QT or
+# CONFIG variables isn't enough for you (for example, to control which
+# static plugins get used).
+#
+# RUN-IF-FAILED is arbitrary code to execute if Qt cannot be found or
+# if any problem happens.  If this argument is omitted, then
+# AC_MSG_ERROR will be called.  RUN-IF-OK is arbitrary code to execute
+# if Qt was successfully found.
+
+AC_DEFUN([AT_WITH_QT],
+  [AC_REQUIRE([AC_CANONICAL_HOST])
+   AC_REQUIRE([AC_CANONICAL_BUILD])
+   AC_REQUIRE([AC_PROG_CXX])
+
+   echo "$as_me: this is autotroll.m4[]_AUTOTROLL_SERIAL" \
+     >& AS_MESSAGE_LOG_FD
+
+   # This is a hack to get decent flow control with `break'.
+   for _qt_ignored in once; do
+
+     AC_ARG_WITH([qt],
+       AS_HELP_STRING([--with-qt@<:@=ARG@:>@],
+         [Qt support.  ARG can be `yes' (the default), `no',
+          or a path to Qt binaries; if `yes' or empty,
+          use PATH and some default directories to find Qt binaries]))
+
+     if test x"$with_qt" = x"no"; then
+       break
+     else
+       AC_DEFINE([HAVE_QT],[1],
+         [Define if the Qt framework is available.])
+     fi
+
+     if test x"$with_qt" = x"yes"; then
+       QT_PATH=
+     else
+       QT_PATH=$with_qt
+     fi
+
+     # Find Qt.
+     AC_ARG_VAR([QT_PATH],
+       [path to Qt binaries])
+     QT_TOOL_PATH=$QT_PATH:$PATH
+
+     # Find qmake.
+     AC_ARG_VAR([QMAKE],
+       [Qt Makefile generator command])
+     AX_PATH_TOOLS([QMAKE],
+       [qmake qmake-qt5 qmake-qt4 qmake-qt3],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test x"$QMAKE" = xmissing; then
+       if test x"$with_qt" = "x"; then
+          with_qt="no"
+       else
+         AX_INSTEAD_IF([$4],
+           [Cannot find qmake.  Try --with-qt=PATH.])
+       fi
+       break
+     fi
+
+     # Find moc (Meta Object Compiler).
+     AC_ARG_VAR([MOC],
+       [Qt Meta Object Compiler command])
+     AX_PATH_TOOLS([MOC],
+       [moc moc-qt5 moc-qt4 moc-qt3],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test x"$MOC" = xmissing; then
+       AX_INSTEAD_IF([$4],
+         [Cannot find moc (Meta Object Compiler).  Try --with-qt=PATH.])
+       break
+     fi
+
+     # Find uic (User Interface Compiler).
+     AC_ARG_VAR([UIC],
+       [Qt User Interface Compiler command])
+     AX_PATH_TOOLS([UIC],
+       [uic uic-qt5 uic-qt4 uic-qt3 uic3],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test x"$UIC" = xmissing; then
+       AX_INSTEAD_IF([$4],
+         [Cannot find uic (User Interface Compiler).  Try --with-qt=PATH.])
+       break
+     fi
+
+     # Find rcc (Qt Resource Compiler).
+     AC_ARG_VAR([RCC],
+       [Qt Resource Compiler command])
+     AX_PATH_TOOLS([RCC],
+       [rcc rcc-qt5],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test x"$RCC" = xmissing; then
+       AC_MSG_WARN(
+         [Cannot find rcc (Qt Resource Compiler).  Try --with-qt=PATH.])
+     fi
+
+     AC_ARG_VAR([LUPDATE],
+       [Qt Linguist updater command])
+     AX_PATH_TOOLS([LUPDATE],
+       [lupdate lupdate-qt5 lrelease-qt6],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test "x$LUPDATE" = "xmissing"; then
+       AC_MSG_WARN(
+         [Cannot find lupdate (Qt Linguist updater).  Try --with-qt=PATH.])
+     fi
+
+     AC_ARG_VAR([LRELEASE],
+       [Qt Linguist compiler command])
+     AX_PATH_TOOLS([LRELEASE],
+       [lrelease lrelease-qt5 lrelease-qt6],
+       [missing],
+       [$QT_TOOL_PATH])
+     if test "x$LRELEASE" = "xmissing"; then
+       AC_MSG_WARN(
+         [Cannot find lrelease (Qt Linguist compiler).  Try --with-qt=PATH.])
+     fi
+
+     AC_MSG_CHECKING([whether host operating system is Darwin])
+     at_darwin=no
+     at_qmake_args=
+     case $host_os in
+       dnl (
+       darwin*)
+         at_darwin=yes
+         ;;
+     esac
+     AC_MSG_RESULT([$at_darwin])
+
+     AC_MSG_CHECKING([whether QMAKESPEC environment variable is set])
+     if test x"$QMAKESPEC" = x; then
+       if test x"$at_darwin" = xyes; then
+         if $CXX --version | grep -q -i clang; then
+           at_qmake_args='-spec macx-clang'
+         else
+           at_qmake_args='-spec macx-g++'
+         fi
+         AC_MSG_RESULT([no, using $at_qmake_args])
+       else
+         AC_MSG_RESULT([no])
+       fi
+     else
+       AC_MSG_RESULT([yes, using $QMAKESPEC])
+     fi
+
+     # If we don't know the path to Qt, guess it from the path to
+     # qmake.
+     if test x"$QT_PATH" = x; then
+       QT_PATH=`dirname "$QMAKE"`
+     fi
+     if test x"$QT_PATH" = x; then
+       AX_INSTEAD_IF([$4],
+         [Cannot find your Qt installation.  Try --with-qt=PATH.])
+       break
+     fi
+     AC_SUBST([QT_PATH])
+
+     # Get ready to build a test-app with Qt.
+     if mkdir conftest.dir \
+        && cd conftest.dir; then
+       :
+     else
+       AX_INSTEAD_IF([$4],
+         [Cannot mkdir conftest.dir or cd to that directory.])
+       break
+     fi
+
+     cat >conftest.h <<_ASEOF
+
+#include <QObject>
+
+class Foo: public QObject
+{
+  Q_OBJECT;
+public:
+  Foo();
+  ~Foo() {}
+public Q_SLOTS:
+  void setValue(int value);
+Q_SIGNALS:
+  void valueChanged(int newValue);
+private:
+  int value_;
+};
+
+_ASEOF
+
+     cat >conftest.cpp <<_ASEOF
+
+#include "conftest.h"
+
+Foo::Foo()
+  : value_ (42)
+{
+  connect(this, SIGNAL(valueChanged(int)),
+          this, SLOT(setValue(int)));
+}
+
+void Foo::setValue(int value)
+{
+  value_ = value;
+}
+
+int main()
+{
+  Foo f;
+}
+
+_ASEOF
+
+     if $QMAKE -project; then
+       :
+     else
+       AX_INSTEAD_IF([$4],
+         [Calling $QMAKE -project failed.])
+       break
+     fi
+
+     # Find the .pro file generated by qmake.
+     pro_file=conftest.dir.pro
+     test -f $pro_file || pro_file=`echo *.pro`
+     if test -f "$pro_file"; then
+       :
+     else
+       AX_INSTEAD_IF([$4],
+         [Can't find the .pro file generated by Qmake.])
+       break
+     fi
+
+     dnl This is for Qt5; for Qt4 it does nothing special.
+     _AT_TWEAK_PRO_FILE([QT], [+widgets])
+
+     dnl Undocumented qmake: always use absolute paths.
+     dnl Defaults to 4.
+     _AT_TWEAK_PRO_FILE([QMAKE_PROJECT_DEPTH], [0])
+
+     dnl Tweak the value of QT in the .pro file if we have a first
+     dnl argument.
+     m4_ifval([$1],
+       [_AT_TWEAK_PRO_FILE([QT], [$1])])
+
+     dnl Tweak the value of CONFIG in the .pro file if we have a
+     dnl second argument.
+     m4_ifval([$2],
+       [_AT_TWEAK_PRO_FILE([CONFIG], [$2])])
+
+     m4_ifval([$3],
+       [ # Add the extra-settings the user wants to set in the .pro
+         # file.
+         echo "$3" >>"$pro_file"
+       ])
+
+     echo "$as_me:$LINENO: Invoking $QMAKE on $pro_file" \
+       >& AS_MESSAGE_LOG_FD
+     sed 's/^/| /' "$pro_file" >& AS_MESSAGE_LOG_FD
+
+     if $QMAKE $at_qmake_args; then
+       :
+     else
+       AX_INSTEAD_IF([$4],
+         [Calling $QMAKE $at_qmake_args failed.])
+       break
+     fi
+
+     # Try to compile a simple Qt app.
+     AC_CACHE_CHECK([whether we can build a simple Qt application],
+       [at_cv_qt_build],
+       [at_cv_qt_build=ko
+        : ${MAKE=make}
+
+        if $MAKE >& AS_MESSAGE_LOG_FD 2>&1; then
+          at_cv_qt_build='ok, looks like Qt 4, Qt 5, or Qt 6'
+        else
+          echo "$as_me:$LINENO: Build failed, trying to #include <qobject.h> instead" \
+            >& AS_MESSAGE_LOG_FD
+          sed 's/<QObject>/<qobject.h>/' conftest.h > tmp.h \
+            && mv tmp.h conftest.h
+          if $MAKE >& AS_MESSAGE_LOG_FD 2>&1; then
+            at_cv_qt_build='ok, looks like Qt 3'
+          else
+            # Sometimes (such as on Debian) build will fail because Qt
+            # hasn't been installed in debug mode and qmake tries (by
+            # default) to build apps in debug mode => Try again in
+            # release mode.
+            echo "$as_me:$LINENO: Build failed, trying to enforce release mode" \
+              >& AS_MESSAGE_LOG_FD
+
+            _AT_TWEAK_PRO_FILE([CONFIG], [+release])
+
+            sed 's/<qobject.h>/<QObject>/' conftest.h > tmp.h \
+              && mv tmp.h conftest.h
+            if $MAKE >& AS_MESSAGE_LOG_FD 2>&1; then
+              at_cv_qt_build='ok, looks like Qt 4 or Qt 5, release mode forced'
+            else
+              echo "$as_me:$LINENO: Build failed, trying to #include <qobject.h> instead" \
+                >& AS_MESSAGE_LOG_FD
+              sed 's/<QObject>/<qobject.h>/' conftest.h > tmp.h \
+                && mv tmp.h conftest.h
+              if $MAKE >& AS_MESSAGE_LOG_FD 2>&1; then
+                at_cv_qt_build='ok, looks like Qt 3, release mode forced'
+              else
+                at_cv_qt_build=ko
+                echo "$as_me:$LINENO: failed program was:" \
+                  >& AS_MESSAGE_LOG_FD
+                sed 's/^/| /' conftest.h >& AS_MESSAGE_LOG_FD
+                echo "$as_me:$LINENO: failed program was:" \
+                  >& AS_MESSAGE_LOG_FD
+                sed 's/^/| /' conftest.cpp >& AS_MESSAGE_LOG_FD
+              fi # if make with Qt3-style #include and release mode forced.
+            fi # if make with Qt4/5-style #include and release mode forced.
+          fi # if make with Qt3-style #include.
+        fi # if make with Qt4/5-style #include.
+       ])dnl end: AC_CACHE_CHECK(at_cv_qt_build)
+
+     if test x"$at_cv_qt_build" = xko; then
+       AX_INSTEAD_IF([$4],
+         [Cannot build a test Qt program])
+       cd ..
+       break
+     fi
+
+     QT_VERSION_MAJOR=`echo "$at_cv_qt_build" | sed 's/[[^0-9]]*//g'`
+     AC_SUBST([QT_VERSION_MAJOR])
+
+     # This sed filter is applied after an expression of the form
+     # /^FOO.*=/!d; it starts by removing the beginning of the line
+     # (using the empty regular expression //, which repeats the last
+     # regular expression match), removing references to SUBLIBS,
+     # removing unnecessary whitespace at the beginning, then prefixing
+     # our exported variables with QT_.  Note that `LDFLAGS' is
+     # intentionally omitted.
+     qt_sed_filter='s///;
+                    s/$(SUBLIBS)//g;
+                    s/^ *//;
+                    s/\$(DEFINES)/$(QT_DEFINES)/g;
+                    s/\$(CFLAGS)/$(QT_CFLAGS)/g;
+                    s/\$(CXXFLAGS)/$(QT_CXXFLAGS)/g;
+                    s/\$(INCPATH)/$(QT_INCPATH)/g;
+                    s/\$(CPPFLAGS)/$(QT_CPPFLAGS)/g;
+                    s/\$(LFLAGS)/$(QT_LFLAGS)/g;
+                    s/\$(LIBS)/$(QT_LIBS)/g'
+
+     # Find the Makefile (qmake happens to generate a fake Makefile
+     # which invokes a Makefile.Debug or Makefile.Release).  If we
+     # have both, we'll pick the Makefile.Release.  The reason is that
+     # this release uses -Os and debug -g.  We can override -Os by
+     # passing another -O but we usually don't override -g.
+     if test -f Makefile.Release; then
+       at_mfile='Makefile.Release'
+     else
+       at_mfile='Makefile'
+     fi
+     if test -f $at_mfile; then
+       :
+     else
+       AX_INSTEAD_IF([$4],
+         [Cannot find the Makefile generated by qmake.])
+       cd ..
+       break
+     fi
+
+     # Find the DEFINES of Qt (should have been named CPPFLAGS).
+     AC_CACHE_CHECK([for the DEFINES to use with Qt],
+       [at_cv_env_QT_DEFINES],
+       [at_cv_env_QT_DEFINES=`sed "/^DEFINES@<:@^A-Z=@:>@*=/!d;
+                                   $qt_sed_filter" $at_mfile`])
+     AC_SUBST([QT_DEFINES],
+       [$at_cv_env_QT_DEFINES])
+
+     # Find the CFLAGS of Qt.  (We can use Qt in C?!)
+     AC_CACHE_CHECK([for the CFLAGS to use with Qt],
+       [at_cv_env_QT_CFLAGS],
+       [at_cv_env_QT_CFLAGS=`sed "/^CFLAGS@<:@^A-Z=@:>@*=/!d;
+                                  $qt_sed_filter" $at_mfile`])
+     AC_SUBST([QT_CFLAGS],
+       [$at_cv_env_QT_CFLAGS])
+
+     # Find the CXXFLAGS of Qt.
+     AC_CACHE_CHECK([for the CXXFLAGS to use with Qt],
+       [at_cv_env_QT_CXXFLAGS],
+       [at_cv_env_QT_CXXFLAGS=`sed "/^CXXFLAGS@<:@^A-Z=@:>@*=/!d;
+                                    $qt_sed_filter" $at_mfile`])
+     AC_SUBST([QT_CXXFLAGS],
+       [$at_cv_env_QT_CXXFLAGS])
+
+     # Find the INCPATH of Qt.
+     AC_CACHE_CHECK([for the INCPATH to use with Qt],
+       [at_cv_env_QT_INCPATH],
+       [at_cv_env_QT_INCPATH=`sed "/^INCPATH@<:@^A-Z=@:>@*=/!d;
+                                   $qt_sed_filter" $at_mfile`])
+     AC_SUBST([QT_INCPATH],
+       [$at_cv_env_QT_INCPATH])
+
+     AC_SUBST([QT_CPPFLAGS],
+       ["$at_cv_env_QT_DEFINES $at_cv_env_QT_INCPATH"])
+
+     # Find the LFLAGS of Qt (should have been named LDFLAGS).
+     AC_CACHE_CHECK([for the LDFLAGS to use with Qt],
+       [at_cv_env_QT_LDFLAGS],
+       [at_cv_env_QT_LDFLAGS=`sed "/^LFLAGS@<:@^A-Z=@:>@*=/!d;
+                                   $qt_sed_filter" $at_mfile`])
+     AC_SUBST([QT_LFLAGS],
+       [$at_cv_env_QT_LDFLAGS])
+     AC_SUBST([QT_LDFLAGS],
+       [$at_cv_env_QT_LDFLAGS])
+
+     # Find the LIBS of Qt.
+     AC_CACHE_CHECK([for the LIBS to use with Qt],
+      [at_cv_env_QT_LIBS],
+      [at_cv_env_QT_LIBS=`sed "/^LIBS@<:@^A-Z@:>@*=/!d;
+                               $qt_sed_filter" $at_mfile`
+       if test x$at_darwin = xyes; then
+         # Fix QT_LIBS: as of today Libtool (GNU Libtool 1.5.23a)
+         # doesn't handle -F properly.  The "bug" has been fixed on 22
+         # October 2006 by Peter O'Gorman but we provide backward
+         # compatibility here.
+         at_cv_env_QT_LIBS=`echo "$at_cv_env_QT_LIBS" \
+                            | sed 's/^-F/-Wl,-F/;
+                                   s/ -F/ -Wl,-F/g'`
+       fi])
+     AC_SUBST([QT_LIBS],
+       [$at_cv_env_QT_LIBS])
+
+     # We can't use AC_CACHE_CHECK for data that contains newlines.
+     AC_MSG_CHECKING([for necessary static plugin code])
+     # find static plugin data generated by qmake
+     if test -f conftest.dir_plugin_import.cpp; then
+       QT_STATIC_PLUGINS=`cat conftest.dir_plugin_import.cpp`
+     else
+       QT_STATIC_PLUGINS="\
+// We have Qt earlier than version 5 or a dynamic build.
+// Provide dummy typedef to avoid empty source code.
+typedef int _qt_not_a_static_build;"
+     fi
+     AC_SUBST([QT_STATIC_PLUGINS])
+     AM_SUBST_NOTMAKE([QT_STATIC_PLUGINS])
+     AC_MSG_RESULT([$QT_STATIC_PLUGINS])
+
+     cd .. && rm -rf conftest.dir
+
+     # Run the user code
+     $5
+
+   done  # end hack (useless FOR to be able to use break)
+  ])
+
+
+# AT_REQUIRE_QT_VERSION(QT_version, [RUN-IF-FAILED], [RUN-IF-OK])
+# ---------------------------------------------------------------
+# Check (using qmake) that Qt's version "matches" QT_version.  Must be
+# run *AFTER* AT_WITH_QT.  Requires autoconf 2.60.
+#
+# This macro is ignored if Qt support has been disabled (using
+# `--with-qt=no' or `--without-qt').
+#
+# RUN-IF-FAILED is arbitrary code to execute if Qt cannot be found or
+# if any problem happens.  If this argument is omitted, then
+# AC_MSG_ERROR will be called.  RUN-IF-OK is arbitrary code to execute
+# if Qt was successfully found.
+#
+# This macro provides the Qt version in $(QT_VERSION).
+
+AC_DEFUN([AT_REQUIRE_QT_VERSION],
+  [AC_PREREQ([2.60])
+
+   # This is a hack to get decent flow control with `break'.
+   for _qt_ignored in once; do
+
+     if test x"$with_qt" = x"no"; then
+       break
+     fi
+
+     if test x"$QMAKE" = x; then
+       AX_INSTEAD_IF([$2],
+        [\$QMAKE is empty.  Did you invoke AT@&t@_WITH_QT before AT@&t@_REQUIRE_QT_VERSION?])
+       break
+     fi
+
+     AC_CACHE_CHECK([for Qt's version],
+       [at_cv_QT_VERSION],
+       [echo "$as_me:$LINENO: Running $QMAKE --version:" \
+          >& AS_MESSAGE_LOG_FD
+        $QMAKE --version >& AS_MESSAGE_LOG_FD 2>&1
+        qmake_version_sed=['/^.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*$/!d;s//\1/']
+        at_cv_QT_VERSION=`$QMAKE --version 2>&1 \
+                          | sed "$qmake_version_sed"`])
+     if test x"$at_cv_QT_VERSION" = x; then
+       AX_INSTEAD_IF([$2],
+         [Cannot detect Qt's version.])
+       break
+     fi
+     AC_SUBST([QT_VERSION],
+       [$at_cv_QT_VERSION])
+     AS_VERSION_COMPARE([$QT_VERSION], [$1],
+       [AX_INSTEAD_IF([$2],
+          [This package requires Qt $1 or above.])
+        break])
+
+     # Run the user code
+     $3
+
+   done  # end hack (useless FOR to be able to use break)
+  ])
+
+
+# _AT_TWEAK_PRO_FILE(QT_VAR, VALUE)
+# ---------------------------------
+# @internal.  Tweak the variable QT_VAR in the .pro file.  VALUE is an
+# IFS-separated list of values, and each value is rewritten as
+# follows:
+#
+#   +value  => QT_VAR += value
+#   -value  => QT_VAR -= value
+#    value  => QT_VAR += value
+
+AC_DEFUN([_AT_TWEAK_PRO_FILE],
+  [ # Tweak the value of $1 in the .pro file for $2.
+   qt_conf=''
+   for at_mod in $2; do
+     at_mod=`echo "$at_mod" | sed 's/^-//; tough
+                                   s/^+//; beef
+                                   :ough
+                                   s/^/$1 -= /;n
+                                   :eef
+                                   s/^/$1 += /'`
+     qt_conf="\
+$qt_conf
+$at_mod"
+   done
+    echo "$qt_conf" | sed 1d >>"$pro_file"
+  ])
+
+# eof
--- a/m4/m4_ax_have_qt.m4	Wed Jan 24 20:18:59 2024 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +0,0 @@
-# ===========================================================================
-#        https://www.gnu.org/software/autoconf-archive/ax_have_qt.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-#   AX_HAVE_QT
-#
-# DESCRIPTION
-#
-#   Searches $PATH and queries qmake for Qt include files, libraries and Qt
-#   binary utilities. The macro only supports Qt5 or later.
-#
-#   The following shell variable is set to either "yes" or "no":
-#
-#     have_qt
-#
-#   Additionally, the following variables are exported:
-#
-#     QT_CXXFLAGS
-#     QT_LIBS
-#     QT_MOC
-#     QT_UIC
-#     QT_RCC
-#     QT_LRELEASE
-#     QT_LUPDATE
-#     QT_DIR
-#     QMAKE
-#
-#   which respectively contain an "-I" flag pointing to the Qt include
-#   directory, link flags necessary to link with Qt and X, the full path to
-#   the meta object compiler and the user interface compiler both, and
-#   finally the variable QTDIR as Qt likes to see it defined.
-#
-#   Example lines for Makefile.in:
-#
-#     CXXFLAGS = @QT_CXXFLAGS@
-#     MOC      = @QT_MOC@
-#
-#   After the variables have been set, a trial compile and link is performed
-#   to check the correct functioning of the meta object compiler. This test
-#   may fail when the different detected elements stem from different
-#   releases of the Qt framework. In that case, an error message is emitted
-#   and configure stops.
-#
-#   No common variables such as $LIBS or $CFLAGS are polluted.
-#
-# LICENSE
-#
-#   Copyright (c) 2008 Bastiaan Veelo <Bastiaan@Veelo.net>
-#   Copyright (c) 2014 Alex Henrie <alexhenrie24@gmail.com>
-#   Copyright (c) 2024 Paper <mrpapersonic@gmail.com>
-#
-#   Copying and distribution of this file, with or without modification, are
-#   permitted in any medium without royalty provided the copyright notice
-#   and this notice are preserved. This file is offered as-is, without any
-#   warranty.
-
-#serial 25
-
-AU_ALIAS([BNV_HAVE_QT], [AX_HAVE_QT])
-AC_DEFUN([AX_HAVE_QT],
-[
-	AC_REQUIRE([AC_PROG_CXX])
-	AC_REQUIRE([AC_PATH_X])
-	AC_REQUIRE([AC_PATH_XTRA])
-	# openSUSE leap 15.3 installs qmake-qt5, not qmake, for example.
-	# Store the full name (like qmake-qt5) into QMAKE
-	# and the specifier (like -qt5 or empty) into am_have_qt_qmexe_suff.
-	AC_ARG_VAR([QMAKE],"Qt make tool")
-	AC_CHECK_TOOLS([QMAKE],[qmake qmake-qt6 qmake-qt5],[false])
-
-	AC_MSG_CHECKING(for Qt)
-	am_have_qt_qmexe_suff=`echo $QMAKE | sed 's,^.*qmake,,'`
-	# If we have Qt5 or later in the path, we're golden
-	ver=`$QMAKE --version | grep -o "Qt version ."`
-
-	if test "$ver" ">" "Qt version 4"; then
-		have_qt=yes
-		# This pro file dumps qmake's variables, but it only works on Qt 5 or later
-		am_have_qt_dir=`mktemp -d`
-		am_have_qt_pro="$am_have_qt_dir/test.pro"
-		am_have_qt_stash="$am_have_qt_dir/.qmake.stash"
-		am_have_qt_makefile="$am_have_qt_dir/Makefile"
-		# http://qt-project.org/doc/qt-5/qmake-variable-reference.html#qt
-		cat > $am_have_qt_pro << EOF
-win32 {
-		CONFIG -= debug_and_release
-		CONFIG += release
-}
-
-CONFIG += sdk_no_version_check
-
-# use absolute paths, useful on windows
-# where qmake really loves giving relative paths
-QMAKE_PROJECT_DEPTH = 0
-
-# commented out all the modules we don't use
-#qtHaveModule(axcontainer):       QT += axcontainer
-#qtHaveModule(axserver):          QT += axserver
-#qtHaveModule(concurrent):        QT += concurrent
-qtHaveModule(core):              QT += core
-#qtHaveModule(dbus):              QT += dbus
-#qtHaveModule(declarative):       QT += declarative
-#qtHaveModule(designer):          QT += designer
-qtHaveModule(gui):               QT += gui
-#qtHaveModule(help):              QT += help
-#qtHaveModule(multimedia):        QT += multimedia
-#qtHaveModule(multimediawidgets): QT += multimediawidgets
-#qtHaveModule(network):           QT += network
-#qtHaveModule(opengl):            QT += opengl
-#qtHaveModule(printsupport):      QT += printsupport
-#qtHaveModule(qml):               QT += qml
-#qtHaveModule(qmltest):           QT += qmltest
-#qtHaveModule(x11extras):         QT += x11extras
-#qtHaveModule(script):            QT += script
-#qtHaveModule(scripttools):       QT += scripttools
-#qtHaveModule(sensors):           QT += sensors
-#qtHaveModule(serialport):        QT += serialport
-#qtHaveModule(sql):               QT += sql
-#qtHaveModule(svg):               QT += svg
-#qtHaveModule(testlib):           QT += testlib
-#qtHaveModule(uitools):           QT += uitools
-#qtHaveModule(webkit):            QT += webkit
-#qtHaveModule(webkitwidgets):     QT += webkitwidgets
-#qtHaveModule(xml):               QT += xml
-#qtHaveModule(xmlpatterns):       QT += xmlpatterns
-qtHaveModule(widgets):           QT += widgets
-percent.target = %
-percent.commands = @echo -n "\$(\$(@))\ "
-QMAKE_EXTRA_TARGETS += percent
-EOF
-		am_have_qt_makefile_cxxflags=`cat << EOF
-include $am_have_qt_makefile
-
-VAR:
-	@echo \\$(CXXFLAGS) \\$(INCPATH)
-EOF`
-		am_have_qt_makefile_libs=`cat << EOF
-include $am_have_qt_makefile
-
-VAR:
-	@echo \\$(LIBS)
-EOF`
-		$QMAKE "$am_have_qt_pro" -o "$am_have_qt_makefile"
-		QT_CXXFLAGS=`cd $am_have_qt_dir; echo "\$am_have_qt_makefile_cxxflags" | make -s -f - VAR`
-		QT_LIBS=`cd $am_have_qt_dir; echo "\$am_have_qt_makefile_libs" | make -s -f - VAR`
-		rm $am_have_qt_pro $am_have_qt_stash $am_have_qt_makefile
-		rmdir $am_have_qt_dir
-
-		# Look for specific tools in $PATH
-		QT_MOC=`which moc$am_have_qt_qmexe_suff`
-		QT_UIC=`which uic$am_have_qt_qmexe_suff`
-		QT_RCC=`which rcc$am_have_qt_qmexe_suff`
-		QT_LRELEASE=`which lrelease$am_have_qt_qmexe_suff`
-		QT_LUPDATE=`which lupdate$am_have_qt_qmexe_suff`
-
-		# Get Qt version from qmake
-		QT_DIR=`$QMAKE --version | grep -o -E /.+`
-
-		# All variables are defined, report the result
-		AC_MSG_RESULT([$have_qt:
-		QT_CXXFLAGS=$QT_CXXFLAGS
-		QT_DIR=$QT_DIR
-		QT_LIBS=$QT_LIBS
-		QT_UIC=$QT_UIC
-		QT_MOC=$QT_MOC
-		QT_RCC=$QT_RCC
-		QT_LRELEASE=$QT_LRELEASE
-		QT_LUPDATE=$QT_LUPDATE])
-	else
-		# Qt was not found
-		have_qt=no
-		QT_CXXFLAGS=
-		QT_DIR=
-		QT_LIBS=
-		QT_UIC=
-		QT_MOC=
-		QT_RCC=
-		QT_LRELEASE=
-		QT_LUPDATE=
-		AC_MSG_RESULT($have_qt)
-	fi
-	AC_SUBST(QT_CXXFLAGS)
-	AC_SUBST(QT_DIR)
-	AC_SUBST(QT_LIBS)
-	AC_SUBST(QT_UIC)
-	AC_SUBST(QT_MOC)
-	AC_SUBST(QT_RCC)
-	AC_SUBST(QT_LRELEASE)
-	AC_SUBST(QT_LUPDATE)
-	AC_SUBST(QMAKE)
-
-	#### Being paranoid:
-	if test x"$have_qt" = xyes; then
-		AC_MSG_CHECKING(correct functioning of Qt installation)
-		AC_CACHE_VAL(ax_cv_qt_test_result,
-		[
-			cat > ax_qt_test.h << EOF
-#include <qobject.h>
-class Test : public QObject
-{
-Q_OBJECT
-public:
-	Test() {}
-	~Test() {}
-public slots:
-	void receive() {}
-signals:
-	void send();
-};
-EOF
-
-			cat > ax_qt_main.$ac_ext << EOF
-#include "ax_qt_test.h"
-#include <qapplication.h>
-int main( int argc, char **argv )
-{
-	QApplication app( argc, argv );
-	Test t;
-	QObject::connect( &t, SIGNAL(send()), &t, SLOT(receive()) );
-}
-EOF
-
-			ax_cv_qt_test_result="failure"
-			ax_try_1="$QT_MOC ax_qt_test.h -o moc_ax_qt_test.$ac_ext >/dev/null 2>/dev/null"
-			AC_TRY_EVAL(ax_try_1)
-			if test x"$ac_status" != x0; then
-				echo "$ax_err_1" >&AS_MESSAGE_LOG_FD
-				echo "configure: could not run $QT_MOC on:" >&AS_MESSAGE_LOG_FD
-				cat ax_qt_test.h >&AS_MESSAGE_LOG_FD
-			else
-				ax_try_2="$CXX $QT_CXXFLAGS -c $CXXFLAGS -o moc_ax_qt_test.o moc_ax_qt_test.$ac_ext >/dev/null 2>/dev/null"
-				AC_TRY_EVAL(ax_try_2)
-				if test x"$ac_status" != x0; then
-					echo "$ax_err_2" >&AS_MESSAGE_LOG_FD
-					echo "configure: could not compile:" >&AS_MESSAGE_LOG_FD
-					cat moc_ax_qt_test.$ac_ext >&AS_MESSAGE_LOG_FD
-				else
-					ax_try_3="$CXX $QT_CXXFLAGS -c $CXXFLAGS -o ax_qt_main.o ax_qt_main.$ac_ext >/dev/null 2>/dev/null"
-					AC_TRY_EVAL(ax_try_3)
-					if test x"$ac_status" != x0; then
-						echo "$ax_err_3" >&AS_MESSAGE_LOG_FD
-						echo "configure: could not compile:" >&AS_MESSAGE_LOG_FD
-						cat ax_qt_main.$ac_ext >&AS_MESSAGE_LOG_FD
-					else
-						ax_try_4="$CXX -o ax_qt_main ax_qt_main.o moc_ax_qt_test.o $QT_LIBS $LIBS >/dev/null 2>/dev/null"
-						AC_TRY_EVAL(ax_try_4)
-						if test x"$ac_status" != x0; then
-							echo "$ax_err_4" >&AS_MESSAGE_LOG_FD
-						else
-							ax_cv_qt_test_result="success"
-						fi
-					fi
-				fi
-			fi
-		])dnl AC_CACHE_VAL ax_cv_qt_test_result
-		AC_MSG_RESULT([$ax_cv_qt_test_result])
-		if test x"$ax_cv_qt_test_result" = "xfailure"; then
-			AC_MSG_ERROR([Failed to find matching components of a complete
-						  Qt installation. Try using more options,
-						  see ./configure --help.])
-		fi
-
-		rm -f ax_qt_test.h moc_ax_qt_test.$ac_ext moc_ax_qt_test.o \
-					ax_qt_main.$ac_ext ax_qt_main.o ax_qt_main
-	fi
-])
--- a/src/core/config.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/core/config.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -23,6 +23,8 @@
 #include <QFile>
 #include <QTextStream>
 
+#include <iostream>
+
 /* I'll use an INI-based config file instead of using an
  * XML file like Taiga.
  *
@@ -89,14 +91,16 @@
 		}
 	}
 
+	locale.RefreshAvailableLocales();
 	locale.SetActiveLocale(QLocale(Strings::ToQString(INI::GetIniValue<std::string>(ini, "General", "Locale", "en_US"))));
 
 	theme.SetTheme(Translate::ToTheme(INI::GetIniValue<std::string>(ini, "Appearance", "Theme", "Default")));
 
 	{
 		std::vector<std::string> v = Strings::Split(INI::GetIniValue<std::string>(ini, "Library", "Folders", ""), ";");
-		library.paths = std::set(std::make_move_iterator(v.begin()),
-								 std::make_move_iterator(v.end()));
+		for (const auto& s : v)
+			if (!library.paths.count(s))
+				library.paths.insert(s);
 	}
 
 	library.real_time_monitor = INI::GetIniValue<bool>(ini, "Library", "Real-time monitor", true);
@@ -104,7 +108,7 @@
 	return 0;
 }
 
-int Config::Save() const {
+int Config::Save() {
 	std::filesystem::path cfg_path = Filesystem::GetConfigPath();
 	Filesystem::CreateDirectories(cfg_path);
 
--- a/src/core/filesystem.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/core/filesystem.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -1,23 +1,10 @@
-#ifdef WIN32
-#	include <shlobj.h>
-#elif defined(MACOSX)
-#	include "sys/osx/filesystem.h"
-#elif defined(__linux__)
-#	include <pwd.h>
-#	include <sys/types.h>
-#endif
-
-#ifndef WIN32
-#	include <errno.h>
-#	include <unistd.h>
-#	include <sys/stat.h>
-#endif
-
 #include "core/filesystem.h"
 #include "core/config.h"
 #include "core/strings.h"
+
+#include <QStandardPaths>
+
 #include <filesystem>
-#include <limits.h>
 
 namespace Filesystem {
 
@@ -33,39 +20,18 @@
 }
 
 std::filesystem::path GetDotPath() {
+	/*
+	 * Windows: ~/AppData/Roaming/Minori
+	 * macOS: ~/Library/Application Support/Minori
+	 * ...: ~/.config/minori
+	 *
+	 * FIXME: are windows and mac properly cased?
+	*/
 #ifdef WIN32
-	std::filesystem::path path;
-	wchar_t* buf;
-
-	if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, NULL, &buf) == S_OK)
-		path = buf;
-	else
-		return std::filesystem::path();
-
-	CoTaskMemFree(buf);
-
-	return path / CONFIG_DIR;
-#elif defined(MACOSX)
-	std::string appsupport;
-	if (!osx::GetApplicationSupportDirectory(appsupport))
-		return "";
-
-	return std::filesystem::path(appsupport) / CONFIG_DIR;
-#else // just assume POSIX
-	std::filesystem::path path;
-	const char* home = getenv("HOME");
-
-#	ifdef __linux__
-	if (!home)
-		home = getpwuid(getuid())->pw_dir;
-#	endif // __linux__
-
-	/* only do this if the home directory was really found */
-	if (home)
-		return std::filesystem::path(home) / ".config" / CONFIG_DIR;
-	else
-		return std::filesystem::path();
-#endif     // !WIN32 && !MACOSX
+	return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+#else
+	return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
+#endif
 }
 
 std::filesystem::path GetConfigPath() {
--- a/src/core/strings.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/core/strings.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -23,7 +23,7 @@
 /* ew */
 std::string Implode(const std::vector<std::string>& vector, const std::string& delimiter) {
 	if (vector.size() < 1)
-		return "-";
+		return "";
 
 	std::string out;
 
@@ -38,7 +38,7 @@
 
 std::string Implode(const std::set<std::string>& set, const std::string& delimiter) {
 	if (set.size() < 1)
-		return "-";
+		return "";
 
 	std::string out;
 
@@ -52,6 +52,9 @@
 }
 
 std::vector<std::string> Split(const std::string &text, const std::string& delimiter) {
+	if (text.length() < 1)
+		return {};
+
 	std::vector<std::string> tokens;
 
 	std::size_t start = 0, end = 0;
@@ -91,7 +94,8 @@
 }
 
 /* removes dumb HTML tags because anilist is aids and
-   gives us HTML for synopses :/ */
+ * gives us HTML for synopses :/
+*/
 std::string RemoveHtmlTags(std::string string) {
 	while (string.find("<") != std::string::npos) {
 		auto startpos = string.find("<");
--- a/src/gui/dialog/information.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/dialog/information.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -30,6 +30,9 @@
 /* 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) {
+	if (!anime.IsInUserList())
+		return;
+
 	anime.SetUserProgress(_progress);
 	anime.SetUserScore(_score);
 	anime.SetUserIsRewatching(_rewatching);
@@ -98,7 +101,7 @@
 					tabbed_widget->addTab(main_information_widget, tr("Main information"));
 				}
 
-				{
+				if (anime.IsInUserList()) {
 					/* My list and settings */
 					QWidget* settings_widget = new QWidget(tabbed_widget);
 					settings_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
--- a/src/gui/pages/anime_list.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/pages/anime_list.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -275,7 +275,6 @@
 void AnimeListPage::DisplayListMenu() {
 	QMenu* menu = new QMenu(this);
 	menu->setAttribute(Qt::WA_DeleteOnClose);
-	menu->setTitle(tr("Column visibility"));
 	menu->setToolTipsVisible(true);
 
 	AnimeListPageModel* source_model =
@@ -424,6 +423,7 @@
 
 void AnimeListPage::showEvent(QShowEvent*) {
 	SetupLayout();
+	Refresh();
 }
 
 /* --------- QTabWidget replication end ---------- */
@@ -497,6 +497,4 @@
 	SetColumnDefaults();
 	setFocusPolicy(Qt::TabFocus);
 	setFocusProxy(tab_bar);
-
-	Refresh();
 }
--- a/src/gui/pages/search.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/pages/search.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -1,4 +1,346 @@
 #include "gui/pages/search.h"
+#include "core/anime.h"
+#include "core/anime_db.h"
+#include "core/strings.h"
+#include "core/http.h"
+#include "core/session.h"
+#include "core/filesystem.h"
+#include "gui/widgets/text.h"
+#include "gui/dialog/information.h"
+#include "track/media.h"
+#include "gui/translate/anime.h"
+#include "services/services.h"
+
+#include <QHeaderView>
+#include <QVBoxLayout>
+#include <QToolBar>
+#include <QTreeView>
+#include <QDate>
+#include <QMenu>
+
+#include <iostream>
+#include <sstream>
+#include <fstream>
+#include <algorithm>
+
+#include "pugixml.hpp"
+#include "anitomy/anitomy.h"
+
+SearchPageListSortFilter::SearchPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
+}
+
+bool SearchPageListSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
+	QVariant left = sourceModel()->data(l, sortRole());
+	QVariant right = sourceModel()->data(r, sortRole());
+
+	switch (left.userType()) {
+		case QMetaType::Int:
+		case QMetaType::UInt:
+		case QMetaType::LongLong:
+		case QMetaType::ULongLong:
+			return left.toInt() < right.toInt();
+		case QMetaType::QDate:
+			return left.toDate() < right.toDate();
+		case QMetaType::QString:
+		default: // meh
+			return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
+	}
+}
+
+/* -------------------------------------------- */
+
+SearchPageListModel::SearchPageListModel(QObject* parent) : QAbstractListModel(parent) {
+}
+
+void SearchPageListModel::ParseSearch(const std::vector<int>& ids) {
+	/* hack!!! */
+	if (!rowCount(index(0))) {
+		beginInsertRows(QModelIndex(), 0, 0);
+		endInsertRows();
+	}
+
+	beginResetModel();
+
+	this->ids = ids;
+
+	endResetModel();
+}
+
+int SearchPageListModel::rowCount(const QModelIndex& parent) const {
+	return ids.size();
+	(void)(parent);
+}
+
+int SearchPageListModel::columnCount(const QModelIndex& parent) const {
+	return NB_COLUMNS;
+	(void)(parent);
+}
+
+QVariant SearchPageListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
+	switch (role) {
+		case Qt::DisplayRole: {
+			switch (section) {
+				case SR_TITLE: return tr("Anime title");
+				case SR_EPISODES: return tr("Episode");
+				case SR_TYPE: return tr("Type");
+				case SR_SCORE: return tr("Score");
+				case SR_SEASON: return tr("Season");
+				default: return {};
+			}
+			break;
+		}
+		case Qt::TextAlignmentRole: {
+			switch (section) {
+				case SR_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+				case SR_TYPE: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
+				case SR_EPISODES:
+				case SR_SCORE:
+				case SR_SEASON: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+				default: return {};
+			}
+			break;
+		}
+	}
+	return QAbstractListModel::headerData(section, orientation, role);
+}
+
+QVariant SearchPageListModel::data(const QModelIndex& index, int role) const {
+	if (!index.isValid())
+		return QVariant();
+
+	const Anime::Anime& anime = Anime::db.items[ids[index.row()]];
+
+	switch (role) {
+		case Qt::DisplayRole:
+			switch (index.column()) {
+				case SR_TITLE: return Strings::ToQString(anime.GetUserPreferredTitle());
+				case SR_TYPE: return Strings::ToQString(Translate::ToLocalString(anime.GetFormat()));
+				case SR_EPISODES: return anime.GetEpisodes();
+				case SR_SCORE: return QString::number(anime.GetAudienceScore()) + "%";
+				case SR_SEASON: return Strings::ToQString(Translate::ToLocalString(anime.GetSeason())) + " " + QString::number(anime.GetAirDate().GetYear().value_or(2000));
+				default: return {};
+			}
+			break;
+		case Qt::UserRole:
+			switch (index.column()) {
+				case SR_SCORE: return anime.GetAudienceScore();
+				case SR_EPISODES: return anime.GetEpisodes();
+				case SR_SEASON: return anime.GetAirDate().GetAsQDate();
+				/* We have to use this to work around some stupid
+				 * "conversion ambiguous" error on Linux
+				*/
+				default: return data(index, Qt::DisplayRole);
+			}
+			break;
+		case Qt::SizeHintRole: {
+			switch (index.column()) {
+				default: {
+					/* max horizontal size of 100, height size = size of current font */
+					const QString d = data(index, Qt::DisplayRole).toString();
+					const QFontMetrics metric = QFontMetrics(QFont());
+
+					return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height());
+				}
+			}
+			break;
+		}
+		case Qt::TextAlignmentRole:
+			return headerData(index.column(), Qt::Horizontal, Qt::TextAlignmentRole);
+	}
+	return QVariant();
+}
+
+Qt::ItemFlags SearchPageListModel::flags(const QModelIndex& index) const {
+	if (!index.isValid())
+		return Qt::NoItemFlags;
+
+	return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+Anime::Anime* SearchPageListModel::GetAnimeFromIndex(const QModelIndex& index) const {
+	return &Anime::db.items[ids[index.row()]];
+}
+
+void SearchPage::DisplayListMenu() {
+	QMenu* menu = new QMenu(this);
+	menu->setAttribute(Qt::WA_DeleteOnClose);
+	menu->setToolTipsVisible(true);
+
+	const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());
+
+	bool add_to_list_enable = true;
 
-SearchPage::SearchPage(QWidget* parent) : QWidget(parent) {
+	std::set<Anime::Anime*> animes;
+	for (const auto& index : selection.indexes()) {
+		if (!index.isValid())
+			continue;
+
+		Anime::Anime* anime = model->GetAnimeFromIndex(index);
+		if (anime) {
+			animes.insert(anime);
+			if (anime->IsInUserList())
+				add_to_list_enable = false;
+		}
+	}
+
+	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);
+
+			dialog->show();
+			dialog->raise();
+			dialog->activateWindow();
+		}
+	});
+	menu->addSeparator();
+	{
+		QMenu* submenu = menu->addMenu(tr("Add to list..."));
+		submenu->addAction(tr("Currently watching"), [animes]{
+			for (auto& anime : animes) {
+				if (!anime->IsInUserList())
+					anime->AddToUserList();
+				anime->SetUserStatus(Anime::ListStatus::CURRENT);
+				Services::UpdateAnimeEntry(anime->GetId());
+			}
+		});
+		submenu->addAction(tr("Completed"), [animes]{
+			for (auto& anime : animes) {
+				if (!anime->IsInUserList())
+					anime->AddToUserList();
+				anime->SetUserStatus(Anime::ListStatus::COMPLETED);
+				Services::UpdateAnimeEntry(anime->GetId());
+			}
+		});
+		submenu->addAction(tr("On hold"), [animes]{
+			for (auto& anime : animes) {
+				if (!anime->IsInUserList())
+					anime->AddToUserList();
+				anime->SetUserStatus(Anime::ListStatus::PAUSED);
+				Services::UpdateAnimeEntry(anime->GetId());
+			}
+		});
+		submenu->addAction(tr("Dropped"), [animes]{
+			for (auto& anime : animes) {
+				if (!anime->IsInUserList())
+					anime->AddToUserList();
+				anime->SetUserStatus(Anime::ListStatus::DROPPED);
+				Services::UpdateAnimeEntry(anime->GetId());
+			}
+		});
+		submenu->addAction(tr("Plan to watch"), [animes]{
+			for (auto& anime : animes) {
+				if (!anime->IsInUserList())
+					anime->AddToUserList();
+				anime->SetUserStatus(Anime::ListStatus::PLANNING);
+				Services::UpdateAnimeEntry(anime->GetId());
+			}
+		});
+		submenu->setEnabled(add_to_list_enable);
+	}
+	menu->popup(QCursor::pos());
 }
+
+void SearchPage::ItemDoubleClicked() {
+	/* throw out any other garbage */
+	const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());
+	if (!selection.indexes().first().isValid())
+		return;
+
+	const QModelIndex index = model->index(selection.indexes().first().row());
+	Anime::Anime* anime = model->GetAnimeFromIndex(index);
+
+	InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
+		//UpdateAnime(anime->GetId());
+	}, InformationDialog::PAGE_MAIN_INFO, this);
+
+	dialog->show();
+	dialog->raise();
+	dialog->activateWindow();
+}
+
+SearchPage::SearchPage(QWidget* parent) : QFrame(parent) {
+	setFrameShape(QFrame::Box);
+	setFrameShadow(QFrame::Sunken);
+
+	QVBoxLayout* layout = new QVBoxLayout(this);
+	layout->setContentsMargins(0, 0, 0, 0);
+	layout->setSpacing(0);
+
+	{
+		/* Toolbar */
+		QToolBar* toolbar = new QToolBar(this);
+		toolbar->setMovable(false);
+
+		{
+			QLineEdit* line_edit = new QLineEdit("", toolbar);
+			connect(line_edit, &QLineEdit::returnPressed, this, [this, line_edit]{
+				/* static thread here. */
+				static QThread* thread = nullptr;
+
+				if (thread)
+					return;
+
+				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();
+			});
+			toolbar->addWidget(line_edit);
+		}
+
+		layout->addWidget(toolbar);
+	}
+
+	{
+		QFrame* line = new QFrame(this);
+		line->setFrameShape(QFrame::HLine);
+		line->setFrameShadow(QFrame::Sunken);
+		line->setLineWidth(1);
+		layout->addWidget(line);
+	}
+
+	{
+		treeview = new QTreeView(this);
+		treeview->setUniformRowHeights(true);
+		treeview->setAllColumnsShowFocus(false);
+		treeview->setAlternatingRowColors(true);
+		treeview->setSortingEnabled(true);
+		treeview->setSelectionMode(QAbstractItemView::ExtendedSelection);
+		treeview->setItemsExpandable(false);
+		treeview->setRootIsDecorated(false);
+		treeview->setContextMenuPolicy(Qt::CustomContextMenu);
+		treeview->setFrameShape(QFrame::NoFrame);
+
+		{
+			sort_model = new SearchPageListSortFilter(treeview);
+			model = new SearchPageListModel(treeview);
+			sort_model->setSourceModel(model);
+			sort_model->setSortRole(Qt::UserRole);
+			sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
+			treeview->setModel(sort_model);
+		}
+
+		// set column sizes
+		treeview->setColumnWidth(SearchPageListModel::SR_TITLE,    400);
+		treeview->setColumnWidth(SearchPageListModel::SR_TYPE,     60);
+		treeview->setColumnWidth(SearchPageListModel::SR_EPISODES, 60);
+		treeview->setColumnWidth(SearchPageListModel::SR_SCORE,    60);
+		treeview->setColumnWidth(SearchPageListModel::SR_SEASON,   100);
+
+		treeview->header()->setStretchLastSection(false);
+
+		/* Double click stuff */
+		connect(treeview, &QAbstractItemView::doubleClicked, this, &SearchPage::ItemDoubleClicked);
+		connect(treeview, &QWidget::customContextMenuRequested, this, &SearchPage::DisplayListMenu);
+
+		layout->addWidget(treeview);
+	}
+}
--- a/src/gui/pages/statistics.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/pages/statistics.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -14,6 +14,12 @@
 #include <QWidget>
 
 #include <sstream>
+#include <cmath>
+
+enum class TimeUnits {
+	SECONDS,
+	MINUTES
+};
 
 StatisticsPage::StatisticsPage(QWidget* parent) : QFrame(parent) {
 	QVBoxLayout* layout = new QVBoxLayout(this);
@@ -74,47 +80,49 @@
 	UpdateStatistics();
 }
 
-/* me abusing macros :) */
-static void add_time_segment(std::ostringstream& str, int x, const std::string_view& s, const std::string_view& p) {
-	if (x > 0)
-		str << x << ((x == 1) ? s : p);
-}
+/*  [in] enum TimeUnits unit:
+ *       which unit to stop on
+ *  [in] int amount:
+ *       amount of units to parse
+ *  [in, defaults to 1.0] double unit_in_seconds:
+ *       equivalent of one of 'amount' in seconds, e.g. minutes would be 60.0
+*/
+static std::string TimeToDateString(TimeUnits unit, int amount, double unit_in_seconds = 1.0) {
+	/* avoid calculating this twice */
+	const double years_conv = (31556952.0 / unit_in_seconds);
+	const double months_conv = (2629746.0 / unit_in_seconds);
+	const double days_conv = (86400.0 / unit_in_seconds);
+	const double hours_conv = (3600.0 / unit_in_seconds);
+	const double minutes_conv = (60.0 / unit_in_seconds);
+	const double seconds_conv = (1.0 / unit_in_seconds);
 
-std::string StatisticsPage::MinutesToDateString(const int minutes) {
-	/* ew */
-	int years = (minutes * (1 / 525949.2F));
-	int months = (minutes * (1 / 43829.1F)) - (years * 12);
-	int days = (minutes * (1 / 1440.0F)) - (years * 365.2425F) - (months * 30.436875F);
-	int hours = (minutes * (1 / 60.0F)) - (years * 8765.82F) - (months * 730.485F) - (days * 24);
-	int rest_minutes = (minutes) - (years * 525949.2F) - (months * 43829.1F) - (days * 1440) - (hours * 60);
-	std::ostringstream return_stream;
-	add_time_segment(return_stream, years, " year ", " years ");
-	add_time_segment(return_stream, months, " month ", " months ");
-	add_time_segment(return_stream, days, " day ", " days ");
-	add_time_segment(return_stream, hours, " hour ", " hours ");
-	if (rest_minutes > 0 || return_stream.str().size() == 0)
-		return_stream << rest_minutes << ((rest_minutes == 1) ? " minute" : " minutes");
-	return return_stream.str();
-}
+	const int years   = amount / years_conv;
+	const int months  = std::fmod(amount, years_conv)   / months_conv;
+	const int days    = std::fmod(amount, months_conv)  / days_conv;
+	const int hours   = std::fmod(amount, days_conv)    / hours_conv;
+	const int minutes = std::fmod(amount, hours_conv)   / minutes_conv;
+	const int seconds = std::fmod(amount, minutes_conv) / seconds_conv;
+
+	const auto add_time_segment = [](std::ostringstream& str, int amount, const std::string_view& singular, const std::string_view& plural, bool always = false) {
+		if (amount > 0 || always)
+			str << amount << ((amount == 1) ? singular : plural);
+	};
 
-std::string StatisticsPage::SecondsToDateString(const int sec) {
-	/* this is all fairly unnecessary, but works:tm: */
-	int years = sec * (1 / 31556952.0F);
-	int months = sec * (1 / 2629746.0F) - (years * 12);
-	int days = sec * (1 / 86400.0F) - (years * 365.2425F) - (months * 30.436875F);
-	int hours = sec * (1 / 3600.0F) - (years * 8765.82F) - (months * 730.485F) - (days * 24);
-	int minutes = (sec) * (1 / 60.0F) - (years * 525949.2F) - (months * 43829.1F) - (days * 1440.0F) - (hours * 60.0F);
-	int seconds =
-	    sec - (years * 31556952.0F) - (months * 2629746.0F) - (days * 86400.0F) - (hours * 3600.0F) - (minutes * 60.0F);
-	std::ostringstream return_stream;
-	add_time_segment(return_stream, years, " year ", " years ");
-	add_time_segment(return_stream, months, " month ", " months ");
-	add_time_segment(return_stream, days, " day ", " days ");
-	add_time_segment(return_stream, hours, " hour ", " hours ");
-	add_time_segment(return_stream, minutes, " minute ", " minutes ");
-	if (seconds > 0 || return_stream.str().size() == 0)
-		return_stream << seconds << ((seconds == 1) ? " second" : " seconds");
-	return return_stream.str();
+	std::ostringstream string;
+	add_time_segment(string, years, " year ", " years ");
+	add_time_segment(string, months, " month ", " months ");
+	add_time_segment(string, days, " day ", " days ");
+	add_time_segment(string, hours, " hour ", " hours ");
+
+	if (unit == TimeUnits::MINUTES) {
+		add_time_segment(string, minutes, " minute", " minutes", true);
+		return string.str();
+	} else {
+		add_time_segment(string, minutes, " minute ", " minutes ");
+	}
+
+	add_time_segment(string, seconds, " second", " seconds", true);
+	return string.str();
 }
 
 inline int GetTotalWithScore(const int score) {
@@ -131,8 +139,8 @@
 	QTextStream ts(&string);
 	ts << Anime::db.GetTotalAnimeAmount() << '\n';
 	ts << Anime::db.GetTotalEpisodeAmount() << '\n';
-	ts << MinutesToDateString(Anime::db.GetTotalWatchedAmount()).c_str() << '\n';
-	ts << MinutesToDateString(Anime::db.GetTotalPlannedAmount()).c_str() << '\n';
+	ts << Strings::ToQString(TimeToDateString(TimeUnits::MINUTES, Anime::db.GetTotalWatchedAmount(), 60.0)) << '\n';
+	ts << Strings::ToQString(TimeToDateString(TimeUnits::MINUTES, Anime::db.GetTotalPlannedAmount(), 60.0)) << '\n';
 	ts << Anime::db.GetAverageScore() << '\n';
 	ts << Anime::db.GetScoreDeviation();
 	_anime_list->GetParagraph()->SetText(string);
@@ -142,7 +150,7 @@
 		_score_distribution_graph->AddItem(i, GetTotalWithScore(i));
 
 	string = "";
-	ts << Strings::ToQString(SecondsToDateString(session.uptime() / 1000)) << '\n';
+	ts << Strings::ToQString(TimeToDateString(TimeUnits::SECONDS, session.uptime() / 1000)) << '\n';
 	ts << session.GetRequests();
 	/* Application */
 	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
--- a/src/gui/pages/torrents.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/pages/torrents.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -6,10 +6,10 @@
 #include "gui/widgets/text.h"
 #include "track/media.h"
 
+#include <QHeaderView>
 #include <QVBoxLayout>
 #include <QToolBar>
 #include <QTreeView>
-#include <QMainWindow>
 #include <QByteArray>
 #include <QDataStream>
 #include <QThread>
@@ -24,10 +24,11 @@
 #include "anitomy/anitomy.h"
 
 /* This file is very, very similar to the anime list page.
-
-   It differs from Taiga in that it uses tabs instead of
-   those "groups", but those are custom painted and a pain in the ass to
-   maintain over multiple platforms. */
+ *
+ * It differs from Taiga in that it uses tabs instead of
+ * those "groups", but those are custom painted and a pain in the ass to
+ * maintain over multiple platforms.
+*/
 
 TorrentsPageListSortFilter::TorrentsPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
 }
@@ -398,6 +399,8 @@
 		treeview->setColumnWidth(TorrentsPageListModel::TL_FILENAME,    200);
 		treeview->setColumnWidth(TorrentsPageListModel::TL_RELEASEDATE, 190);
 
+		treeview->header()->setStretchLastSection(false);
+
 		layout->addWidget(treeview);
 	}
 }
--- a/src/gui/widgets/anime_info.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/widgets/anime_info.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -35,12 +35,13 @@
 	 * QString because QTextStream sucks and assumes
 	 * Latin1 (on Windows?)
 	*/
+	const auto genres = anime.GetGenres();
 	details_data_s << Strings::ToQString(Translate::ToLocalString(anime.GetFormat())) << "\n"
 	               << anime.GetEpisodes() << "\n"
-	               << Strings::ToQString(Translate::ToLocalString(anime.GetUserStatus())) << "\n"
+	               << Strings::ToQString(Translate::ToLocalString(anime.GetAiringStatus())) << "\n"
 	               << Strings::ToQString(Translate::ToLocalString(anime.GetSeason())) << " "
 	                   << anime.GetAirDate().GetYear().value_or(2000) << "\n"
-	               << Strings::ToQString(Strings::Implode(anime.GetGenres(), ", ")) << "\n"
+	               << Strings::ToQString((genres.size() > 1) ? Strings::Implode(genres, ", ") : "-") << "\n"
 	               << anime.GetAudienceScore() << "%";
 	_details->GetParagraph()->SetText(details_data);
 
--- a/src/gui/widgets/sidebar.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/widgets/sidebar.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -4,6 +4,8 @@
 #include <QListWidgetItem>
 #include <QMouseEvent>
 
+#include <iostream>
+
 SideBar::SideBar(QWidget* parent) : QListWidget(parent) {
 	setFrameShape(QFrame::NoFrame);
 	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -30,6 +32,7 @@
 
 void SideBar::SetBackgroundColor(QColor color) {
 	viewport()->setAutoFillBackground(color != Qt::transparent);
+
 	QPalette pal(palette());
 	pal.setColor(QPalette::Window, color);
 	setPalette(pal);
@@ -66,26 +69,30 @@
 }
 
 int SideBar::AddSeparatorsToIndex(int index) {
-	int i, j;
-	for (i = 0, j = 0; i < index;) {
-		i++;
-		if (IndexIsSeparator(indexFromItem(item(i))))
-			j++;
+	int i = 0, separators = 0, items = 0;
+
+	for (; items <= index; ) {
+		if (IndexIsSeparator(indexFromItem(item(i++)))) {
+			separators++;
+		} else {
+			items++;
+		}
 	}
-	return i + j;
+
+	return index + separators;
 }
 
 int SideBar::RemoveSeparatorsFromIndex(int index) {
-	int i, j;
-	for (i = 0, j = 0; i < index; i++) {
+	int i = 0, items = 0;
+	for (; i < index; i++) {
 		if (!IndexIsSeparator(indexFromItem(item(i))))
-			j++;
+			items++;
 	}
-	return j;
+	return items;
 }
 
 bool SideBar::IndexIsSeparator(QModelIndex index) const {
-	return !(index.isValid() && index.flags() & Qt::ItemIsEnabled);
+	return !index.isValid() || !(index.flags() & Qt::ItemIsEnabled);
 }
 
 QItemSelectionModel::SelectionFlags SideBar::selectionCommand(const QModelIndex& index, const QEvent*) const {
--- a/src/gui/window.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/gui/window.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -48,18 +48,6 @@
 #	include "sys/win32/dark_theme.h"
 #endif
 
-enum class Pages {
-	NOW_PLAYING,
-
-	ANIME_LIST,
-	HISTORY,
-	STATISTICS,
-
-	SEARCH,
-	SEASONS,
-	TORRENTS
-};
-
 void PlayingThread::run() {
 	std::vector<std::string> files;
 	Track::Media::GetCurrentlyPlaying(files);
@@ -124,6 +112,7 @@
 
 void MainWindow::AddMainWidgets() {
 	int page = static_cast<int>(Pages::ANIME_LIST);
+
 	if (sidebar.get()) {
 		main_widget->layout()->removeWidget(sidebar.get());
 		sidebar.reset();
@@ -149,10 +138,13 @@
 	sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
 
 	stack.reset(new QStackedWidget(main_widget.get()));
+
 	stack->addWidget(new NowPlayingPage(main_widget.get()));
+	/* ---- */
 	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()));
@@ -166,7 +158,6 @@
 
 void MainWindow::CreateBars() {
 	QMenuBar* menubar = new QMenuBar(this);
-	QMenu* folder_menu; /* this is used twice, so we declare it here */
 	QAction* sync_action;
 
 	{
@@ -176,36 +167,7 @@
 		{
 			folder_menu = menu->addMenu(tr("&Library folders"));
 
-			/* add in all of our existing folders... */
-			std::size_t i = 0;
-			for (const auto& path : session.config.library.paths) {
-				const QString folder = Strings::ToQString(path);
-				QAction* action = folder_menu->addAction(folder, [folder]{
-					QDesktopServices::openUrl(QUrl::fromLocalFile(folder));
-				});
-				if (i < 9)
-					action->setShortcut(QKeySequence(Qt::ALT | static_cast<Qt::Modifier>(Qt::Key_1 + i)));
-				else if (i == 9)
-					action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_0));
-				/* don't bother with a shortcut in case of more... */
-				i++;
-			}
-
-			folder_menu->addSeparator();
-
-			{
-				folder_menu->addAction(tr("&Add new folder..."), [this]{
-					const QString dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"),
-																		  QDir::homePath(),
-																		  QFileDialog::ShowDirsOnly
-																		  | QFileDialog::DontResolveSymlinks);
-					if (dir.isEmpty())
-						return;
-					session.config.library.paths.insert(Strings::ToUtf8String(dir));
-					/* we have to recreate the menu bar to add the new folder */
-					CreateBars();
-				});
-			}
+			UpdateFolderMenu();
 		}
 
 		{
@@ -229,7 +191,7 @@
 		menu->addSeparator();
 
 		{
-			QAction* action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit);
+			QAction* action = menu->addAction(tr("E&xit"), this, &MainWindow::close);
 			action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q));
 		}
 	}
@@ -314,7 +276,7 @@
 			QAction* action = menu->addAction(tr("&Settings"), [this] {
 				SettingsDialog dialog(this);
 				dialog.exec();
-				CreateBars();
+				UpdateFolderMenu();
 			});
 			action->setMenuRole(QAction::PreferencesRole);
 		}
@@ -326,60 +288,78 @@
 
 		{
 			/* Pages... */
-			std::map<QAction*, int> page_to_index_map = {};
-
-			QActionGroup* pages_group = new QActionGroup(this);
+			QActionGroup* pages_group = new QActionGroup(menu);
 			pages_group->setExclusive(true);
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 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);
-				page_to_index_map[action] = 1;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(1);
+				});
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&History")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 2;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(2);
+				});
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Statistics")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 3;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(3);
+				});
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("S&earch")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 4;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(4);
+				});
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("Se&asons")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 5;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(5);
+				});
 			}
 
 			{
 				QAction* action = pages_group->addAction(menu->addAction(tr("&Torrents")));
 				action->setCheckable(true);
-				page_to_index_map[action] = 6;
+				connect(action, &QAction::toggled, this, [this] {
+					sidebar->SetCurrentItem(6);
+				});
 			}
 
 			/* pain in my ass */
-			connect(sidebar.get(), &SideBar::CurrentItemChanged, this,
-					[pages_group](int index) { pages_group->actions()[index]->setChecked(true); });
+			connect(sidebar.get(), &SideBar::CurrentItemChanged, this, [pages_group](int index) {
+				QAction* checked = pages_group->checkedAction();
 
-			connect(pages_group, &QActionGroup::triggered, this,
-					[this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); });
+				const QList<QAction*>& actions = pages_group->actions();
+				if (index > actions.size())
+					return;
+
+				if (checked)
+					checked->setChecked(false);
+				actions[index]->setChecked(true);
+			});
 		}
 
 		menu->addSeparator();
@@ -479,12 +459,55 @@
 		toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] {
 			SettingsDialog dialog(this);
 			dialog.exec();
-			CreateBars();
+			/* library folders might have changed! */
+			UpdateFolderMenu();
 		});
 		addToolBar(toolbar);
 	}
 }
 
+void MainWindow::UpdateFolderMenu() {
+	if (!folder_menu)
+		return;
+
+	folder_menu->clear();
+
+	/* add in all of our existing folders... */
+	std::size_t i = 0;
+	for (const auto& path : session.config.library.paths) {
+		const QString folder = Strings::ToQString(path);
+		QAction* action = folder_menu->addAction(folder, [folder]{
+			QDesktopServices::openUrl(QUrl::fromLocalFile(folder));
+		});
+
+		if (i < 9) {
+			/* Qt::Key_1 is equivalent to 1 in ASCII, so we can use the same
+			 * stupid `'0' + i` trick here
+			*/
+			action->setShortcut(QKeySequence(Qt::ALT | static_cast<Qt::Modifier>(Qt::Key_1 + i)));
+		} else if (i == 9) {
+			action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_0));
+		}
+		/* don't bother with a shortcut in case of more... */
+		i++;
+	}
+
+	folder_menu->addSeparator();
+
+	{
+		folder_menu->addAction(tr("&Add new folder..."), [this]{
+			const QString dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"),
+																  QDir::homePath(),
+																  QFileDialog::ShowDirsOnly
+																  | QFileDialog::DontResolveSymlinks);
+			if (dir.isEmpty())
+				return;
+			session.config.library.paths.insert(Strings::ToUtf8String(dir));
+			UpdateFolderMenu();
+		});
+	}
+}
+
 void MainWindow::SetActivePage(QWidget* page) {
 	this->setCentralWidget(page);
 }
--- a/src/main.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/main.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -1,7 +1,9 @@
 #include "core/session.h"
 #include "core/anime_db.h"
 #include "core/strings.h"
+#include "services/anilist.h"
 #include "gui/window.h"
+
 #include <QApplication>
 #include <QStyleFactory>
 #include <QTranslator>
@@ -13,10 +15,11 @@
 
 int main(int argc, char** argv) {
 	QApplication app(argc, argv);
+	app.setApplicationName("minori");
+	app.setApplicationDisplayName("Minori");
 	app.setAttribute(Qt::AA_DontShowIconsInMenus, true);
 
 	session.config.Load();
-	session.config.locale.RefreshAvailableLocales();
 	Anime::db.LoadDatabaseFromDisk();
 
 	MainWindow window;
--- a/src/services/anilist.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/services/anilist.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -244,6 +244,59 @@
 	return 1;
 }
 
+/* return is a vector of anime ids */
+std::vector<int> Search(const std::string& search) {
+	constexpr std::string_view query =
+		"query ($search: String) {\n"
+		"  Page (page: 1, perPage: 50) {\n"
+		"    media (search: $search, type: ANIME) {\n"
+		"      coverImage {\n"
+		"        large\n"
+		"      }\n"
+		"      id\n"
+		"      title {\n"
+		"        romaji\n"
+		"        english\n"
+		"        native\n"
+		"      }\n"
+		"      format\n"
+		"      status\n"
+		"      averageScore\n"
+		"      season\n"
+		"      startDate {\n"
+		"        year\n"
+		"        month\n"
+		"        day\n"
+		"      }\n"
+		"      genres\n"
+		"      episodes\n"
+		"      duration\n"
+		"      synonyms\n"
+		"      description(asHtml: false)\n"
+		"    }\n"
+		"  }\n"
+		"}\n";
+
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"search", search}
+		}}
+	};
+	// clang-format on
+
+	auto res = SendJSONRequest(json);
+
+	std::vector<int> ret;
+	ret.reserve(res["data"]["Page"]["media"].size());
+
+	for (const auto& media : res["data"]["Page"]["media"].items())
+		ret.push_back(ParseMediaJson(media.value()));
+
+	return ret;
+}
+
 int UpdateAnimeEntry(int id) {
 	/**
 	 * possible values:
@@ -263,18 +316,17 @@
 	 * float[] advancedScores,
 	 * Date startedAt,
 	 * Date completedAt
-	 **/
+	**/
 	Anime::Anime& anime = Anime::db.items[id];
 	if (!anime.IsInUserList())
 		return 0;
 
-	constexpr std::string_view query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, "
-							  "$notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
-							  "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, "
-							  "scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n"
-							  "    id\n"
-							  "  }\n"
-							  "}\n";
+	constexpr std::string_view query =
+		"mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
+		"  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n"
+		"    id\n"
+		"  }\n"
+		"}\n";
 	// clang-format off
 	nlohmann::json json = {
 		{"query", query},
--- a/src/services/services.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/services/services.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -2,7 +2,6 @@
 #include "core/session.h"
 #include "gui/dialog/settings.h"
 #include "services/anilist.h"
-#include <QMessageBox>
 
 namespace Services {
 
@@ -13,6 +12,13 @@
 	}
 }
 
+std::vector<int> Search(const std::string& search) {
+	switch (session.config.service) {
+		case Anime::Services::ANILIST: return AniList::Search(search);
+		default: return {};
+	}
+}
+
 void UpdateAnimeEntry(int id) {
 	switch (session.config.service) {
 		case Anime::Services::ANILIST: AniList::UpdateAnimeEntry(id); break;
--- a/src/sys/glib/dark_theme.cc	Wed Jan 24 20:18:59 2024 -0500
+++ b/src/sys/glib/dark_theme.cc	Sun Feb 04 21:17:17 2024 -0500
@@ -1,11 +1,12 @@
 #include <gio/gio.h>
 #include <cstring>
-
-#include <iostream>
+#include <string_view>
 
 namespace glib {
 
 bool IsInDarkTheme() {
+	bool success = false;
+
 	GSettings* settings = ::g_settings_new("org.gnome.desktop.interface");
 	if (!settings)
 		return false;
@@ -19,12 +20,31 @@
 	if (!str) /* how */
 		return false;
 
-	bool success = !std::strcmp(str, "prefer-dark");
+	success |= !std::strcmp(str, "prefer-dark");
+
+	::g_variant_unref(val);
+
+	if (success) {
+		::g_object_unref(settings);
+		return success;
+	}
+
+	GVariant* gtk_theme = ::g_settings_get_value(settings, "gtk-theme");
+	if (!gtk_theme)
+		return false;
 
-	/* unref these */
-	::g_variant_unref(val);
+	const gchar* gtk_theme_str;
+	::g_variant_get(gtk_theme, "&s", gtk_theme_str);
+	if (!gtk_theme_str)
+		return false;
+
+	static constexpr std::string_view suffix = "-dark";
+
+	size_t gtk_theme_len = strlen(gtk_theme_str);
+
+	success |= !std::strncmp(gtk_theme_str + gtk_theme_len - suffix.length(), suffix.data(), suffix.length());
+
 	::g_object_unref(settings);
-
 	return success;
 }
 
--- a/src/sys/osx/filesystem.cc	Wed Jan 24 20:18:59 2024 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-#include "sys/osx/filesystem.h"
-
-#include <CoreFoundation/CoreFoundation.h>
-#include <objc/runtime.h>
-
-#include <string>
-
-/* These constants are defined in Foundation but not
- * exposed to CoreFoundation users.
-*/
-static constexpr unsigned long NSApplicationSupportDirectory = 14;
-static constexpr unsigned long NSUserDomainMask = 1;
-
-extern "C" {
-	CFArrayRef NSSearchPathForDirectoriesInDomains(unsigned long directory, unsigned long domainMask, BOOL expandTilde);
-}
-
-namespace osx {
-
-bool GetApplicationSupportDirectory(std::string& result) {
-	// NSArray* strings = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, ON);
-	const CFArrayRef strings = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true);
-	if (!strings)
-		return false;
-
-	// NSIndex index = [strings count];
-	const CFIndex count = CFArrayGetCount(strings);
-	if (count < 1) {
-		CFRelease(strings);
-		return false;
-	}
-
-	// NSString* string = [strings objectAtIndex: 0];
-	const CFStringRef string = reinterpret_cast<CFStringRef>(CFArrayGetValueAtIndex(strings, 0));
-	if (!string) {
-		CFRelease(strings);
-		return false;
-	}
-
-	// result = [string UTF8String];
-	result.resize(CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1);
-	if (!CFStringGetCString(string, &result.front(), result.length(), kCFStringEncodingUTF8)) {
-		CFRelease(strings);
-		return false;
-	}
-	result.resize(result.find_first_of('\0'));
-
-	return true;
-}
-
-} // namespace osx