view src/pages/anime_list.cpp @ 7:07a9095eaeed

Update Refactored some code, moved some around
author Paper <mrpapersonic@gmail.com>
date Thu, 24 Aug 2023 23:11:38 -0400
parents src/anime.cpp@1d82f6e04d7d
children
line wrap: on
line source

/**
 * anime_list.cpp: defines the anime list page
 * and widgets.
 *
 * much of this file is based around
 * Qt's original QTabWidget implementation, because
 * I needed a somewhat native way to create a tabbed
 * widget with only one subwidget that worked exactly
 * like a native tabbed widget.
**/
#include <cmath>
#include <QStyledItemDelegate>
#include <QProgressBar>
#include <QShortcut>
#include <QHBoxLayout>
#include <QStylePainter>
#include <QMenu>
#include <QHeaderView>
#include "anilist.h"
#include "anime.h"
#include "anime_list.h"
#include "information.h"
#include "session.h"
#include "time_utils.h"

AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent)
	: QStyledItemDelegate (parent) {
}

QWidget* AnimeListWidgetDelegate::createEditor(QWidget *, const QStyleOptionViewItem &, const QModelIndex &) const {
    // no edit 4 u
    return nullptr;
}

void AnimeListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    switch (index.column()) {
		case AnimeListWidgetModel::AL_PROGRESS: {
			const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
			const int episodes = static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::AL_EPISODES).data(Qt::UserRole).toReal());

			QStyleOptionViewItem customOption (option);
			customOption.state.setFlag(QStyle::State_Enabled, true);

			progress_bar.paint(painter, customOption, index.data().toString(), progress, episodes);
			break;
		}
		default:
			QStyledItemDelegate::paint(painter, option, index);
			break;
    }
}

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

bool AnimeListWidgetSortFilter::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:
			return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
	}
}

AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
                                          : QAbstractListModel(parent)
										  , list(*alist) {
	return;
}

int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const {
	return list.Size();
	(void)(parent);
}

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

QVariant AnimeListWidgetModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
	if (role == Qt::DisplayRole) {
		switch (section) {
			case AL_TITLE:
				return tr("Anime title");
			case AL_PROGRESS:
				return tr("Progress");
			case AL_EPISODES:
				return tr("Episodes");
			case AL_TYPE:
				return tr("Type");
			case AL_SCORE:
				return tr("Score");
			case AL_SEASON:
				return tr("Season");
			case AL_STARTED:
				return tr("Date started");
			case AL_COMPLETED:
				return tr("Date completed");
			case AL_NOTES:
				return tr("Notes");
			case AL_AVG_SCORE:
				return tr("Average score");
			case AL_UPDATED:
				return tr("Last updated");
			default:
				return {};
		}
	} else if (role == Qt::TextAlignmentRole) {
		switch (section) {
			case AL_TITLE:
			case AL_NOTES:
				return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
			case AL_PROGRESS:
			case AL_EPISODES:
			case AL_TYPE:
			case AL_SCORE:
			case AL_AVG_SCORE:
				return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
			case AL_SEASON:
			case AL_STARTED:
			case AL_COMPLETED:
			case AL_UPDATED:
				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
			default:
				return QAbstractListModel::headerData(section, orientation, role);
		}
	}
	return QAbstractListModel::headerData(section, orientation, role);
}

Anime* AnimeListWidgetModel::GetAnimeFromIndex(const QModelIndex& index) {
	return (index.isValid()) ? &(list[index.row()]) : nullptr;
}

QVariant AnimeListWidgetModel::data(const QModelIndex& index, int role) const {
	if (!index.isValid())
		return QVariant();
	switch (role) {
		case Qt::DisplayRole:
			switch (index.column()) {
				case AL_TITLE:
					return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
				case AL_PROGRESS:
					return QString::number(list[index.row()].progress) + "/" + QString::number(list[index.row()].episodes);
				case AL_EPISODES:
					return list[index.row()].episodes;
				case AL_SCORE:
					return list[index.row()].score;
				case AL_TYPE:
					return QString::fromStdString(AnimeFormatToStringMap[list[index.row()].type]);
				case AL_SEASON:
					return QString::fromStdString(AnimeSeasonToStringMap[list[index.row()].season]) + " " + QString::number(list[index.row()].air_date.GetYear());
				case AL_AVG_SCORE:
					return QString::number(list[index.row()].audience_score) + "%";
				case AL_STARTED:
					return list[index.row()].started.GetAsQDate();
				case AL_COMPLETED:
					return list[index.row()].completed.GetAsQDate();
				case AL_UPDATED: {
					if (list[index.row()].updated == 0)
						return QString("-");
					Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated);
					return QString::fromUtf8(duration.AsRelativeString().c_str());
				}
				case AL_NOTES:
					return QString::fromUtf8(list[index.row()].notes.c_str());
				default:
					return "";
			}
			break;
		case Qt::UserRole:
			switch (index.column()) {
				case AL_PROGRESS:
					return list[index.row()].progress;
				case AL_TYPE:
					return list[index.row()].type;
				case AL_SEASON:
					return list[index.row()].air_date.GetAsQDate();
				case AL_AVG_SCORE:
					return list[index.row()].audience_score;
				case AL_UPDATED:
					return list[index.row()].updated;
				default:
					return data(index, Qt::DisplayRole);
			}
			break;
		case Qt::TextAlignmentRole:
			switch (index.column()) {
				case AL_TITLE:
				case AL_NOTES:
					return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
				case AL_PROGRESS:
				case AL_EPISODES:
				case AL_TYPE:
				case AL_SCORE:
				case AL_AVG_SCORE:
					return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
				case AL_SEASON:
				case AL_STARTED:
				case AL_COMPLETED:
				case AL_UPDATED:
					return QVariant(Qt::AlignRight | Qt::AlignVCenter);
				default:
					break;
			}
			break;
	}
	return QVariant();
}

void AnimeListWidgetModel::UpdateAnime(Anime& anime) {
	int i = list.GetAnimeIndex(anime);
	emit dataChanged(index(i), index(i));
}

void AnimeListWidgetModel::Update(AnimeList const& new_list) {
	list = AnimeList(new_list);
	emit dataChanged(index(0), index(rowCount()));
}

int AnimeListWidget::VisibleColumnsCount() const {
    int count = 0;

    for (int i = 0, end = tree_view->header()->count(); i < end; i++)
    {
        if (!tree_view->isColumnHidden(i))
            count++;
    }

    return count;
}

void AnimeListWidget::SetColumnDefaults() {
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
	tree_view->setColumnHidden(AnimeListWidgetModel::AL_NOTES, true);
}

void AnimeListWidget::DisplayColumnHeaderMenu() {
    QMenu *menu = new QMenu(this);
    menu->setAttribute(Qt::WA_DeleteOnClose);
    menu->setTitle(tr("Column visibility"));
    menu->setToolTipsVisible(true);

    for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) {
		if (i == AnimeListWidgetModel::AL_TITLE)
			continue;
        const auto column_name = sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
        QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
            if (!checked && (VisibleColumnsCount() <= 1))
                return;

            tree_view->setColumnHidden(i, !checked);

            if (checked && (tree_view->columnWidth(i) <= 5))
                tree_view->resizeColumnToContents(i);

            // SaveSettings();
        });
        action->setCheckable(true);
        action->setChecked(!tree_view->isColumnHidden(i));
    }

    menu->addSeparator();
    QAction *resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
        for (int i = 0, count = tree_view->header()->count(); i < count; ++i)
        {
            SetColumnDefaults();
        }
		// SaveSettings();
    });
    menu->popup(QCursor::pos());
	(void)(resetAction);
}

void AnimeListWidget::DisplayListMenu() {
    QMenu *menu = new QMenu(this);
    menu->setAttribute(Qt::WA_DeleteOnClose);
    menu->setTitle(tr("Column visibility"));
    menu->setToolTipsVisible(true);

    const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
    if (!selection.indexes().first().isValid()) {
        return;
	}

	QAction* action = menu->addAction("Information", [this, selection]{
		const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
		Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
		if (!anime) {
			return;
		}

		InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);

		dialog->show();
		dialog->raise();
		dialog->activateWindow();
	});
	menu->popup(QCursor::pos());
}

void AnimeListWidget::ItemDoubleClicked() {
	/* throw out any other garbage */
    const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
    if (!selection.indexes().first().isValid()) {
        return;
	}

	const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->index(selection.indexes().first().row());
	Anime* anime = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index);
	if (!anime) {
		return;
	}

	InformationDialog* dialog = new InformationDialog(*anime, ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()), this);

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

void AnimeListWidget::paintEvent(QPaintEvent*) {
    QStylePainter p(this);

    QStyleOptionTabWidgetFrame opt;
	InitStyle(&opt);
	opt.rect = panelRect;
    p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
}

void AnimeListWidget::resizeEvent(QResizeEvent* e) {
	QWidget::resizeEvent(e);
	SetupLayout();
}

void AnimeListWidget::showEvent(QShowEvent*) {
	SetupLayout();
}

void AnimeListWidget::InitBasicStyle(QStyleOptionTabWidgetFrame *option) const
{
	if (!option)
		return;

    option->initFrom(this);
    option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
	option->shape = QTabBar::RoundedNorth;
    option->tabBarRect = tab_bar->geometry();
}

void AnimeListWidget::InitStyle(QStyleOptionTabWidgetFrame *option) const
{
	if (!option)
		return;

	InitBasicStyle(option);

    //int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
    QSize t(0, tree_view->frameWidth());
    if (tab_bar->isVisibleTo(this)) {
        t = tab_bar->sizeHint();
		t.setWidth(width());
    }
	option->tabBarSize = t;

	QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
	selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
	option->selectedTabRect = selected_tab_rect;

	option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
}

void AnimeListWidget::SetupLayout() {
	QStyleOptionTabWidgetFrame option;
	InitStyle(&option);

	QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
	tabRect.setLeft(tabRect.left()+1);
	panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
	QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);

	tab_bar->setGeometry(tabRect);
	tree_view->parentWidget()->setGeometry(contentsRect);
}

AnimeListWidget::AnimeListWidget(QWidget* parent)
                               : QWidget(parent) {
	/* Tab bar */
	tab_bar = new QTabBar(this);
	tab_bar->setExpanding(false);
	tab_bar->setDrawBase(false);

	/* Tree view... */
	QWidget* tree_widget = new QWidget(this);
	tree_view = new QTreeView(tree_widget);
	tree_view->setItemDelegate(new AnimeListWidgetDelegate(tree_view));
	tree_view->setUniformRowHeights(true);
	tree_view->setAllColumnsShowFocus(false);
	tree_view->setSortingEnabled(true);
	tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
	tree_view->setItemsExpandable(false);
	tree_view->setRootIsDecorated(false);
	tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
	tree_view->setFrameShape(QFrame::NoFrame);
	QHBoxLayout* layout = new QHBoxLayout;
	layout->addWidget(tree_view);
	layout->setMargin(0);
	tree_widget->setLayout(layout);
	connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked);
	connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu);

	/* Enter & return keys */
    connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);

    connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut),
	        &QShortcut::activated, this, &AnimeListWidget::ItemDoubleClicked);

	tree_view->header()->setStretchLastSection(false);
	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
	connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayColumnHeaderMenu);

	connect(tab_bar, &QTabBar::currentChanged, this, [this](int index){
		if (index < sort_models.size())
			tree_view->setModel(sort_models[index]);
	});

	setFocusPolicy(Qt::TabFocus);
	setFocusProxy(tab_bar);
}

void AnimeListWidget::SyncAnimeList() {
	switch (session.config.service) {
		case ANILIST: {
			session.config.anilist.user_id = AniList::GetUserId(session.config.anilist.username);
			FreeAnimeList();
			AniList::UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
			break;
		}
		default:
			break;
	}
	for (unsigned int i = 0; i < anime_lists.size(); i++) {
		tab_bar->addTab(QString::fromStdString(anime_lists[i].name));
		AnimeListWidgetSortFilter* sort_model = new AnimeListWidgetSortFilter(tree_view);
		sort_model->setSourceModel(new AnimeListWidgetModel(this, &anime_lists[i]));
		sort_model->setSortRole(Qt::UserRole);
		sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
		sort_models.push_back(sort_model);
	}
	if (anime_lists.size() > 0)
		tree_view->setModel(sort_models.at(0));
	SetColumnDefaults();
	SetupLayout();
}

void AnimeListWidget::FreeAnimeList() {
	while (tab_bar->count())
		tab_bar->removeTab(0);
	while (sort_models.size()) {
		delete sort_models[sort_models.size()-1];
		sort_models.pop_back();
	}
	for (auto& list : anime_lists) {
		list.Clear();
	}
	anime_lists.clear();
}

int AnimeListWidget::GetTotalAnimeAmount() {
	int total = 0;
	for (auto& list : anime_lists) {
		total += list.Size();
	}
	return total;
}

int AnimeListWidget::GetTotalEpisodeAmount() {
	/* FIXME: this also needs to take into account rewatches... */
	int total = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			total += anime.progress;
		}
	}
	return total;
}

/* Returns the total watched amount in minutes. */
int AnimeListWidget::GetTotalWatchedAmount() {
	int total = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			total += anime.duration*anime.progress;
		}
	}
	return total;
}

/* Returns the total planned amount in minutes.
   Note that we should probably limit progress to the
   amount of episodes, as AniList will let you
   set episode counts up to 32768. But that should
   rather be handled elsewhere. */
int AnimeListWidget::GetTotalPlannedAmount() {
	int total = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			total += anime.duration*(anime.episodes-anime.progress);
		}
	}
	return total;
}

double AnimeListWidget::GetAverageScore() {
	double avg = 0;
	int amt = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			avg += anime.score;
			if (anime.score != 0)
				amt++;
		}
	}
	return avg/amt;
}

double AnimeListWidget::GetScoreDeviation() {
	double squares_sum = 0, avg = GetAverageScore();
	int amt = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			if (anime.score != 0) {
				squares_sum += std::pow((double)anime.score - avg, 2);
				amt++;
			}
		}
	}
	return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
}

#include "moc_anime_list.cpp"