view src/gui/window.cc @ 319:d928ec7b6a0d

services/kitsu: implement GetAnimeList() it finally works!
author Paper <paper@paper.us.eu.org>
date Wed, 12 Jun 2024 17:52:26 -0400
parents 34347fd2a2de
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();
}