changeset 47:d8eb763e6661

information.cpp: add widgets to the list tab, and add an "optional date" widget like taiga has so users can specify whether to set the date or not
author Paper <mrpapersonic@gmail.com>
date Mon, 25 Sep 2023 00:43:38 -0400
parents d0adc4aedfc8
children e613772f41d5
files CMakeLists.txt include/core/date.h include/core/session.h include/gui/dialog/information.h include/gui/widgets/optional_date.h src/core/date.cpp src/gui/dialog/information.cpp src/gui/pages/anime_list.cpp src/gui/pages/statistics.cpp src/gui/translate/anilist.cpp src/gui/widgets/optional_date.cpp src/services/anilist.cpp src/services/services.cpp
diffstat 13 files changed, 304 insertions(+), 189 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sat Sep 23 01:02:15 2023 -0400
+++ b/CMakeLists.txt	Mon Sep 25 00:43:38 2023 -0400
@@ -1,6 +1,8 @@
 cmake_minimum_required(VERSION 3.16)
 project(minori LANGUAGES CXX)
 
+# this should check for the target system, rather than
+# the host system, for cross-compiling purposes
 if(APPLE)
 	enable_language(OBJCXX)
 endif()
@@ -51,6 +53,7 @@
 	# Custom widgets
 	src/gui/widgets/sidebar.cpp
 	src/gui/widgets/text.cpp
+	src/gui/widgets/optional_date.cpp
 
 	# Dialogs
 	src/gui/dialog/information.cpp
@@ -80,8 +83,9 @@
 	list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp)
 endif()
 
-add_executable(minori ${SRC_FILES})
-set_property(TARGET minori PROPERTY CXX_STANDARD 20)
+add_executable(minori WIN32 ${SRC_FILES})
+# There's a bug in JFMC++ that keeps me from setting this to C++11.
+set_property(TARGET minori PROPERTY CXX_STANDARD 17)
 set_property(TARGET minori PROPERTY AUTOMOC ON)
 set_property(TARGET minori PROPERTY AUTORCC ON)
 
--- a/include/core/date.h	Sat Sep 23 01:02:15 2023 -0400
+++ b/include/core/date.h	Mon Sep 25 00:43:38 2023 -0400
@@ -8,6 +8,7 @@
 		Date();
 		Date(int32_t y);
 		Date(int32_t y, int8_t m, int8_t d);
+		bool IsValid() const;
 		void SetYear(int32_t y);
 		void SetMonth(int8_t m);
 		void SetDay(int8_t d);
--- a/include/core/session.h	Sat Sep 23 01:02:15 2023 -0400
+++ b/include/core/session.h	Mon Sep 25 00:43:38 2023 -0400
@@ -6,12 +6,15 @@
 struct Session {
 		Config config;
 		Session() { timer.start(); }
+		void AppendRequest() { requests++; };
+		int GetRequests() { return requests; };
 		int uptime() { return timer.elapsed(); }
 
 	private:
+		int requests = 0;
 		QElapsedTimer timer;
 };
 
 extern Session session;
 
-#endif // __core__session_h
\ No newline at end of file
+#endif // __core__session_h
--- a/include/gui/dialog/information.h	Sat Sep 23 01:02:15 2023 -0400
+++ b/include/gui/dialog/information.h	Mon Sep 25 00:43:38 2023 -0400
@@ -11,5 +11,9 @@
 
 	public:
 		InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
+
+	private:
+		int id;
+		void SaveData();
 };
 #endif // __gui__dialog__information_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/gui/widgets/optional_date.h	Mon Sep 25 00:43:38 2023 -0400
@@ -0,0 +1,24 @@
+#ifndef __gui__widgets__optional_date_h
+#define __gui__widgets__optional_date_h
+#include <QWidget>
+
+class QCheckBox;
+class QDateEdit;
+class QDate;
+
+class OptionalDate : public QWidget {
+	public:
+		OptionalDate(QWidget* parent = nullptr);
+		OptionalDate(bool enabled, QWidget* parent = nullptr);
+		QDateEdit* GetDateEdit();
+		QCheckBox* GetCheckBox();
+		void SetDate(QDate date);
+		void SetEnabled(bool enabled);
+		bool IsEnabled();
+
+	private:
+		QDateEdit* _dateedit;
+		QCheckBox* _checkbox;
+};
+
+#endif // __gui__widgets__optional_date_h
--- a/src/core/date.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/core/date.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -85,9 +85,14 @@
 	return -1;
 }
 
+bool Date::IsValid() const {
+	return year.get() && month.get() && day.get();
+}
+
 bool Date::operator<(const Date& other) const {
+	int y = GetYear(), m = GetMonth(), d = GetDay();
 	int o_y = other.GetYear(), o_m = other.GetMonth(), o_d = other.GetDay();
-	return std::tie(*year, *month, *day) < std::tie(o_y, o_m, o_d);
+	return std::tie(y, m, d) < std::tie(o_y, o_m, o_d);
 }
 
 bool Date::operator>(const Date& other) const {
@@ -103,8 +108,8 @@
 }
 
 QDate Date::GetAsQDate() const {
-	/* QDates don't (yet) support "missing" values */
-	if (year.get() && month.get() && day.get())
+	/* QDates don't support "missing" values, for good reason. */
+	if (IsValid())
 		return QDate(*year, *month, *day);
 	else
 		return QDate();
--- a/src/gui/dialog/information.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/gui/dialog/information.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -1,13 +1,16 @@
 #include "gui/dialog/information.h"
 #include "core/anime.h"
+#include "core/anime_db.h"
 #include "core/array.h"
 #include "core/strings.h"
 #include "gui/pages/anime_list.h"
 #include "gui/translate/anime.h"
 #include "gui/widgets/text.h"
+#include "gui/widgets/optional_date.h"
 #include "gui/window.h"
 #include <QCheckBox>
 #include <QComboBox>
+#include <QDateEdit>
 #include <QDebug>
 #include <QDialogButtonBox>
 #include <QLineEdit>
@@ -20,6 +23,10 @@
 
 /* TODO: Taiga disables rendering of the tab widget entirely when the anime is not part of a list,
    which sucks. Think of a better way to implement this later. */
+void InformationDialog::SaveData() {
+	Anime::Anime& anime = Anime::db.items[id];
+}
+
 InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
     : QDialog(parent) {
 	setFixedSize(842, 613);
@@ -116,23 +123,58 @@
 	sg_anime_list_content->layout()->setSpacing(5);
 	sg_anime_list_content->layout()->setContentsMargins(12, 0, 0, 0);
 
-	/* Note: PLEASE find a way we can consolidate these. By ANY other means than
-	   putting them in a separate function. Macros are very much preferred. */
-#define LAYOUT_HORIZ_SPACING 9
-#define LAYOUT_VERT_SPACING 5
-	{
+/* these macros make this a lot easier to edit */
+#define LAYOUT_HORIZ_SPACING 25
+#define LAYOUT_VERT_SPACING  5
+#define LAYOUT_ITEM_WIDTH    175
+/* Creates a subsection with a width of 175 */
+#define CREATE_SUBSECTION(x) \
+	{ \
+		QWidget* subsection = new QWidget(section); \
+		subsection->setLayout(new QVBoxLayout); \
+		subsection->setFixedWidth(LAYOUT_ITEM_WIDTH); \
+		subsection->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); \
+		subsection->layout()->setSpacing(LAYOUT_VERT_SPACING); \
+		subsection->layout()->setMargin(0); \
+		x; \
+		layout->addWidget(subsection); \
+	}
+/* Creates a section in the parent `a` */
+#define CREATE_SECTION(a, x) \
+	{ \
+		QWidget* section = new QWidget(a); \
+		QHBoxLayout* layout = new QHBoxLayout(section); \
+		layout->setSpacing(LAYOUT_HORIZ_SPACING); \
+		layout->setMargin(0); \
+		x; \
+		layout->addStretch(); \
+		a->layout()->addWidget(section); \
+	}
+/* Creates a subsection that takes up whatever space is necessary */
+#define CREATE_FULL_WIDTH_SUBSECTION(x) \
+	{ \
+		QWidget* subsection = new QWidget(section); \
+		subsection->setLayout(new QVBoxLayout); \
+		subsection->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); \
+		subsection->layout()->setSpacing(LAYOUT_VERT_SPACING); \
+		subsection->layout()->setMargin(0); \
+		x; \
+		layout->addWidget(subsection); \
+	}
+/* Creates a section in the parent `a` */
+#define CREATE_FULL_WIDTH_SECTION(a, x) \
+	{ \
+		QWidget* section = new QWidget(a); \
+		QHBoxLayout* layout = new QHBoxLayout(section); \
+		layout->setSpacing(LAYOUT_HORIZ_SPACING); \
+		layout->setMargin(0); \
+		x; \
+		a->layout()->addWidget(section); \
+	}
+
+	CREATE_SECTION(sg_anime_list_content, {
 		/* Episodes watched section */
-		QWidget* section = new QWidget(sg_anime_list_content);
-		QHBoxLayout* layout = new QHBoxLayout;
-		layout->setSpacing(LAYOUT_HORIZ_SPACING);
-		layout->setMargin(0);
-		section->setLayout(layout);
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Episodes watched:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
@@ -140,37 +182,17 @@
 			spin_box->setSingleStep(1);
 			spin_box->setValue(anime.GetUserProgress());
 			subsection->layout()->addWidget(spin_box);
-
-			layout->addWidget(subsection);
-		}
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		});
+		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr(" "), subsection));
 
 			QCheckBox* rewatched_checkbox = new QCheckBox("Rewatching");
 			subsection->layout()->addWidget(rewatched_checkbox);
-
-			layout->addWidget(subsection);
-		}
-		sg_anime_list_content->layout()->addWidget(section);
-	}
-	{
+		});
+	});
+	CREATE_SECTION(sg_anime_list_content, {
 		/* Status & score section */
-		QWidget* section = new QWidget(sg_anime_list_content);
-		QHBoxLayout* layout = new QHBoxLayout;
-		layout->setSpacing(LAYOUT_HORIZ_SPACING);
-		layout->setMargin(0);
-		section->setLayout(layout);
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Status:"), subsection));
 
 			QStringList string_list;
@@ -180,15 +202,8 @@
 			QComboBox* combo_box = new QComboBox(subsection);
 			combo_box->addItems(string_list);
 			subsection->layout()->addWidget(combo_box);
-
-			layout->addWidget(subsection);
-		}
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		});
+		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Score:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
@@ -196,34 +211,35 @@
 			spin_box->setSingleStep(5);
 			spin_box->setValue(anime.GetUserScore());
 			subsection->layout()->addWidget(spin_box);
-
-			layout->addWidget(subsection);
-		}
-		sg_anime_list_content->layout()->addWidget(section);
-	}
-	{
+		});
+	});
+	CREATE_FULL_WIDTH_SECTION(sg_anime_list_content, {
 		/* Notes section */
-		QWidget* section = new QWidget(sg_anime_list_content);
-		QHBoxLayout* layout = new QHBoxLayout;
-		layout->setSpacing(LAYOUT_HORIZ_SPACING);
-		layout->setMargin(0);
-		section->setLayout(layout);
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		CREATE_FULL_WIDTH_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Notes:"), subsection));
 
 			QLineEdit* line_edit = new QLineEdit(QString::fromStdString(anime.GetUserNotes()), subsection);
 			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
 			subsection->layout()->addWidget(line_edit);
+		});
+	});
+	CREATE_SECTION(sg_anime_list_content, {
+		/* Dates section */
+		CREATE_SUBSECTION({
+			subsection->layout()->addWidget(new QLabel(tr("Date started:"), subsection));
 
-			layout->addWidget(subsection);
-		}
-		sg_anime_list_content->layout()->addWidget(section);
-	}
+			OptionalDate* date = new OptionalDate(true, subsection);
+			date->SetDate(QDate(2000, 1, 1));
+			subsection->layout()->addWidget(date);
+		});
+		CREATE_SUBSECTION({
+			subsection->layout()->addWidget(new QLabel(tr("Date completed:"), subsection));
+
+			OptionalDate* date = new OptionalDate(true, subsection);
+			date->SetDate(QDate(2000, 1, 1));
+			subsection->layout()->addWidget(date);
+		});
+	});
 
 	settings_widget->layout()->addWidget(new TextWidgets::Header(tr("Local settings"), settings_widget));
 
@@ -233,32 +249,24 @@
 	sg_local_content->layout()->setSpacing(5);
 	sg_local_content->layout()->setContentsMargins(12, 0, 0, 0);
 
-	{
+	CREATE_FULL_WIDTH_SECTION(sg_local_content, {
 		/* Alternative titles */
-		QWidget* section = new QWidget(sg_local_content);
-		QHBoxLayout* layout = new QHBoxLayout;
-		layout->setSpacing(LAYOUT_HORIZ_SPACING);
-		layout->setMargin(0);
-		section->setLayout(layout);
-		{
-			QWidget* subsection = new QWidget(section);
-			subsection->setLayout(new QVBoxLayout);
-			subsection->layout()->setSpacing(LAYOUT_VERT_SPACING);
-			subsection->layout()->setMargin(0);
-
+		CREATE_FULL_WIDTH_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Alternative titles:"), subsection));
 
 			QLineEdit* line_edit = new QLineEdit(QString::fromStdString(anime.GetUserNotes()), subsection);
-			line_edit->setPlaceholderText(tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
+			line_edit->setPlaceholderText(
+			    tr("Enter alternative titles here, separated by a semicolon (i.e. Title 1; Title 2)"));
 			subsection->layout()->addWidget(line_edit);
 
 			QCheckBox* checkbox = new QCheckBox("Use the first alternative title to search for torrents");
 			subsection->layout()->addWidget(checkbox);
-
-			layout->addWidget(subsection);
-		}
-		sg_local_content->layout()->addWidget(section);
-	}
+		});
+	});
+#undef CREATE_SECTION
+#undef CREATE_SUBSECTION
+#undef CREATE_FULL_WIDTH_SECTION
+#undef CREATE_FULL_WIDTH_SUBSECTION
 
 	static_cast<QBoxLayout*>(settings_widget->layout())->addStretch();
 
--- a/src/gui/pages/anime_list.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/gui/pages/anime_list.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -11,9 +11,9 @@
 #include "gui/pages/anime_list.h"
 #include "core/anime.h"
 #include "core/anime_db.h"
+#include "core/array.h"
 #include "core/session.h"
 #include "core/time.h"
-#include "core/array.h"
 #include "gui/dialog/information.h"
 #include "gui/translate/anime.h"
 #include "services/anilist.h"
@@ -99,33 +99,33 @@
 QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
 	if (role == Qt::DisplayRole) {
 		switch (section) {
-			   case AL_TITLE: return tr("Anime title");
-			   case AL_PROGRESS: return tr("Progress");
-			   case AL_EPISODES: return tr("Episodes");
-			   case AL_TYPE: return tr("Type");
-			   case AL_SCORE: return tr("Score");
-			   case AL_SEASON: return tr("Season");
-			   case AL_STARTED: return tr("Date started");
-			   case AL_COMPLETED: return tr("Date completed");
-			   case AL_NOTES: return tr("Notes");
-			   case AL_AVG_SCORE: return tr("Average score");
-			   case AL_UPDATED: return tr("Last updated");
-			   default: return {};
+			case AL_TITLE: return tr("Anime title");
+			case AL_PROGRESS: return tr("Progress");
+			case AL_EPISODES: return tr("Episodes");
+			case AL_TYPE: return tr("Type");
+			case AL_SCORE: return tr("Score");
+			case AL_SEASON: return tr("Season");
+			case AL_STARTED: return tr("Date started");
+			case AL_COMPLETED: return tr("Date completed");
+			case AL_NOTES: return tr("Notes");
+			case AL_AVG_SCORE: return tr("Average score");
+			case AL_UPDATED: return tr("Last updated");
+			default: return {};
 		}
 	} else if (role == Qt::TextAlignmentRole) {
 		switch (section) {
-			   case AL_TITLE:
-			   case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
-			   case AL_PROGRESS:
-			   case AL_EPISODES:
-			   case AL_TYPE:
-			   case AL_SCORE:
-			   case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
-			   case AL_SEASON:
-			   case AL_STARTED:
-			   case AL_COMPLETED:
-			   case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
-			   default: return QAbstractListModel::headerData(section, orientation, role);
+			case AL_TITLE:
+			case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+			case AL_PROGRESS:
+			case AL_EPISODES:
+			case AL_TYPE:
+			case AL_SCORE:
+			case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+			case AL_SEASON:
+			case AL_STARTED:
+			case AL_COMPLETED:
+			case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+			default: return QAbstractListModel::headerData(section, orientation, role);
 		}
 	}
 	return QAbstractListModel::headerData(section, orientation, role);
@@ -136,56 +136,56 @@
 		return QVariant();
 	switch (role) {
 		case Qt::DisplayRole:
-			   switch (index.column()) {
-				   case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
-				   case AL_PROGRESS:
-					   return QString::number(list[index.row()].GetUserProgress()) + "/" +
-					          QString::number(list[index.row()].GetEpisodes());
-				   case AL_EPISODES: return list[index.row()].GetEpisodes();
-				   case AL_SCORE: return list[index.row()].GetUserScore();
-				   case AL_TYPE: return QString::fromStdString(Translate::ToString(list[index.row()].GetFormat()));
-				   case AL_SEASON:
-					   return QString::fromStdString(Translate::ToString(list[index.row()].GetSeason())) + " " +
-					          QString::number(list[index.row()].GetAirDate().GetYear());
-				   case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
-				   case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
-				   case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate();
-				   case AL_UPDATED: {
-					   if (list[index.row()].GetUserTimeUpdated() == 0)
-						   return QString("-");
-					   Time::Duration duration(Time::GetSystemTime() - list[index.row()].GetUserTimeUpdated());
-					   return QString::fromUtf8(duration.AsRelativeString().c_str());
-				   }
-				   case AL_NOTES: return QString::fromUtf8(list[index.row()].GetUserNotes().c_str());
-				   default: return "";
-			   }
-			   break;
+			switch (index.column()) {
+				case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
+				case AL_PROGRESS:
+					return QString::number(list[index.row()].GetUserProgress()) + "/" +
+					       QString::number(list[index.row()].GetEpisodes());
+				case AL_EPISODES: return list[index.row()].GetEpisodes();
+				case AL_SCORE: return list[index.row()].GetUserScore();
+				case AL_TYPE: return QString::fromStdString(Translate::ToString(list[index.row()].GetFormat()));
+				case AL_SEASON:
+					return QString::fromStdString(Translate::ToString(list[index.row()].GetSeason())) + " " +
+					       QString::number(list[index.row()].GetAirDate().GetYear());
+				case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
+				case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
+				case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate();
+				case AL_UPDATED: {
+					if (list[index.row()].GetUserTimeUpdated() == 0)
+						return QString("-");
+					Time::Duration duration(Time::GetSystemTime() - list[index.row()].GetUserTimeUpdated());
+					return QString::fromUtf8(duration.AsRelativeString().c_str());
+				}
+				case AL_NOTES: return QString::fromUtf8(list[index.row()].GetUserNotes().c_str());
+				default: return "";
+			}
+			break;
 		case Qt::UserRole:
-			   switch (index.column()) {
-				   case AL_PROGRESS: return list[index.row()].GetUserProgress();
-				   case AL_TYPE: return static_cast<int>(list[index.row()].GetFormat());
-				   case AL_SEASON: return list[index.row()].GetAirDate().GetAsQDate();
-				   case AL_AVG_SCORE: return list[index.row()].GetAudienceScore();
-				   case AL_UPDATED: return QVariant::fromValue(list[index.row()].GetUserTimeUpdated());
-				   default: return data(index, Qt::DisplayRole);
-			   }
-			   break;
+			switch (index.column()) {
+				case AL_PROGRESS: return list[index.row()].GetUserProgress();
+				case AL_TYPE: return static_cast<int>(list[index.row()].GetFormat());
+				case AL_SEASON: return list[index.row()].GetAirDate().GetAsQDate();
+				case AL_AVG_SCORE: return list[index.row()].GetAudienceScore();
+				case AL_UPDATED: return QVariant::fromValue(list[index.row()].GetUserTimeUpdated());
+				default: return data(index, Qt::DisplayRole);
+			}
+			break;
 		case Qt::TextAlignmentRole:
-			   switch (index.column()) {
-				   case AL_TITLE:
-				   case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
-				   case AL_PROGRESS:
-				   case AL_EPISODES:
-				   case AL_TYPE:
-				   case AL_SCORE:
-				   case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
-				   case AL_SEASON:
-				   case AL_STARTED:
-				   case AL_COMPLETED:
-				   case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
-				   default: break;
-			   }
-			   break;
+			switch (index.column()) {
+				case AL_TITLE:
+				case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+				case AL_PROGRESS:
+				case AL_EPISODES:
+				case AL_TYPE:
+				case AL_SCORE:
+				case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+				case AL_SEASON:
+				case AL_STARTED:
+				case AL_COMPLETED:
+				case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+				default: break;
+			}
+			break;
 	}
 	return QVariant();
 }
@@ -195,7 +195,7 @@
 	int i = 0;
 	for (const auto& a : Anime::db.items) {
 		if (a.second.IsInUserList() && a.first == id && a.second.GetUserStatus() == status) {
-			   emit dataChanged(index(i), index(i));
+			emit dataChanged(index(i), index(i));
 		}
 		i++;
 	}
@@ -212,8 +212,8 @@
 	else {
 		int count = 0;
 		for (const auto& a : Anime::db.items)
-			   if (a.second.IsInUserList() && a.second.GetUserStatus() == status)
-				   count++;
+			if (a.second.IsInUserList() && a.second.GetUserStatus() == status)
+				count++;
 		beginInsertRows(index(0), 0, count - 1);
 	}
 
@@ -221,7 +221,7 @@
 
 	for (const auto& a : Anime::db.items) {
 		if (a.second.IsInUserList() && a.second.GetUserStatus() == status) {
-			   list.push_back(a.second);
+			list.push_back(a.second);
 		}
 	}
 
@@ -236,7 +236,7 @@
 
 	for (int i = 0, end = tree_view->header()->count(); i < end; i++) {
 		if (!tree_view->isColumnHidden(i))
-			   count++;
+			count++;
 	}
 
 	return count;
@@ -265,7 +265,7 @@
 
 	for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
 		if (i == AnimeListWidgetModel::AL_TITLE)
-			   continue;
+			continue;
 		const auto column_name =
 		    sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
 		QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
--- a/src/gui/pages/statistics.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/gui/pages/statistics.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -29,7 +29,7 @@
 	anime_list_data = anime_list_paragraph->GetParagraph();
 
 	TextWidgets::LabelledTextParagraph* application_paragraph =
-	    new TextWidgets::LabelledTextParagraph("Minori", "Uptime:", "", this);
+	    new TextWidgets::LabelledTextParagraph("Minori", "Uptime:\nRequests made:", "", this);
 	application_data = application_paragraph->GetParagraph();
 
 	layout()->addWidget(anime_list_paragraph);
@@ -104,9 +104,12 @@
 	ts << Anime::db.GetScoreDeviation();
 	TextWidgets::SetPlainTextEditData(anime_list_data, string);
 
+	string = "";
+	ts << QString::fromUtf8(SecondsToDateString(session.uptime() / 1000).c_str()) << '\n';
+	ts << session.GetRequests();
 	/* Application */
 	// UiUtils::SetPlainTextEditData(application_data, QString::number(session.uptime() / 1000));
-	TextWidgets::SetPlainTextEditData(application_data, QString(SecondsToDateString(session.uptime() / 1000).c_str()));
+	TextWidgets::SetPlainTextEditData(application_data, string);
 }
 
 #include "gui/pages/moc_statistics.cpp"
--- a/src/gui/translate/anilist.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/gui/translate/anilist.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -3,7 +3,7 @@
 namespace Translate::AniList {
 
 Anime::SeriesStatus ToSeriesStatus(std::string status) {
-	std::unordered_map<std::string, Anime::SeriesStatus> map = {
+	const std::unordered_map<std::string, Anime::SeriesStatus> map = {
 	    {"FINISHED",         Anime::SeriesStatus::FINISHED        },
 	    {"RELEASING",        Anime::SeriesStatus::RELEASING       },
 	    {"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED},
@@ -11,26 +11,26 @@
 	    {"HIATUS",           Anime::SeriesStatus::HIATUS          }
     };
 
-	if (!map.contains(status))
+	if (map.find(status) == map.end())
 		return Anime::SeriesStatus::UNKNOWN;
-	return map[status];
+	return map.at(status);
 }
 
 Anime::SeriesSeason ToSeriesSeason(std::string season) {
-	std::unordered_map<std::string, Anime::SeriesSeason> map = {
+	const std::unordered_map<std::string, Anime::SeriesSeason> map = {
 	    {"WINTER", Anime::SeriesSeason::WINTER},
 	    {"SPRING", Anime::SeriesSeason::SPRING},
 	    {"SUMMER", Anime::SeriesSeason::SUMMER},
 	    {"FALL",   Anime::SeriesSeason::FALL  }
     };
 
-	if (!map.contains(season))
+	if (map.find(season) == map.end())
 		return Anime::SeriesSeason::UNKNOWN;
-	return map[season];
+	return map.at(season);
 }
 
 Anime::SeriesFormat ToSeriesFormat(std::string format) {
-	std::unordered_map<std::string, enum Anime::SeriesFormat> map = {
+	const std::unordered_map<std::string, enum Anime::SeriesFormat> map = {
 	    {"TV",       Anime::SeriesFormat::TV      },
         {"TV_SHORT", Anime::SeriesFormat::TV_SHORT},
 	    {"MOVIE",    Anime::SeriesFormat::MOVIE   },
@@ -43,9 +43,9 @@
         {"ONE_SHOT", Anime::SeriesFormat::ONE_SHOT}
     };
 
-	if (!map.contains(format))
+	if (map.find(format) == map.end())
 		return Anime::SeriesFormat::UNKNOWN;
-	return map[format];
+	return map.at(format);
 }
 
 } // namespace Translate::AniList
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/widgets/optional_date.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -0,0 +1,49 @@
+#include "gui/widgets/optional_date.h"
+#include <QCheckBox>
+#include <QDateEdit>
+#include <QHBoxLayout>
+
+OptionalDate::OptionalDate(QWidget* parent) {
+	OptionalDate(false, parent);
+}
+
+OptionalDate::OptionalDate(bool enabled, QWidget* parent) : QWidget(parent) {
+	QHBoxLayout* layout = new QHBoxLayout(this);
+	layout->setMargin(0);
+
+	_checkbox = new QCheckBox(this);
+	_checkbox->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
+	_checkbox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
+
+	layout->addWidget(_checkbox);
+
+	_dateedit = new QDateEdit(this);
+	_dateedit->setDisplayFormat("yyyy-MM-dd");
+	_dateedit->setCalendarPopup(true);
+	_dateedit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+
+	layout->addWidget(_dateedit);
+
+	SetEnabled(enabled);
+	connect(_checkbox, &QCheckBox::stateChanged, this, [this](int state) { SetEnabled(state == Qt::Checked); });
+}
+
+void OptionalDate::SetEnabled(bool enabled) {
+	_dateedit->setEnabled(enabled);
+}
+
+bool OptionalDate::IsEnabled() {
+	return _dateedit->isEnabled();
+}
+
+void OptionalDate::SetDate(QDate date) {
+	_dateedit->setDate(date);
+}
+
+QDateEdit* OptionalDate::GetDateEdit() {
+	return _dateedit;
+}
+
+QCheckBox* OptionalDate::GetCheckBox() {
+	return _checkbox;
+}
--- a/src/services/anilist.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/services/anilist.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -87,7 +87,7 @@
 		return;
 	}
 
-	if (!map.contains(status)) {
+	if (map.find(status) == map.end()) {
 		anime.SetUserStatus(Anime::ListStatus::NOT_IN_LIST);
 		return;
 	}
@@ -107,7 +107,7 @@
 	if (anime.GetUserIsRewatching())
 		return "REWATCHING";
 
-	if (!map.contains(anime.GetUserStatus()))
+	if (map.find(anime.GetUserStatus()) == map.end())
 		return "CURRENT";
 	return map[anime.GetUserStatus()];
 }
--- a/src/services/services.cpp	Sat Sep 23 01:02:15 2023 -0400
+++ b/src/services/services.cpp	Mon Sep 25 00:43:38 2023 -0400
@@ -1,13 +1,27 @@
 #include "services/services.h"
 #include "core/session.h"
 #include "services/anilist.h"
+#include "gui/dialog/settings.h"
+#include <QMessageBox>
 
 namespace Services {
 
 void Synchronize() {
 	switch (session.config.service) {
 		case Anime::Services::ANILIST: AniList::GetAnimeList(); break;
-		default: break;
+		default: {
+			QMessageBox msg;
+			msg.setInformativeText("It seems you haven't yet selected a service to use.");
+			msg.setText("Would you like to select one now?");
+			msg.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+			msg.setDefaultButton(QMessageBox::Yes);
+			int ret = msg.exec();
+			if (ret == QMessageBox::Yes) {
+				SettingsDialog dialog;
+				dialog.exec();
+			}
+			break;
+		}
 	}
 }