view src/gui/pages/search.cc @ 367:8d45d892be88 default tip

*: instead of pugixml, use Qt XML features this means we have one extra Qt dependency though...
author Paper <paper@tflc.us>
date Sun, 17 Nov 2024 22:55:47 -0500
parents b5d6c27c308f
children
line wrap: on
line source

#include "gui/pages/search.h"
#include "core/anime.h"
#include "core/anime_db.h"
#include "core/filesystem.h"
#include "core/http.h"
#include "core/session.h"
#include "core/strings.h"
#include "gui/dialog/information.h"
#include "gui/translate/anime.h"
#include "gui/widgets/text.h"
#include "services/services.h"
#include "track/media.h"

#include <QDate>
#include <QHeaderView>
#include <QMenu>
#include <QToolBar>
#include <QTreeView>
#include <QVBoxLayout>

#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>

SearchPageSearchThread::SearchPageSearchThread(QObject* parent) : QThread(parent) {
}

void SearchPageSearchThread::SetSearch(const std::string& search) {
	search_ = search;
}

void SearchPageSearchThread::run() {
	emit GotResults(Services::Search(search_));
}

SearchPageListSortFilter::SearchPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
}

bool SearchPageListSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
	QVariant left = sourceModel()->data(l, sortRole());
	QVariant right = sourceModel()->data(r, sortRole());

	switch (left.userType()) {
		case QMetaType::Int:
		case QMetaType::UInt:
		case QMetaType::LongLong:
		case QMetaType::ULongLong: return left.toInt() < right.toInt();
		case QMetaType::QDate: return left.toDate() < right.toDate();
		case QMetaType::QString:
		default: // meh
			return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
	}
}

/* -------------------------------------------- */

SearchPageListModel::SearchPageListModel(QObject* parent) : QAbstractListModel(parent) {
}

void SearchPageListModel::ParseSearch(const std::vector<int>& ids) {
	/* hack!!! */
	if (!rowCount(index(0))) {
		beginInsertRows(QModelIndex(), 0, 0);
		endInsertRows();
	}

	beginResetModel();

	this->ids = ids;

	endResetModel();
}

int SearchPageListModel::rowCount(const QModelIndex& parent) const {
	return ids.size();
	(void)(parent);
}

int SearchPageListModel::columnCount(const QModelIndex& parent) const {
	return NB_COLUMNS;
	(void)(parent);
}

QVariant SearchPageListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
	switch (role) {
		case Qt::DisplayRole: {
			switch (section) {
				case SR_TITLE: return tr("Anime title");
				case SR_EPISODES: return tr("Episode");
				case SR_TYPE: return tr("Type");
				case SR_SCORE: return tr("Score");
				case SR_SEASON: return tr("Season");
				default: return {};
			}
			break;
		}
		case Qt::TextAlignmentRole: {
			switch (section) {
				case SR_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
				case SR_TYPE: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
				case SR_EPISODES:
				case SR_SCORE:
				case SR_SEASON: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
				default: return {};
			}
			break;
		}
	}
	return QAbstractListModel::headerData(section, orientation, role);
}

QVariant SearchPageListModel::data(const QModelIndex& index, int role) const {
	if (!index.isValid())
		return QVariant();

	const Anime::Anime& anime = Anime::db.items[ids[index.row()]];

	switch (role) {
		case Qt::DisplayRole:
			switch (index.column()) {
				case SR_TITLE: return Strings::ToQString(anime.GetUserPreferredTitle());
				case SR_TYPE: return Strings::ToQString(Translate::ToLocalString(anime.GetFormat()));
				case SR_EPISODES: return anime.GetEpisodes();
				case SR_SCORE: return QString::number(anime.GetAudienceScore()) + "%";
				case SR_SEASON: return Strings::ToQString(Translate::ToLocalString(anime.GetSeason()));
				default: return {};
			}
			break;
		case Qt::UserRole:
			switch (index.column()) {
				case SR_SCORE: return anime.GetAudienceScore();
				case SR_EPISODES: return anime.GetEpisodes();
				case SR_SEASON: return anime.GetStartedDate().GetAsQDate();
				/* We have to use this to work around some stupid
				 * "conversion ambiguous" error on Linux
				 */
				default: return data(index, Qt::DisplayRole);
			}
			break;
		case Qt::SizeHintRole: {
			switch (index.column()) {
				default: {
					/* max horizontal size of 100, height size = size of current font */
					const QString d = data(index, Qt::DisplayRole).toString();
					const QFontMetrics metric = QFontMetrics(QFont());

					return QSize(std::max(metric.boundingRect(d).width(), 100), metric.height());
				}
			}
			break;
		}
		case Qt::TextAlignmentRole: return headerData(index.column(), Qt::Horizontal, Qt::TextAlignmentRole);
	}
	return QVariant();
}

Qt::ItemFlags SearchPageListModel::flags(const QModelIndex& index) const {
	if (!index.isValid())
		return Qt::NoItemFlags;

	return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

Anime::Anime* SearchPageListModel::GetAnimeFromIndex(const QModelIndex& index) const {
	return &Anime::db.items[ids[index.row()]];
}

void SearchPage::DisplayListMenu() {
	QMenu* menu = new QMenu(this);
	menu->setAttribute(Qt::WA_DeleteOnClose);
	menu->setToolTipsVisible(true);

	const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());

	bool add_to_list_enable = true;

	std::set<Anime::Anime*> animes;
	for (const auto& index : selection.indexes()) {
		if (!index.isValid())
			continue;

		Anime::Anime* anime = model->GetAnimeFromIndex(index);
		if (anime) {
			animes.insert(anime);
			if (anime->IsInUserList())
				add_to_list_enable = false;
		}
	}

	menu->addAction(tr("Information"), [this, animes] {
		for (auto& anime : animes) {
			InformationDialog* dialog = new InformationDialog(
			    anime,
			    [this](Anime::Anime* anime) {
				    // UpdateAnime(anime->GetId());
			    },
			    InformationDialog::PAGE_MAIN_INFO, this);

			dialog->show();
			dialog->raise();
			dialog->activateWindow();
		}
	});
	menu->addSeparator();
	{
		QMenu* submenu = menu->addMenu(tr("Add to list..."));
		for (const auto& status : Anime::ListStatuses) {
			submenu->addAction(Strings::ToQString(Translate::ToLocalString(status)), [animes, status] {
				for (auto& anime : animes) {
					if (!anime->IsInUserList())
						anime->AddToUserList();
					anime->SetUserStatus(status);
					Services::UpdateAnimeEntry(anime->GetId());
				}
			});
		}
		submenu->setEnabled(add_to_list_enable);
	}
	menu->popup(QCursor::pos());
}

void SearchPage::ItemDoubleClicked() {
	/* throw out any other garbage */
	const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());
	if (!selection.indexes().first().isValid())
		return;

	const QModelIndex index = model->index(selection.indexes().first().row());
	Anime::Anime* anime = model->GetAnimeFromIndex(index);

	InformationDialog* dialog = new InformationDialog(
	    anime,
	    [this](Anime::Anime* anime) {
		    // UpdateAnime(anime->GetId());
	    },
	    InformationDialog::PAGE_MAIN_INFO, this);

	dialog->show();
	dialog->raise();
	dialog->activateWindow();
}

SearchPage::SearchPage(QWidget* parent) : QFrame(parent) {
	setFrameShape(QFrame::Box);
	setFrameShadow(QFrame::Sunken);

	QVBoxLayout* layout = new QVBoxLayout(this);
	layout->setContentsMargins(0, 0, 0, 0);
	layout->setSpacing(0);

	{
		/* Toolbar */
		QToolBar* toolbar = new QToolBar(this);
		toolbar->setMovable(false);

		{
			QLineEdit* line_edit = new QLineEdit("", toolbar);
			connect(line_edit, &QLineEdit::returnPressed, this, [this, line_edit] {
				/* static thread here. */
				if (thread_.isRunning())
					thread_.exit(1); /* fail */

				thread_.SetSearch(Strings::ToUtf8String(line_edit->text()));

				thread_.start();
			});
			connect(&thread_, &SearchPageSearchThread::GotResults, this, [this](const std::vector<int>& search) {
				model->ParseSearch(search);
			});
			toolbar->addWidget(line_edit);
		}

		layout->addWidget(toolbar);
	}

	{
		QFrame* line = new QFrame(this);
		line->setFrameShape(QFrame::HLine);
		line->setFrameShadow(QFrame::Sunken);
		line->setLineWidth(1);
		layout->addWidget(line);
	}

	{
		treeview = new QTreeView(this);
		treeview->setUniformRowHeights(true);
		treeview->setAllColumnsShowFocus(false);
		treeview->setAlternatingRowColors(true);
		treeview->setSortingEnabled(true);
		treeview->setSelectionMode(QAbstractItemView::ExtendedSelection);
		treeview->setItemsExpandable(false);
		treeview->setRootIsDecorated(false);
		treeview->setContextMenuPolicy(Qt::CustomContextMenu);
		treeview->setFrameShape(QFrame::NoFrame);

		{
			sort_model = new SearchPageListSortFilter(treeview);
			model = new SearchPageListModel(treeview);
			sort_model->setSourceModel(model);
			sort_model->setSortRole(Qt::UserRole);
			sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
			treeview->setModel(sort_model);
		}

		// set column sizes
		treeview->setColumnWidth(SearchPageListModel::SR_TITLE, 400);
		treeview->setColumnWidth(SearchPageListModel::SR_TYPE, 60);
		treeview->setColumnWidth(SearchPageListModel::SR_EPISODES, 60);
		treeview->setColumnWidth(SearchPageListModel::SR_SCORE, 60);
		treeview->setColumnWidth(SearchPageListModel::SR_SEASON, 100);

		treeview->header()->setStretchLastSection(false);

		/* Double click stuff */
		connect(treeview, &QAbstractItemView::doubleClicked, this, &SearchPage::ItemDoubleClicked);
		connect(treeview, &QWidget::customContextMenuRequested, this, &SearchPage::DisplayListMenu);

		layout->addWidget(treeview);
	}
}