view src/gui/window.cc @ 235:593108b3d555

dep/animia: x11: finalize xcb conversion
author Paper <paper@paper.us.eu.org>
date Tue, 16 Jan 2024 15:22:29 -0500
parents ff0061e75f0f
children 4d461ef7d424
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/theme.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/widgets/sidebar.h"
#include "services/services.h"
#include "track/media.h"
#include "library/library.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 <QTextStream>
#include <QThread>
#include <QThreadPool>
#include <QTimer>
#include <QToolBar>
#include <QToolButton>

#include <iostream>

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

enum class Pages {
	NOW_PLAYING,

	ANIME_LIST,
	HISTORY,
	STATISTICS,

	SEARCH,
	SEASONS,
	TORRENTS
};

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

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
	setWindowIcon(QIcon(":/favicon.png"));

	main_widget.reset(new QWidget(this));
	new QHBoxLayout(main_widget.get());

	AddMainWidgets();

	setCentralWidget(main_widget.get());

	CreateBars();

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

	qRegisterMetaType<std::vector<std::string>>();

	/* This thread will be destroyed on
	 * close of the program OR on the destruction
	 * of MainWindow
	*/
	thread.reset(new PlayingThread(this));

	connect(thread.get(), &PlayingThread::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();

			int id = Anime::db.GetAnimeFromTitle(Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)));
			if (id <= 0)
				continue;

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

	QTimer* timer = new QTimer(this);

	connect(timer, &QTimer::timeout, this, [this, page] {
		if (!thread.get() || thread->isRunning())
			return;

		thread->start();
	});

	timer->start(5000);
}

void MainWindow::AddMainWidgets() {
	int page = static_cast<int>(Pages::ANIME_LIST);
	if (sidebar.get()) {
		main_widget->layout()->removeWidget(sidebar.get());
		sidebar.reset();
	}

	if (stack.get()) {
		page = stack->currentIndex();
		main_widget->layout()->removeWidget(stack.get());
	}

	sidebar.reset(new SideBar(main_widget.get()));
	sidebar->setFixedWidth(128);
	sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);

	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"));

	stack.reset(new QStackedWidget(main_widget.get()));
	stack->addWidget(new NowPlayingPage(main_widget.get()));
	stack->addWidget(new AnimeListPage(main_widget.get()));
	stack->addWidget(new HistoryPage(main_widget.get()));
	stack->addWidget(new StatisticsPage(main_widget.get()));
	stack->addWidget(new SearchPage(main_widget.get()));
	stack->addWidget(new SeasonsPage(main_widget.get()));
	stack->addWidget(new TorrentsPage(main_widget.get()));

	connect(sidebar.get(), &SideBar::CurrentItemChanged, stack.get(), &QStackedWidget::setCurrentIndex);
	sidebar->SetCurrentItem(page);

	main_widget->layout()->addWidget(sidebar.get());
	main_widget->layout()->addWidget(stack.get());
}

void MainWindow::CreateBars() {
	QMenuBar* menubar = new QMenuBar(this);
	QMenu* folder_menu; /* this is used twice, so we declare it here */

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

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

			/* add in all of our existing folders... */
			std::size_t i = 0;
			for (const auto& path : session.config.library.paths) {
				const QString folder = Strings::ToQString(path);
				QAction* action = folder_menu->addAction(folder, [folder]{
					QDesktopServices::openUrl(QUrl::fromLocalFile(folder));
				});
				if (i < 9)
					action->setShortcut(QKeySequence(Qt::ALT | static_cast<Qt::Modifier>(Qt::Key_1 + i)));
				else if (i == 9)
					action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_0));
				/* don't bother with a shortcut in case of more... */
				i++;
			}

			folder_menu->addSeparator();

			{
				folder_menu->addAction(tr("&Add new folder..."), [this]{
					const QString dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"),
																		  QDir::homePath(),
																		  QFileDialog::ShowDirsOnly
																		  | QFileDialog::DontResolveSymlinks);
					if (dir.isEmpty())
						return;
					session.config.library.paths.insert(Strings::ToUtf8String(dir));
					/* we have to recreate the menu bar to add the new folder */
					CreateBars();
				});
			}
		}

		{
			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"), qApp, &QApplication::quit);
			action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q));
		}
	}

	{
		/* Services */
		QMenu* menu = menubar->addMenu(tr("&Services"));
		{
			{
				QAction* action = menu->addAction(tr("Synchronize &list"), [this] { AsyncSynchronize(stack.get()); });
				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();
			});
			action->setMenuRole(QAction::PreferencesRole);
		}
	}

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

		{
			/* Pages... */
			std::map<QAction*, int> page_to_index_map = {};

			QActionGroup* pages_group = new QActionGroup(this);
			pages_group->setExclusive(true);

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Now Playing")));
				action->setCheckable(true);
				page_to_index_map[action] = 0;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Anime List")));
				action->setCheckable(true);
				action->setChecked(true);
				page_to_index_map[action] = 1;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&History")));
				action->setCheckable(true);
				page_to_index_map[action] = 2;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Statistics")));
				action->setCheckable(true);
				page_to_index_map[action] = 3;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("S&earch")));
				action->setCheckable(true);
				page_to_index_map[action] = 4;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("Se&asons")));
				action->setCheckable(true);
				page_to_index_map[action] = 5;
			}

			{
				QAction* action = pages_group->addAction(menu->addAction(tr("&Torrents")));
				action->setCheckable(true);
				page_to_index_map[action] = 6;
			}

			/* pain in my ass */
			connect(sidebar.get(), &SideBar::CurrentItemChanged, this,
					[pages_group](int index) { pages_group->actions()[index]->setChecked(true); });

			connect(pages_group, &QActionGroup::triggered, this,
					[this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); });
		}

		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(QIcon(":/icons/24x24/arrow-circle-double-135.png"), tr("&Synchronize"),
						   [this] { AsyncSynchronize(stack.get()); });

		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();
		});
		addToolBar(toolbar);
	}
}

void MainWindow::SetActivePage(QWidget* page) {
	this->setCentralWidget(page);
}

void MainWindow::AsyncSynchronize(QStackedWidget* stack) {
	if (session.config.service == Anime::Services::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();
		}
	}
	QThreadPool::globalInstance()->start([stack] {
		Services::Synchronize();
		reinterpret_cast<AnimeListPage*>(stack->widget(static_cast<int>(Pages::ANIME_LIST)))->Refresh();
	});
}

void MainWindow::RetranslateUI() {
	/* This kinda sucks but nobody's really going to be changing
	   the application language all the time :p */
	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) {
	session.config.Save();
	Anime::db.SaveDatabaseToDisk();
	event->accept();
}

#include "gui/moc_window.cpp"