diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pages/anime_list.cpp	Thu Aug 24 23:11:38 2023 -0400
@@ -0,0 +1,565 @@
+/**
+ * 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"