view src/anime.cpp @ 1:1ae666fdf9e2

*: initial commit
author Paper <mrpapersonic@gmail.com>
date Tue, 08 Aug 2023 19:49:15 -0400
parents
children 23d0d9319a00
line wrap: on
line source

#include <chrono>
#include <string>
#include <vector>
#include <cmath>
#include "window.h"
#include "anilist.h"
#include "config.h"
#include "anime.h"
//#include "information.h"

std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
	{CURRENT,   "Watching"},
	{PLANNING,  "Planning"},
	{COMPLETED, "Completed"},
	{DROPPED,   "Dropped"},
	{PAUSED,    "On hold"},
	{REPEATING, "Rewatching"}
};

std::map<enum AnimeAiringStatus, std::string> AnimeAiringToStringMap = {
	{FINISHED,         "Finished"},
	{RELEASING,        "Airing"},
	{NOT_YET_RELEASED, "Not aired yet"},
	{CANCELLED,        "Cancelled"},
	{HIATUS,           "On hiatus"}
};

std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap = {
	{WINTER, "Winter"},
	{SPRING, "Spring"},
	{SUMMER, "Summer"},
	{FALL,   "Fall"}
};

std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap = {
	{TV,       "TV"},
	{TV_SHORT, "TV short"},
	{MOVIE,    "Movie"},
	{SPECIAL,  "Special"},
	{OVA,      "OVA"},
	{ONA,      "ONA"},
	{MUSIC,    "Music video"},
	/* these should NEVER be in the list. naybe we should
	   remove them? */
	{MANGA,    "Manga"},
	{NOVEL,    "Novel"},
	{ONE_SHOT, "One-shot"}
};

Anime::Anime() {}
Anime::Anime(const Anime& a) {
	status = a.status;
	progress = a.progress;
	score = a.score;
	started = a.started;
	completed = a.completed;
	notes = a.notes;
	id = a.id;
	title = a.title;
	episodes = a.episodes;
	airing = a.airing;
	air_date = a.air_date;
	genres = a.genres;
	producers = a.producers;
	type = a.type;
	season = a.season;
	audience_score = a.audience_score;
	synopsis = a.synopsis;
	duration = a.duration;
}

void AnimeList::Add(Anime& anime) {
	if (anime_id_to_anime.contains(anime.id))
		return;
	anime_list.push_back(anime);
	anime_id_to_anime.emplace(anime.id, &anime);
}

void AnimeList::Insert(size_t pos, Anime& anime) {
	if (anime_id_to_anime.contains(anime.id))
		return;
	anime_list.insert(anime_list.begin()+pos, anime);
	anime_id_to_anime.emplace(anime.id, &anime);
}

void AnimeList::Delete(size_t index) {
	anime_list.erase(anime_list.begin()+index);
}

void AnimeList::Clear() {
	anime_list.clear();
}

size_t AnimeList::Size() const {
	return anime_list.size();
}

std::vector<Anime>::iterator AnimeList::begin() noexcept {
	return anime_list.begin();
}

std::vector<Anime>::iterator AnimeList::end() noexcept {
	return anime_list.end();
}

std::vector<Anime>::const_iterator AnimeList::cbegin() noexcept {
	return anime_list.cbegin();
}

std::vector<Anime>::const_iterator AnimeList::cend() noexcept {
	return anime_list.cend();
}

AnimeList::AnimeList() {}
AnimeList::AnimeList(const AnimeList& l) {
	for (int i = 0; i < l.Size(); i++) {
		anime_list.push_back(Anime(l[i]));
	}
	name = l.name;
}

AnimeList::~AnimeList() {
	anime_list.clear();
	anime_list.shrink_to_fit();
}

Anime* AnimeList::AnimeById(int id) {
	return anime_id_to_anime.contains(id) ? anime_id_to_anime[id] : nullptr;
}

bool AnimeList::AnimeInList(int id) {
	return anime_id_to_anime.contains(id);
}

Anime& AnimeList::operator[](std::size_t index) {
	return anime_list.at(index);
}

const Anime& AnimeList::operator[](std::size_t index) const {
	return anime_list.at(index);
}

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

/* Thank you qBittorrent for having a great example of a
   widget model. */
AnimeListWidgetModel::AnimeListWidgetModel (QWidget* parent, AnimeList* alist)
                                          : QAbstractListModel(parent)
										  , list(*alist) {
	return;
}

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

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

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_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_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();
	if (role == Qt::DisplayRole) {
		switch (index.column()) {
			case AL_TITLE:
				return QString::fromWCharArray(list[index.row()].title.c_str());
			case AL_PROGRESS:
				return list[index.row()].progress;
			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((int)list[index.row()].air_date.year());
			case AL_AVG_SCORE:
				return list[index.row()].audience_score;
			case AL_STARTED:
				/* why c++20 chrono is stinky: the game */
				return QDate(int(list[index.row()].started.year()), static_cast<int>((unsigned int)list[index.row()].started.month()), static_cast<int>((unsigned int)list[index.row()].started.day()));
			case AL_COMPLETED:
				return QDate(int(list[index.row()].completed.year()), static_cast<int>((unsigned int)list[index.row()].completed.month()), static_cast<int>((unsigned int)list[index.row()].completed.day()));
			case AL_NOTES:
				return QString::fromWCharArray(list[index.row()].notes.c_str());
			default:
				return "";
		}
	} else if (role == Qt::TextAlignmentRole) {
		switch (index.column()) {
			case AL_TITLE:
			case AL_NOTES:
				return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
			case AL_PROGRESS:
			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:
				return QVariant(Qt::AlignRight | Qt::AlignVCenter);
			default:
				break;
		}
	}
	return QVariant();
}

/* this should ALWAYS be called if the list is edited */
void AnimeListWidgetModel::Update() {

}

/* Most of this stuff is const and/or should be edited in the Information dialog

bool AnimeListWidgetModel::setData(const QModelIndex &index, const QVariant &value, int role) {
	if (!index.isValid() || role != Qt::DisplayRole)
		return false;

	Anime* const anime = &list[index.row()];

	switch (index.column()) {
		case AL_TITLE:
			break;
		case AL_CATEGORY:
			break;
		default:
			return false;
	}

	return true;
}
*/

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

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

    return count;
}

void AnimeListWidget::SetColumnDefaults() {
	setColumnHidden(AnimeListWidgetModel::AL_SEASON, false);
	setColumnHidden(AnimeListWidgetModel::AL_TYPE, false);
	setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false);
	setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false);
	setColumnHidden(AnimeListWidgetModel::AL_SCORE, false);
	setColumnHidden(AnimeListWidgetModel::AL_TITLE, false);
	setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true);
	setColumnHidden(AnimeListWidgetModel::AL_STARTED, true);
	setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true);
	setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true);
	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 = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
        QAction *action = menu->addAction(column_name, this, [this, i](const bool checked) {
            if (!checked && (VisibleColumnsCount() <= 1))
                return;

            setColumnHidden(i, !checked);

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

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

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

    menu->popup(QCursor::pos());
}

void AnimeListWidget::DisplayListMenu() {
	/* throw out any other garbage */
    const QModelIndexList selected_items = selectionModel()->selectedRows();
    if (selected_items.size() != 1 || !selected_items.first().isValid())
        return;

	const QModelIndex index = model->index(selected_items.first().row());
	Anime* anime = model->GetAnimeFromIndex(index);
	if (!anime)
		return;

}

void AnimeListWidget::ItemDoubleClicked() {
	/* throw out any other garbage */
    const QModelIndexList selected_items = selectionModel()->selectedRows();
    if (selected_items.size() != 1 || !selected_items.first().isValid())
        return;

	/* TODO: after we implement our sort model, we have to use mapToSource here... */
	const QModelIndex index = model->index(selected_items.first().row());
	Anime* anime = model->GetAnimeFromIndex(index);
	if (!anime)
		return;

	/* todo: open information dialog... */
}

AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist)
                               : QTreeView(parent) {
	model = new AnimeListWidgetModel(parent, alist);
	this->setModel(model);
	setUniformRowHeights(true);
	setAllColumnsShowFocus(false);
	setSortingEnabled(true);
	setSelectionMode(QAbstractItemView::ExtendedSelection);
	setItemsExpandable(false);
	setRootIsDecorated(false);
	setContextMenuPolicy(Qt::CustomContextMenu);
	connect(this, &QAbstractItemView::doubleClicked, this, &ItemDoubleClicked);
	connect(this, &QWidget::customContextMenuRequested, this, &DisplayListMenu);

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

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

	header()->setStretchLastSection(false);
	header()->setContextMenuPolicy(Qt::CustomContextMenu);
	connect(header(), &QWidget::customContextMenuRequested, this, &DisplayColumnHeaderMenu);
	// if(!session.config.anime_list.columns) {
		SetColumnDefaults();
	// }
}

AnimeListPage::AnimeListPage(QWidget* parent) : QTabWidget (parent) {
	setDocumentMode(true);
	SyncAnimeList();
	for (AnimeList& list : anime_lists) {
		addTab(new AnimeListWidget(this, &list), QString::fromWCharArray(list.name.c_str()));
	}
}

void AnimeListPage::SyncAnimeList() {
	switch (session.config.service) {
		case ANILIST: {
			AniList anilist = AniList();
			anilist.Authorize();
			session.config.anilist.user_id = anilist.GetUserId(session.config.anilist.username);
			FreeAnimeList();
			anilist.UpdateAnimeList(&anime_lists, session.config.anilist.user_id);
			break;
		}
	}
}

void AnimeListPage::FreeAnimeList() {
	if (anime_lists.size() > 0) {
		/* FIXME: we may not need this, but to prevent memleaks
		   we should keep it until we're sure we don't */
		for (auto& list : anime_lists) {
			list.Clear();
		}
		anime_lists.clear();
	}
}

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

int AnimeListPage::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 AnimeListPage::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 AnimeListPage::GetTotalPlannedAmount() {
	int total = 0;
	for (auto& list : anime_lists) {
		for (auto& anime : list) {
			total += anime.duration*(anime.episodes-anime.progress);
		}
	}
	return total;
}

double AnimeListPage::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 AnimeListPage::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.cpp"