Mercurial > minori
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"