changeset 51:75c804f713b2

window: add about window, *: use tr() when applicable (useful for i18n)
author Paper <mrpapersonic@gmail.com>
date Mon, 25 Sep 2023 20:29:26 -0400
parents 10868c3fb2be
children 0c4138de2ea7
files CMakeLists.txt include/core/anime.h include/core/date.h include/core/session.h include/core/version.h include/gui/dialog/about.h include/gui/dialog/information.h include/gui/widgets/optional_date.h src/core/date.cpp src/gui/dialog/about.cpp src/gui/dialog/information.cpp src/gui/pages/anime_list.cpp src/gui/pages/statistics.cpp src/gui/translate/anime.cpp src/gui/widgets/optional_date.cpp src/gui/widgets/text.cpp src/gui/window.cpp
diffstat 17 files changed, 363 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Sep 25 13:50:56 2023 -0400
+++ b/CMakeLists.txt	Mon Sep 25 20:29:26 2023 -0400
@@ -56,6 +56,7 @@
 	src/gui/widgets/optional_date.cpp
 
 	# Dialogs
+	src/gui/dialog/about.cpp
 	src/gui/dialog/information.cpp
 	src/gui/dialog/settings.cpp
 	src/gui/dialog/settings/application.cpp
@@ -83,7 +84,7 @@
 	list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp)
 endif()
 
-add_executable(minori WIN32 ${SRC_FILES})
+add_executable(minori ${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)
--- a/include/core/anime.h	Mon Sep 25 13:50:56 2023 -0400
+++ b/include/core/anime.h	Mon Sep 25 20:29:26 2023 -0400
@@ -10,10 +10,10 @@
 enum class ListStatus {
 	NOT_IN_LIST,
 	CURRENT,
-	PLANNING,
 	COMPLETED,
+	PAUSED,
 	DROPPED,
-	PAUSED
+	PLANNING
 };
 
 constexpr std::array<ListStatus, 5> ListStatuses{ListStatus::CURRENT, ListStatus::COMPLETED, ListStatus::PAUSED,
@@ -169,4 +169,4 @@
 
 } // namespace Anime
 
-#endif // __core__anime_h
\ No newline at end of file
+#endif // __core__anime_h
--- a/include/core/date.h	Mon Sep 25 13:50:56 2023 -0400
+++ b/include/core/date.h	Mon Sep 25 20:29:26 2023 -0400
@@ -6,18 +6,19 @@
 class Date {
 	public:
 		Date();
-		Date(int32_t y);
-		Date(int32_t y, int8_t m, int8_t d);
+		Date(unsigned int y);
+		Date(unsigned int y, unsigned int m, unsigned int d);
+		Date(const QDate& date);
 		bool IsValid() const;
-		void SetYear(int32_t y);
-		void SetMonth(int8_t m);
-		void SetDay(int8_t d);
+		void SetYear(unsigned int y);
+		void SetMonth(unsigned int m);
+		void SetDay(unsigned int d);
 		void VoidYear();
 		void VoidMonth();
 		void VoidDay();
-		int32_t GetYear() const;
-		int8_t GetMonth() const;
-		int8_t GetDay() const;
+		unsigned int GetYear() const;
+		unsigned int GetMonth() const;
+		unsigned int GetDay() const;
 		QDate GetAsQDate() const;
 		nlohmann::json GetAsAniListJson() const;
 		bool operator<(const Date& other) const;
@@ -26,8 +27,10 @@
 		bool operator>=(const Date& other) const;
 
 	private:
-		std::shared_ptr<int32_t> year;
-		std::shared_ptr<int8_t> month;
-		std::shared_ptr<int8_t> day;
+		/* note: it might be worth it to change these all to int, as
+		   large bit precisions aren't exactly useful here... */
+		std::shared_ptr<unsigned int> year;
+		std::shared_ptr<unsigned int> month;
+		std::shared_ptr<unsigned int> day;
 };
 #endif // __core__date_h
--- a/include/core/session.h	Mon Sep 25 13:50:56 2023 -0400
+++ b/include/core/session.h	Mon Sep 25 20:29:26 2023 -0400
@@ -4,8 +4,10 @@
 #include <QElapsedTimer>
 
 struct Session {
+	public:
 		Config config;
 		Session() { timer.start(); }
+		/* we literally *cannot* be lying to the user by doing this */
 		void IncrementRequests() { requests++; };
 		int GetRequests() { return requests; };
 		int uptime() { return timer.elapsed(); }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/core/version.h	Mon Sep 25 20:29:26 2023 -0400
@@ -0,0 +1,6 @@
+#ifndef __core__version_h
+#define __core__version_h
+
+#define MINORI_VERSION "v0.1alpha"
+
+#endif // __core__version_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/gui/dialog/about.h	Mon Sep 25 20:29:26 2023 -0400
@@ -0,0 +1,10 @@
+#ifndef __gui__dialog__about_h
+#define __gui__dialog__about_h
+#include <QDialog>
+
+class AboutWindow : public QDialog {
+	public:
+		AboutWindow(QWidget* parent = nullptr);
+};
+
+#endif // __gui__dialog__about_h
--- a/include/gui/dialog/information.h	Mon Sep 25 13:50:56 2023 -0400
+++ b/include/gui/dialog/information.h	Mon Sep 25 20:29:26 2023 -0400
@@ -2,9 +2,8 @@
 #define __gui__dialog__information_h
 #include <QDialog>
 #include <functional>
-namespace Anime {
-class Anime;
-}
+#include "core/date.h"
+#include "core/anime.h"
 
 class InformationDialog : public QDialog {
 		Q_OBJECT
@@ -13,7 +12,15 @@
 		InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
 
 	private:
-		int id;
+		unsigned int id;
+		unsigned int progress;
+		unsigned int episodes;
+		unsigned int score;
+		bool rewatching;
+		Anime::ListStatus status;
+		std::string notes;
+		Date started;
+		Date completed;
 		void SaveData();
 };
 #endif // __gui__dialog__information_h
--- a/include/gui/widgets/optional_date.h	Mon Sep 25 13:50:56 2023 -0400
+++ b/include/gui/widgets/optional_date.h	Mon Sep 25 20:29:26 2023 -0400
@@ -1,21 +1,29 @@
 #ifndef __gui__widgets__optional_date_h
 #define __gui__widgets__optional_date_h
 #include <QWidget>
+#include "core/date.h"
 
 class QCheckBox;
 class QDateEdit;
 class QDate;
 
 class OptionalDate : public QWidget {
+	Q_OBJECT
+
 	public:
 		OptionalDate(QWidget* parent = nullptr);
 		OptionalDate(bool enabled, QWidget* parent = nullptr);
 		QDateEdit* GetDateEdit();
 		QCheckBox* GetCheckBox();
 		void SetDate(QDate date);
+		void SetDate(Date date);
+		Date GetDate();
 		void SetEnabled(bool enabled);
 		bool IsEnabled();
 
+	signals:
+		void DataChanged(bool checked, Date date);
+
 	private:
 		QDateEdit* _dateedit;
 		QCheckBox* _checkbox;
--- a/src/core/date.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/core/date.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -1,6 +1,7 @@
 #include "core/date.h"
 #include "core/json.h"
 #include <QDate>
+#include <QDebug>
 #include <cstdint>
 #include <tuple>
 
@@ -30,16 +31,22 @@
 Date::Date() {
 }
 
-Date::Date(int32_t y) {
+Date::Date(unsigned int y) {
 	SetYear(y);
 }
 
-Date::Date(int32_t y, int8_t m, int8_t d) {
+Date::Date(unsigned int y, unsigned int m, unsigned int d) {
 	SetYear(y);
 	SetMonth(m);
 	SetDay(d);
 }
 
+Date::Date(const QDate& date) {
+	SetYear(date.year());
+	SetMonth(date.month());
+	SetDay(date.day());
+}
+
 void Date::VoidYear() {
 	year.reset();
 }
@@ -52,34 +59,34 @@
 	day.reset();
 }
 
-void Date::SetYear(int32_t y) {
-	year.reset(new int32_t(MAX(0, y)));
+void Date::SetYear(unsigned int y) {
+	year.reset(new unsigned int(MAX(0U, y)));
 }
 
-void Date::SetMonth(int8_t m) {
-	month.reset(new int8_t(CLAMP(m, 1, 12)));
+void Date::SetMonth(unsigned int m) {
+	month.reset(new unsigned int(CLAMP(m, 1U, 12U)));
 }
 
-void Date::SetDay(int8_t d) {
-	day.reset(new int8_t(CLAMP(d, 1, 31)));
+void Date::SetDay(unsigned int d) {
+	day.reset(new unsigned int(CLAMP(d, 1U, 31U)));
 }
 
-int32_t Date::GetYear() const {
-	int32_t* ptr = year.get();
+unsigned int Date::GetYear() const {
+	unsigned int* ptr = year.get();
 	if (ptr != nullptr)
 		return *year;
 	return -1;
 }
 
-int8_t Date::GetMonth() const {
-	int8_t* ptr = month.get();
+unsigned int Date::GetMonth() const {
+	unsigned int* ptr = month.get();
 	if (ptr != nullptr)
 		return *month;
 	return -1;
 }
 
-int8_t Date::GetDay() const {
-	int8_t* ptr = day.get();
+unsigned int Date::GetDay() const {
+	unsigned int* ptr = day.get();
 	if (ptr != nullptr)
 		return *day;
 	return -1;
@@ -90,8 +97,8 @@
 }
 
 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();
+	unsigned int y = GetYear(), m = GetMonth(), d = GetDay();
+	unsigned int o_y = other.GetYear(), o_m = other.GetMonth(), o_d = other.GetDay();
 	return std::tie(y, m, d) < std::tie(o_y, o_m, o_d);
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui/dialog/about.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -0,0 +1,118 @@
+#include "core/version.h"
+#include "gui/widgets/text.h"
+#include "gui/dialog/about.h"
+#include "core/json.h"
+#include <curl/curl.h>
+#include <QFont>
+#include <QTextCharFormat>
+#include <QTextBrowser>
+#include <QTextCursor>
+#include <QHBoxLayout>
+
+#define CONCAT_VERSION_NX(major, minor, patch) \
+	("v" #major "." #minor "." #patch)
+
+#define CONCAT_VERSION(major, minor, patch) \
+	CONCAT_VERSION_NX(major, minor, patch)
+
+#define SET_TITLE_FONT(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setPointSize(10); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_PARAGRAPH_FONT(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setPointSize(8); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_FONT_BOLD(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setBold(true); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define UNSET_FONT_BOLD(font, format, cursor) \
+	{ \
+		font = cursor.charFormat().font(); \
+		font.setBold(false); \
+		format.setFont(font); \
+		cursor.setCharFormat(format); \
+	}
+
+#define SET_FORMAT_HYPERLINK(format, cursor, link) \
+	{ \
+		format.setAnchor(true); \
+		format.setAnchorHref(link); \
+		cursor.setCharFormat(format); \
+	}
+#define UNSET_FORMAT_HYPERLINK(format, cursor) \
+	{ \
+		format.setAnchor(false); \
+		format.setAnchorHref(""); \
+		cursor.setCharFormat(format); \
+	}
+
+AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) {
+	setWindowTitle(tr("About Minori"));
+	setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
+	setLayout(new QHBoxLayout);
+
+	QPalette pal = QPalette();
+	pal.setColor(QPalette::Window, pal.color(QPalette::Base));
+	setPalette(pal);
+	setAutoFillBackground(true);
+
+	QFont font;
+	QTextCharFormat format;
+	QTextBrowser* paragraph = new QTextBrowser(this);
+	paragraph->setOpenExternalLinks(true);
+	QTextCursor cursor = paragraph->textCursor();
+	SET_TITLE_FONT(font, format, cursor);
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText("Minori");
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(" " MINORI_VERSION);
+	SET_PARAGRAPH_FONT(font, format, cursor);
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Author:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	cursor.insertText(tr("Paper"));
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Third party components:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	SET_FORMAT_HYPERLINK(format, cursor, "https://github.com/nlohmann/json");
+	cursor.insertText(tr("JSON for Modern C++ ") + CONCAT_VERSION(NLOHMANN_JSON_VERSION_MAJOR, NLOHMANN_JSON_VERSION_MINOR, NLOHMANN_JSON_VERSION_PATCH));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertText(", ");
+	{
+		curl_version_info_data* data = curl_version_info(CURLVERSION_NOW);
+		SET_FORMAT_HYPERLINK(format, cursor, "https://curl.se/");
+		cursor.insertText(tr("libcurl v") + data->version);
+		UNSET_FORMAT_HYPERLINK(format, cursor);
+		cursor.insertText(", ");
+	}
+	SET_FORMAT_HYPERLINK(format, cursor, "https://p.yusukekamiyamane.com/");
+	cursor.insertText(tr("Fugue Icons ") + CONCAT_VERSION(3, 5, 6));
+	UNSET_FORMAT_HYPERLINK(format, cursor);
+	cursor.insertBlock();
+	cursor.insertBlock();
+	SET_FONT_BOLD(font, format, cursor);
+	cursor.insertText(tr("Links:"));
+	UNSET_FONT_BOLD(font, format, cursor);
+	cursor.insertBlock();
+	layout()->addWidget(paragraph);
+}
--- a/src/gui/dialog/information.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/dialog/information.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -25,6 +25,13 @@
    which sucks. Think of a better way to implement this later. */
 void InformationDialog::SaveData() {
 	Anime::Anime& anime = Anime::db.items[id];
+	anime.SetUserProgress(progress);
+	anime.SetUserScore(score);
+	anime.SetUserIsRewatching(rewatching);
+	anime.SetUserStatus(status);
+	anime.SetUserNotes(notes);
+	anime.SetUserDateStarted(started);
+	anime.SetUserDateCompleted(completed);
 }
 
 InformationDialog::InformationDialog(const Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
@@ -56,6 +63,7 @@
 
 	main_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 
+	id = anime.GetId();
 	/* anime title header text */
 	TextWidgets::Paragraph* anime_title =
 	    new TextWidgets::Paragraph(QString::fromUtf8(anime.GetUserPreferredTitle().c_str()), main_widget);
@@ -89,7 +97,7 @@
 
 	/* alt titles */
 	main_information_widget->layout()->addWidget(new TextWidgets::SelectableTextParagraph(
-	    "Alternative titles", QString::fromUtf8(Strings::Implode(anime.GetTitleSynonyms(), ", ").c_str()),
+	    tr("Alternative titles"), QString::fromUtf8(Strings::Implode(anime.GetTitleSynonyms(), ", ").c_str()),
 	    main_information_widget));
 
 	/* details */
@@ -102,11 +110,11 @@
 	               << Strings::Implode(anime.GetGenres(), ", ").c_str() << "\n"
 	               << anime.GetAudienceScore() << "%";
 	main_information_widget->layout()->addWidget(new TextWidgets::LabelledTextParagraph(
-	    "Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data, main_information_widget));
+	    tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), details_data, main_information_widget));
 
 	/* synopsis */
 	TextWidgets::SelectableTextParagraph* synopsis = new TextWidgets::SelectableTextParagraph(
-	    "Synopsis", QString::fromUtf8(anime.GetSynopsis().c_str()), main_information_widget);
+	    tr("Synopsis"), QString::fromUtf8(anime.GetSynopsis().c_str()), main_information_widget);
 
 	synopsis->GetParagraph()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
 	main_information_widget->layout()->addWidget(synopsis);
@@ -178,16 +186,23 @@
 			subsection->layout()->addWidget(new QLabel(tr("Episodes watched:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) {
+				progress = i;
+			});
 			spin_box->setRange(0, anime.GetEpisodes());
 			spin_box->setSingleStep(1);
-			spin_box->setValue(anime.GetUserProgress());
+			spin_box->setValue(progress = anime.GetUserProgress());
 			subsection->layout()->addWidget(spin_box);
 		});
 		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr(" "), subsection));
 
-			QCheckBox* rewatched_checkbox = new QCheckBox("Rewatching");
-			subsection->layout()->addWidget(rewatched_checkbox);
+			QCheckBox* checkbox = new QCheckBox(tr("Rewatching"));
+			connect(checkbox, QOverload<int>::of(&QCheckBox::stateChanged), this, [this](int state) {
+				rewatching = (state == Qt::Checked);
+			});
+			checkbox->setCheckState(anime.GetUserIsRewatching() ? Qt::Checked : Qt::Unchecked);
+			subsection->layout()->addWidget(checkbox);
 		});
 	});
 	CREATE_SECTION(sg_anime_list_content, {
@@ -201,15 +216,22 @@
 
 			QComboBox* combo_box = new QComboBox(subsection);
 			combo_box->addItems(string_list);
+			connect(combo_box, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int i) {
+				status = Anime::ListStatuses[i];
+			});
+			combo_box->setCurrentIndex(static_cast<int>(status = anime.GetUserStatus())-1);
 			subsection->layout()->addWidget(combo_box);
 		});
 		CREATE_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Score:"), subsection));
 
 			QSpinBox* spin_box = new QSpinBox(subsection);
+			connect(spin_box, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int i) {
+				score = i;
+			});
 			spin_box->setRange(0, 100);
 			spin_box->setSingleStep(5);
-			spin_box->setValue(anime.GetUserScore());
+			spin_box->setValue(score = anime.GetUserScore());
 			subsection->layout()->addWidget(spin_box);
 		});
 	});
@@ -218,7 +240,12 @@
 		CREATE_FULL_WIDTH_SUBSECTION({
 			subsection->layout()->addWidget(new QLabel(tr("Notes:"), subsection));
 
-			QLineEdit* line_edit = new QLineEdit(QString::fromStdString(anime.GetUserNotes()), subsection);
+			QLineEdit* line_edit = new QLineEdit(subsection);
+			connect(line_edit, &QLineEdit::textChanged, this, [this](const QString& text) {
+				/* this sucks but I don't really want to implement anything smarter :) */
+				notes = text.toStdString();
+			});
+			line_edit->setText(QString::fromStdString(notes = anime.GetUserNotes()));
 			line_edit->setPlaceholderText(tr("Enter your notes about this anime"));
 			subsection->layout()->addWidget(line_edit);
 		});
@@ -229,14 +256,30 @@
 			subsection->layout()->addWidget(new QLabel(tr("Date started:"), subsection));
 
 			OptionalDate* date = new OptionalDate(true, subsection);
-			date->SetDate(QDate(2000, 1, 1));
+			connect(date, &OptionalDate::DataChanged, this, [this](bool enabled, Date date) {
+				started = (enabled) ? date : Date();
+			});
+			started = anime.GetUserDateStarted();
+			if (!started.IsValid()) {
+				date->SetEnabled(false);
+				started = anime.GetAirDate();
+			}
+			date->SetDate(started);
 			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));
+			connect(date, &OptionalDate::DataChanged, this, [this](bool enabled, Date date) {
+				completed = (enabled) ? date : Date();
+			});
+			completed = anime.GetUserDateCompleted();
+			if (!completed.IsValid()) {
+				date->SetEnabled(false);
+				completed = anime.GetAirDate();
+			}
+			date->SetDate(completed);
 			subsection->layout()->addWidget(date);
 		});
 	});
@@ -259,7 +302,7 @@
 			    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");
+			QCheckBox* checkbox = new QCheckBox(tr("Use the first alternative title to search for torrents"));
 			subsection->layout()->addWidget(checkbox);
 		});
 	});
@@ -270,8 +313,8 @@
 
 	static_cast<QBoxLayout*>(settings_widget->layout())->addStretch();
 
-	tabbed_widget->addTab(main_information_widget, "Main information");
-	tabbed_widget->addTab(settings_widget, "My list and settings");
+	tabbed_widget->addTab(main_information_widget, tr("Main information"));
+	tabbed_widget->addTab(settings_widget, tr("My list and settings"));
 
 	QVBoxLayout* main_layout = new QVBoxLayout;
 	main_layout->addWidget(anime_title);
@@ -286,6 +329,7 @@
 
 	QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
 	connect(button_box, &QDialogButtonBox::accepted, this, [this, accept] {
+		SaveData();
 		accept();
 		QDialog::accept();
 	});
--- a/src/gui/pages/anime_list.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/pages/anime_list.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -47,8 +47,8 @@
 			int text_width = 59;
 			QRectF text_rect(option.rect.x() + text_width, option.rect.y(), text_width, option.decorationSize.height());
 			painter->save();
-			painter->drawText(text_rect, "/", QTextOption(Qt::AlignCenter | Qt::AlignVCenter));
-			// drawText(const QRectF &rectangle, const QString &text, const QTextOption &option =
+			painter->drawText(text_rect, tr("/"), QTextOption(Qt::AlignCenter | Qt::AlignVCenter));
+			drawText(const QRectF &rectangle, const QString &text, const QTextOption &option =
 			   QTextOption()) painter->drawText(QRectF(text_rect.x(), text_rect.y(), text_width / 2 - 2,
 			   text_rect.height()), QString::number(progress), QTextOption(Qt::AlignRight | Qt::AlignVCenter));
 			   painter->drawText(
@@ -306,7 +306,7 @@
 		return;
 	}
 
-	QAction* action = menu->addAction("Information", [this, selection] {
+	QAction* action = menu->addAction(tr("Information"), [this, selection] {
 		const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
 		                              ->index(selection.indexes().first().row());
 		Anime::Anime* anime =
@@ -315,13 +315,9 @@
 			return;
 		}
 
-		InformationDialog* dialog = new InformationDialog(
-		    *anime,
-		    [this, anime] {
-			    ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())
-			        ->UpdateAnime(anime->GetId());
-		    },
-		    this);
+		InformationDialog* dialog = new InformationDialog(*anime, [this] {
+		    Refresh();
+	    }, this);
 
 		dialog->show();
 		dialog->raise();
@@ -343,12 +339,9 @@
 	Anime::Anime* anime =
 	    ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
 
-	InformationDialog* dialog = new InformationDialog(
-	    *anime,
-	    [this, anime] {
-		    ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(anime->GetId());
-	    },
-	    this);
+	InformationDialog* dialog = new InformationDialog(*anime, [this] {
+		Refresh();
+	}, this);
 
 	dialog->show();
 	dialog->raise();
--- a/src/gui/pages/statistics.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/pages/statistics.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -23,13 +23,13 @@
 	setAutoFillBackground(true);
 
 	TextWidgets::LabelledTextParagraph* anime_list_paragraph = new TextWidgets::LabelledTextParagraph(
-	    "Anime list",
-	    "Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", "\n\n\n\n\n\n",
+	    tr("Anime list"),
+	    tr("Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:"), "\n\n\n\n\n\n",
 	    this);
 	anime_list_data = anime_list_paragraph->GetParagraph();
 
 	TextWidgets::LabelledTextParagraph* application_paragraph =
-	    new TextWidgets::LabelledTextParagraph("Minori", "Uptime:\nRequests made:", "\n\n", this);
+	    new TextWidgets::LabelledTextParagraph(tr("Minori"), tr("Uptime:\nRequests made:"), "\n\n", this);
 	application_data = application_paragraph->GetParagraph();
 
 	layout()->addWidget(anime_list_paragraph);
--- a/src/gui/translate/anime.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/translate/anime.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -1,53 +1,54 @@
 #include "core/anime.h"
 #include "gui/translate/anime.h"
+#include <QCoreApplication>
 
 namespace Translate {
 
 std::string ToString(const Anime::ListStatus status) {
 	switch (status) {
-		case Anime::ListStatus::NOT_IN_LIST: return "Not in list";
-		case Anime::ListStatus::CURRENT: return "Currently watching";
-		case Anime::ListStatus::PLANNING: return "Plan to watch";
-		case Anime::ListStatus::COMPLETED: return "Completed";
-		case Anime::ListStatus::DROPPED: return "Dropped";
-		case Anime::ListStatus::PAUSED: return "On hold";
+		case Anime::ListStatus::NOT_IN_LIST: return QCoreApplication::tr("Not in list").toStdString();
+		case Anime::ListStatus::CURRENT: return QCoreApplication::tr("Currently watching").toStdString();
+		case Anime::ListStatus::PLANNING: return QCoreApplication::tr("Plan to watch").toStdString();
+		case Anime::ListStatus::COMPLETED: return QCoreApplication::tr("Completed").toStdString();
+		case Anime::ListStatus::DROPPED: return QCoreApplication::tr("Dropped").toStdString();
+		case Anime::ListStatus::PAUSED: return QCoreApplication::tr("On hold").toStdString();
 		default: return "";
 	}
 }
 
 std::string ToString(const Anime::SeriesFormat format) {
 	switch (format) {
-		case Anime::SeriesFormat::UNKNOWN: return "Unknown";
-		case Anime::SeriesFormat::TV: return "TV";
-		case Anime::SeriesFormat::TV_SHORT: return "TV short";
-		case Anime::SeriesFormat::OVA: return "OVA";
-		case Anime::SeriesFormat::MOVIE: return "Movie";
-		case Anime::SeriesFormat::SPECIAL: return "Special";
-		case Anime::SeriesFormat::ONA: return "ONA";
-		case Anime::SeriesFormat::MUSIC: return "Music";
+		case Anime::SeriesFormat::UNKNOWN: return QCoreApplication::tr("Unknown").toStdString();
+		case Anime::SeriesFormat::TV: return QCoreApplication::tr("TV").toStdString();
+		case Anime::SeriesFormat::TV_SHORT: return QCoreApplication::tr("TV short").toStdString();
+		case Anime::SeriesFormat::OVA: return QCoreApplication::tr("OVA").toStdString();
+		case Anime::SeriesFormat::MOVIE: return QCoreApplication::tr("Movie").toStdString();
+		case Anime::SeriesFormat::SPECIAL: return QCoreApplication::tr("Special").toStdString();
+		case Anime::SeriesFormat::ONA: return QCoreApplication::tr("ONA").toStdString();
+		case Anime::SeriesFormat::MUSIC: return QCoreApplication::tr("Music").toStdString();
 		default: return "";
 	}
 }
 
 std::string ToString(const Anime::SeriesSeason season) {
 	switch (season) {
-		case Anime::SeriesSeason::UNKNOWN: return "Unknown";
-		case Anime::SeriesSeason::WINTER: return "Winter";
-		case Anime::SeriesSeason::SUMMER: return "Summer";
-		case Anime::SeriesSeason::FALL: return "Fall";
-		case Anime::SeriesSeason::SPRING: return "Spring";
+		case Anime::SeriesSeason::UNKNOWN: return QCoreApplication::tr("Unknown").toStdString();
+		case Anime::SeriesSeason::WINTER: return QCoreApplication::tr("Winter").toStdString();
+		case Anime::SeriesSeason::SUMMER: return QCoreApplication::tr("Summer").toStdString();
+		case Anime::SeriesSeason::FALL: return QCoreApplication::tr("Fall").toStdString();
+		case Anime::SeriesSeason::SPRING: return QCoreApplication::tr("Spring").toStdString();
 		default: return "";
 	}
 }
 
 std::string ToString(const Anime::SeriesStatus status) {
 	switch (status) {
-		case Anime::SeriesStatus::UNKNOWN: return "Unknown";
-		case Anime::SeriesStatus::RELEASING: return "Currently airing";
-		case Anime::SeriesStatus::FINISHED: return "Finished airing";
-		case Anime::SeriesStatus::NOT_YET_RELEASED: return "Not yet aired";
-		case Anime::SeriesStatus::CANCELLED: return "Cancelled";
-		case Anime::SeriesStatus::HIATUS: return "On hiatus";
+		case Anime::SeriesStatus::UNKNOWN: return QCoreApplication::tr("Unknown").toStdString();
+		case Anime::SeriesStatus::RELEASING: return QCoreApplication::tr("Currently airing").toStdString();
+		case Anime::SeriesStatus::FINISHED: return QCoreApplication::tr("Finished airing").toStdString();
+		case Anime::SeriesStatus::NOT_YET_RELEASED: return QCoreApplication::tr("Not yet aired").toStdString();
+		case Anime::SeriesStatus::CANCELLED: return QCoreApplication::tr("Cancelled").toStdString();
+		case Anime::SeriesStatus::HIATUS: return QCoreApplication::tr("On hiatus").toStdString();
 		default: return "";
 	}
 }
--- a/src/gui/widgets/optional_date.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/widgets/optional_date.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -1,4 +1,5 @@
 #include "gui/widgets/optional_date.h"
+#include "core/date.h"
 #include <QCheckBox>
 #include <QDateEdit>
 #include <QHBoxLayout>
@@ -12,7 +13,6 @@
 	layout->setMargin(0);
 
 	_checkbox = new QCheckBox(this);
-	_checkbox->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
 	_checkbox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
 
 	layout->addWidget(_checkbox, 0, Qt::AlignVCenter);
@@ -25,10 +25,17 @@
 	layout->addWidget(_dateedit);
 
 	SetEnabled(enabled);
-	connect(_checkbox, &QCheckBox::stateChanged, this, [this](int state) { SetEnabled(state == Qt::Checked); });
+	connect(_checkbox, &QCheckBox::stateChanged, this, [this](int state) {
+		SetEnabled(state == Qt::Checked);
+		emit DataChanged(IsEnabled(), GetDate());
+	});
+	connect(_dateedit, &QDateEdit::dateChanged, this, [this](QDate) {
+		emit DataChanged(IsEnabled(), GetDate());
+	});
 }
 
 void OptionalDate::SetEnabled(bool enabled) {
+	_checkbox->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
 	_dateedit->setEnabled(enabled);
 }
 
@@ -40,6 +47,15 @@
 	_dateedit->setDate(date);
 }
 
+void OptionalDate::SetDate(Date date) {
+	if (!date.IsValid()) return;
+	SetDate(date.GetAsQDate());
+}
+
+Date OptionalDate::GetDate() {
+	return Date(_dateedit->date());
+}
+
 QDateEdit* OptionalDate::GetDateEdit() {
 	return _dateedit;
 }
@@ -47,3 +63,5 @@
 QCheckBox* OptionalDate::GetCheckBox() {
 	return _checkbox;
 }
+
+#include "gui/widgets/moc_optional_date.cpp"
--- a/src/gui/widgets/text.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/widgets/text.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -146,7 +146,6 @@
 
 /* inherits QPlainTextEdit and gives a much more reasonable minimum size */
 Paragraph::Paragraph(QString text, QWidget* parent) : QPlainTextEdit(text, parent) {
-	setReadOnly(true);
 	setTextInteractionFlags(Qt::TextBrowserInteraction);
 	setFrameShape(QFrame::NoFrame);
 	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
--- a/src/gui/window.cpp	Mon Sep 25 13:50:56 2023 -0400
+++ b/src/gui/window.cpp	Mon Sep 25 20:29:26 2023 -0400
@@ -3,6 +3,7 @@
 #include "core/session.h"
 #include "gui/dark_theme.h"
 #include "gui/dialog/settings.h"
+#include "gui/dialog/about.h"
 #include "gui/pages/anime_list.h"
 #include "gui/pages/now_playing.h"
 #include "gui/pages/statistics.h"
@@ -37,15 +38,15 @@
 	main_widget = new QWidget(parent);
 
 	SideBar* sidebar = new SideBar(main_widget);
-	sidebar->AddItem("Now Playing", SideBar::CreateIcon(":/icons/16x16/film.png"));
+	sidebar->AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
 	sidebar->AddSeparator();
-	sidebar->AddItem("Anime List", SideBar::CreateIcon(":/icons/16x16/document-list.png"));
-	sidebar->AddItem("History", SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
-	sidebar->AddItem("Statistics", SideBar::CreateIcon(":/icons/16x16/chart.png"));
+	sidebar->AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
+	sidebar->AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
+	sidebar->AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
 	sidebar->AddSeparator();
-	sidebar->AddItem("Search", SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
-	sidebar->AddItem("Seasons", SideBar::CreateIcon(":/icons/16x16/calendar.png"));
-	sidebar->AddItem("Torrents", SideBar::CreateIcon(":/icons/16x16/feed.png"));
+	sidebar->AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
+	sidebar->AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
+	sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));
 	sidebar->setFixedWidth(128);
 	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
 
@@ -67,26 +68,26 @@
 	/* Menu Bar */
 	QAction* action;
 	QMenuBar* menubar = new QMenuBar(parent);
-	QMenu* menu = menubar->addMenu("&File");
+	QMenu* menu = menubar->addMenu(tr("&File"));
 
-	QMenu* submenu = menu->addMenu("&Library folders");
-	action = submenu->addAction("&Add new folder...");
+	QMenu* submenu = menu->addMenu(tr("&Library folders"));
+	action = submenu->addAction(tr("&Add new folder..."));
 
-	action = menu->addAction("&Scan available episodes");
+	action = menu->addAction(tr("&Scan available episodes"));
 
 	menu->addSeparator();
 
-	action = menu->addAction("Play &next episode");
+	action = menu->addAction(tr("Play &next episode"));
 	action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_N));
-	action = menu->addAction("Play &random episode");
+	action = menu->addAction(tr("Play &random episode"));
 	action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_R));
 
 	menu->addSeparator();
 
-	action = menu->addAction("E&xit", qApp, &QApplication::quit);
+	action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit);
 
-	menu = menubar->addMenu("&Services");
-	action = menu->addAction("Synchronize &list", [this, stack] {
+	menu = menubar->addMenu(tr("&Services"));
+	action = menu->addAction(tr("Synchronize &list"), [this, stack] {
 		Services::Synchronize();
 		((AnimeListWidget*)stack->widget((int)Pages::ANIME_LIST))->Refresh();
 	});
@@ -94,75 +95,75 @@
 
 	menu->addSeparator();
 
-	submenu = menu->addMenu("&AniList");
-	action = submenu->addAction("Go to my &profile");
-	action = submenu->addAction("Go to my &stats");
+	submenu = menu->addMenu(tr("&AniList"));
+	action = submenu->addAction(tr("Go to my &profile"));
+	action = submenu->addAction(tr("Go to my &stats"));
 
-	submenu = menu->addMenu("&Kitsu");
-	action = submenu->addAction("Go to my &feed");
-	action = submenu->addAction("Go to my &library");
-	action = submenu->addAction("Go to my &profile");
+	submenu = menu->addMenu(tr("&Kitsu"));
+	action = submenu->addAction(tr("Go to my &feed"));
+	action = submenu->addAction(tr("Go to my &library"));
+	action = submenu->addAction(tr("Go to my &profile"));
 
-	submenu = menu->addMenu("&MyAnimeList");
-	action = submenu->addAction("Go to my p&anel");
-	action = submenu->addAction("Go to my &profile");
-	action = submenu->addAction("Go to my &history");
+	submenu = menu->addMenu(tr("&MyAnimeList"));
+	action = submenu->addAction(tr("Go to my p&anel"));
+	action = submenu->addAction(tr("Go to my &profile"));
+	action = submenu->addAction(tr("Go to my &history"));
 
-	menu = menubar->addMenu("&Tools");
-	submenu = menu->addMenu("&Export anime list");
-	action = submenu->addAction("Export as &Markdown...");
-	action = submenu->addAction("Export as MyAnimeList &XML...");
+	menu = menubar->addMenu(tr("&Tools"));
+	submenu = menu->addMenu(tr("&Export anime list"));
+	action = submenu->addAction(tr("Export as &Markdown..."));
+	action = submenu->addAction(tr("Export as MyAnimeList &XML..."));
 
 	menu->addSeparator();
 
-	action = menu->addAction("Enable anime &recognition");
+	action = menu->addAction(tr("Enable anime &recognition"));
 	action->setCheckable(true);
-	action = menu->addAction("Enable auto &sharing");
+	action = menu->addAction(tr("Enable auto &sharing"));
 	action->setCheckable(true);
-	action = menu->addAction("Enable &auto synchronization");
+	action = menu->addAction(tr("Enable &auto synchronization"));
 	action->setCheckable(true);
 
 	menu->addSeparator();
 
-	action = menu->addAction("&Settings", [this] {
+	action = menu->addAction(tr("&Settings"), [this] {
 		SettingsDialog dialog(this);
 		dialog.exec();
 	});
 	action->setMenuRole(QAction::PreferencesRole);
 
-	menu = menubar->addMenu("&View");
+	menu = menubar->addMenu(tr("&View"));
 
 	std::map<QAction*, int> page_to_index_map = {};
 
 	QActionGroup* pages_group = new QActionGroup(this);
 	pages_group->setExclusive(true);
 
-	action = pages_group->addAction(menu->addAction("&Now Playing"));
+	action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 0;
 
-	action = pages_group->addAction(menu->addAction("&Anime List"));
+	action = pages_group->addAction(menu->addAction(tr("&Anime List")));
 	page_to_index_map[action] = 1;
 
 	action->setCheckable(true);
 	action->setChecked(true);
-	action = pages_group->addAction(menu->addAction("&History"));
+	action = pages_group->addAction(menu->addAction(tr("&History")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 2;
 
-	action = pages_group->addAction(menu->addAction("&Statistics"));
+	action = pages_group->addAction(menu->addAction(tr("&Statistics")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 3;
 
-	action = pages_group->addAction(menu->addAction("S&earch"));
+	action = pages_group->addAction(menu->addAction(tr("S&earch")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 4;
 
-	action = pages_group->addAction(menu->addAction("Se&asons"));
+	action = pages_group->addAction(menu->addAction(tr("Se&asons")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 5;
 
-	action = pages_group->addAction(menu->addAction("&Torrents"));
+	action = pages_group->addAction(menu->addAction(tr("&Torrents")));
 	action->setCheckable(true);
 	page_to_index_map[action] = 6;
 
@@ -176,10 +177,14 @@
 		}
 	});
 	menu->addSeparator();
-	menu->addAction("Show sidebar");
+	menu->addAction(tr("Show sidebar"));
 
-	menu = menubar->addMenu("&Help");
-	action = menu->addAction("About &Qt", qApp, [this] { qApp->aboutQt(); });
+	menu = menubar->addMenu(tr("&Help"));
+	action = menu->addAction(tr("About Minori"), this, [this] {
+		AboutWindow dialog(this);
+		dialog.exec();
+	});
+	action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
 	action->setMenuRole(QAction::AboutQtRole);
 
 	setMenuBar(menubar);