view src/gui/window.cc @ 327:b5d6c27c308f

anime: refactor Anime::SeriesSeason to Season class ToLocalString has also been altered to take in both season and year because lots of locales actually treat formatting seasons differently! most notably is Russian which adds a suffix at the end to notate seasons(??)
author Paper <paper@paper.us.eu.org>
date Thu, 13 Jun 2024 01:49:18 -0400
parents d928ec7b6a0d
children 71396ecb6f7e
line wrap: on
line source

#include "gui/window.h"
#include "core/anime_db.h"
#include "core/config.h"
#include "core/session.h"
#include "core/strings.h"
#include "gui/dialog/about.h"
#include "gui/dialog/settings.h"
#include "gui/pages/anime_list.h"
#include "gui/pages/history.h"
#include "gui/pages/now_playing.h"
#include "gui/pages/search.h"
#include "gui/pages/seasons.h"
#include "gui/pages/statistics.h"
#include "gui/pages/torrents.h"
#include "gui/theme.h"
#include "gui/widgets/sidebar.h"
#include "library/library.h"
#include "services/services.h"
#include "track/media.h"

#include "anitomy/anitomy.h"

#include <QActionGroup>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QMainWindow>
#include <QMenuBar>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTextStream>
#include <QThread>
#include <QThreadPool>
#include <QTimer>
#include <QToolBar>
#include <QToolButton>

#include <iostream>

#ifdef MACOSX
#	include "sys/osx/dark_theme.h"
#	include "sys/osx/permissions.h"
#elif defined(WIN32)
#	include "sys/win32/dark_theme.h"
#endif

void MainWindowPlayingThread::run() {
	std::vector<std::string> files;
	Track::Media::GetCurrentlyPlaying(files);
	emit Done(files);
}

MainWindowAsyncSynchronizeThread::MainWindowAsyncSynchronizeThread(QAction* action, AnimeListPage* page, QObject* parent) : QThread(parent) {
	SetAction(action);
	SetPage(page);
}

void MainWindowAsyncSynchronizeThread::SetAction(QAction* action) {
	action_ = action;
}

void MainWindowAsyncSynchronizeThread::SetPage(AnimeListPage* page) {
	page_ = page;
}

void MainWindowAsyncSynchronizeThread::run() {
	action_->setEnabled(false);
	Services::Synchronize();
	page_->Refresh();
	action_->setEnabled(true);
}

MainWindow::MainWindow(QWidget* parent)
	: QMainWindow(parent)
	, async_synchronize_thread_(nullptr, nullptr) {
	sidebar_.setFixedWidth(128);
	sidebar_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);

	statusBar();

	new QHBoxLayout(&main_widget_);

	CreateBars();

	stack_.addWidget(&now_playing_page_);
	/* ---- */
	stack_.addWidget(&anime_list_page_);
	stack_.addWidget(&history_page_);
	stack_.addWidget(&statistics_page_);
	/* ---- */
	stack_.addWidget(&search_page_);
	stack_.addWidget(&seasons_page_);
	stack_.addWidget(&torrents_page_);

	AddMainWidgets();
	sidebar_.SetCurrentItem(static_cast<int>(Pages::ANIME_LIST));
	setCentralWidget(&main_widget_);

	NowPlayingPage* page = reinterpret_cast<NowPlayingPage*>(stack_.widget(static_cast<int>(Pages::NOW_PLAYING)));

	connect(&playing_thread_, &MainWindowPlayingThread::Done, this, [page](const std::vector<std::string>& files) {
		for (const auto& file : files) {
			anitomy::Anitomy anitomy;
			anitomy.Parse(Strings::ToWstring(file));

			const auto& elements = anitomy.elements();
			const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle));

			int id = Anime::db.LookupAnimeTitle(title);
			if (id <= 0)
				continue;

			page->SetPlaying(Anime::db.items[id], elements);
			break;
		}
	});

	connect(&playing_thread_timer_, &QTimer::timeout, this, [this] {
		if (playing_thread_.isRunning())
			return;

		playing_thread_.start();
	});

#ifdef MACOSX
	if (!osx::AskForPermissions())
		return;
#endif

	playing_thread_timer_.start(5000);
}

void MainWindow::SetStatusMessage(const std::string& message) {
	statusBar()->showMessage(Strings::ToQString(message), 2000);
}

/* Does the main part of what Qt's generic "RetranslateUI" function would do */
void MainWindow::AddMainWidgets() {
	int page = sidebar_.GetCurrentItem();

	sidebar_.clear();

	sidebar_.AddItem(tr("Now Playing"), SideBar::CreateIcon(":/icons/16x16/film.png"));
	sidebar_.AddSeparator();
	sidebar_.AddItem(tr("Anime List"), SideBar::CreateIcon(":/icons/16x16/document-list.png"));
	sidebar_.AddItem(tr("History"), SideBar::CreateIcon(":/icons/16x16/clock-history-frame.png"));
	sidebar_.AddItem(tr("Statistics"), SideBar::CreateIcon(":/icons/16x16/chart.png"));
	sidebar_.AddSeparator();
	sidebar_.AddItem(tr("Search"), SideBar::CreateIcon(":/icons/16x16/magnifier.png"));
	sidebar_.AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png"));
	sidebar_.AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png"));

	sidebar_.SetCurrentItem(page);

	main_widget_.layout()->addWidget(&sidebar_);
	main_widget_.layout()->addWidget(&stack_);
}

void MainWindow::CreateBars() {
	QMenuBar* menubar = new QMenuBar(this);
	QAction* sync_action;

	{
		/* File */
		QMenu* menu = menubar->addMenu(tr("&File"));

		{
			folder_menu = menu->addMenu(tr("&Library folders"));

			UpdateFolderMenu();
		}

		{
			menu->addAction(tr("&Scan available episodes"), [] { Library::SearchLibraryFolders(); });
		}

		menu->addSeparator();

		//		{
		//			QAction* action = menu->addAction(tr("Play &next episode"));
		//			action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N));
		//		}
		//
		//		{
		//			QAction* action = menu->addAction(tr("Play &random episode"));
		//			action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
		//		}

		menu->addSeparator();

		{
			QAction* action = menu->addAction(tr("E&xit"), this, &MainWindow::close);
			action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q));
		}
	}

	{
		/* Services */
		QMenu* menu = menubar->addMenu(tr("&Services"));
		{
			{
				sync_action = menu->addAction(tr("Synchronize &list"));

				connect(sync_action, &QAction::triggered, this,
				        [this, sync_action] { AsyncSynchronize(sync_action, &stack_); });

				sync_action->setIcon(QIcon(":/icons/24x24/arrow-circle-double-135.png"));
				sync_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
			}

			//			menu->addSeparator();
			//
			//			{
			//				/* AniList */
			//				QMenu* submenu = menu->addMenu(tr("&AniList"));
			//				QAction* action = submenu->addAction(tr("Go to my &profile"));
			//				action = submenu->addAction(tr("Go to my &stats"));
			//			}
			//
			//			{
			//				/* Kitsu */
			//				QMenu* submenu = menu->addMenu(tr("&Kitsu"));
			//				QAction* action = submenu->addAction(tr("Go to my &feed"));
			//				action = submenu->addAction(tr("Go to my &library"));
			//				action = submenu->addAction(tr("Go to my &profile"));
			//			}
			//			{
			//				QMenu* submenu = menu->addMenu(tr("&MyAnimeList"));
			//				QAction* 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"));
			//			}
		}
	}

	{
		/* Tools */
		QMenu* menu = menubar->addMenu(tr("&Tools"));
		//		{
		//			/* Export anime list */
		//			QMenu* submenu = menu->addMenu(tr("&Export anime list"));
		//
		//			{
		//				/* Markdown export */
		//				QAction* action = submenu->addAction(tr("Export as &Markdown..."));
		//			}
		//
		//			{
		//				/* XML export */
		//				QAction* action = submenu->addAction(tr("Export as MyAnimeList &XML..."));
		//			}
		//		}
		//		menu->addSeparator();
		//
		//		{
		//			QAction* action = menu->addAction(tr("Enable anime &recognition"));
		//			action->setCheckable(true);
		//		}
		//
		//		{
		//			QAction* action = menu->addAction(tr("Enable auto &sharing"));
		//			action->setCheckable(true);
		//		}
		//
		//		{
		//			QAction* action = menu->addAction(tr("Enable &auto synchronization"));
		//			action->setCheckable(true);
		//		}
		//
		//		menu->addSeparator();

		{
			QAction* action = menu->addAction(tr("&Settings"), [this] {
				SettingsDialog dialog(this);
				dialog.exec();
				UpdateFolderMenu();
			});
			action->setMenuRole(QAction::PreferencesRole);
		}
	}

	{
		/* View */
		QMenu* menu = menubar->addMenu(tr("&View"));

		{
			/* Pages... */
			QActionGroup* pages_group = new QActionGroup(menu);
			pages_group->setExclusive(true);

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(0); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Anime List")));
				action->setCheckable(true);
				action->setChecked(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(1); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&History")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(2); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Statistics")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(3); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("S&earch")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(4); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("Se&asons")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(5); });
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Torrents")));
				action->setCheckable(true);
				connect(action, &QAction::toggled, this, [this] { sidebar_.SetCurrentItem(6); });
			}

			/* pain in the ass */
			disconnect(&sidebar_, &SideBar::CurrentItemChanged, nullptr, nullptr);
			connect(&sidebar_, &SideBar::CurrentItemChanged, &stack_, &QStackedWidget::setCurrentIndex);
			connect(&sidebar_, &SideBar::CurrentItemChanged, this, [pages_group](int index) {
				QAction* checked = pages_group->checkedAction();

				const QList<QAction*>& actions = pages_group->actions();
				if (index > actions.size())
					return;

				if (checked)
					checked->setChecked(false);
				actions[index]->setChecked(true);
			});
		}

		menu->addSeparator();

		//		{
		//			QAction* action = menu->addAction(tr("Show sidebar"));
		//		}
	}

	{
		/* Help */
		QMenu* menu = menubar->addMenu(tr("&Help"));

		{
			/* About Minori */
			menu->addAction(tr("&About Minori"), this, [this] {
				AboutWindow dialog(this);
				dialog.exec();
			});
		}

		{
			/* About Qt */
			QAction* action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
			action->setMenuRole(QAction::AboutQtRole);
		}
	}
	/* QMainWindow will delete the old one for us,
	 * according to the docs
	 */
	setMenuBar(menubar);

	/* Toolbar */

	/* remove old toolbar(s) */
	/* the empty QString() is a Qt 5 wart... */
	for (QToolBar*& t : findChildren<QToolBar*>(QString(), Qt::FindDirectChildrenOnly)) {
		removeToolBar(t);
		delete t;
	}

	{
		/* Toolbar */
		QToolBar* toolbar = new QToolBar(this);
		toolbar->addAction(sync_action);

		toolbar->addSeparator();

		{
			QToolButton* button = new QToolButton(toolbar);
			{ button->setMenu(folder_menu); }
			button->setIcon(QIcon(":/icons/24x24/folder-open.png"));
			button->setPopupMode(QToolButton::InstantPopup);
			toolbar->addWidget(button);
		}

		{
			QToolButton* button = new QToolButton(toolbar);

			{
				/* links */
				QMenu* menu = new QMenu(button);
				menu->addAction("Hibari", [] { QDesktopServices::openUrl(QUrl("https://hb.wopian.me/")); });
				menu->addAction("MALgraph", [] { QDesktopServices::openUrl(QUrl("https://graph.anime.plus/")); });
				menu->addSeparator();
				menu->addAction("AniChart", [] { QDesktopServices::openUrl(QUrl("https://anichart.net/airing")); });
				menu->addAction("Monthly.moe",
				                [] { QDesktopServices::openUrl(QUrl("https://www.monthly.moe/weekly")); });
				menu->addAction("Senpai Anime Charts",
				                [] { QDesktopServices::openUrl(QUrl("https://www.senpai.moe/?mode=calendar")); });
				menu->addSeparator();
				menu->addAction("Anime Streaming Search Engine",
				                [] { QDesktopServices::openUrl(QUrl("https://because.moe/")); });
				menu->addAction("The Fansub Database", [] { QDesktopServices::openUrl(QUrl("https://fansubdb.com")); });

				button->setMenu(menu);
			}

			button->setIcon(QIcon(":/icons/24x24/application-export.png"));
			button->setPopupMode(QToolButton::InstantPopup);
			toolbar->addWidget(button);
		}

		toolbar->addSeparator();
		toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] {
			SettingsDialog dialog(this);
			dialog.exec();
			/* 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);
}

void MainWindow::AsyncSynchronize(QAction* action, QStackedWidget* stack) {
	if (session.config.service == Anime::Service::None) {
		QMessageBox msg;
		msg.setWindowTitle(tr("Error synchronizing with service!"));
		msg.setText(tr("It seems you haven't yet selected a service to use."));
		msg.setInformativeText(tr("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();
		}
	}

	/* FIXME: make this use a QThread; this is *very* unsafe */
	AnimeListPage* page = reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)));
	if (!async_synchronize_thread_.isRunning()) {
		async_synchronize_thread_.SetAction(action);
		async_synchronize_thread_.SetPage(page);
		async_synchronize_thread_.start();
	}
}

void MainWindow::RetranslateUI() {
	/* This sucks a LOT */
	setUpdatesEnabled(false);
	AddMainWidgets();
	CreateBars();
	setUpdatesEnabled(true);
}

void MainWindow::changeEvent(QEvent* event) {
	if (event) { /* is this really necessary */
		switch (event->type()) {
			// this event is send if a translator is loaded
			case QEvent::LanguageChange: RetranslateUI(); break;

			default: break;
		}
	}
	QMainWindow::changeEvent(event);
}

void MainWindow::showEvent(QShowEvent* event) {
	QMainWindow::showEvent(event);
#ifdef WIN32
	/* Technically this *should* be
	 * session.config.theme.IsInDarkTheme() && win32::IsInDarkTheme()
	 * but I prefer the title bar being black even when light mode
	 * is enabled :/ */
	win32::SetTitleBarsToBlack(session.config.theme.IsInDarkTheme());
#endif
}

void MainWindow::closeEvent(QCloseEvent* event) {
	playing_thread_timer_.stop();
	playing_thread_.wait();
	async_synchronize_thread_.wait();

	session.config.Save();
	Anime::db.SaveDatabaseToDisk();
	event->accept();
}