# HG changeset patch # User Paper # Date 1699362222 18000 # Node ID ab191e28e69d90fe880e39ce360e9d8f4f97d995 # Parent 32afe0e940bf1b00600d68a90e8cb8617105d09d *: add initial torrent stuff WOAH! these checkboxes are a pain in my fucking ass diff -r 32afe0e940bf -r ab191e28e69d CMakeLists.txt --- a/CMakeLists.txt Mon Nov 06 13:48:11 2023 -0500 +++ b/CMakeLists.txt Tue Nov 07 08:03:42 2023 -0500 @@ -12,14 +12,7 @@ option(BUILD_SHARED_LIBS "Build using shared libraries" ON) option(USE_QT6 "Force build with Qt 6" OFF) option(USE_QT5 "Force build with Qt 5" OFF) -# The reason I'm not specifying this an an option() is that -# that will *save the value*, which causes the *.qm translation -# files to not automatically be generated, screwing up the whole -# "automation" part of it. -# -# Ugh. -# -# option(UPDATE_TRANSLATIONS "Update *.ts translation files" OFF) +option(UPDATE_TRANSLATIONS "Update *.ts translation files" OFF) add_subdirectory(dep/anitomy) add_subdirectory(dep/animia) @@ -45,6 +38,7 @@ ${Qt${QT_VERSION_MAJOR}Widgets_LIBRARIES} anitomy animia + pugixml ) # We need Cocoa for some OS X stuff diff -r 32afe0e940bf -r ab191e28e69d include/core/config.h --- a/include/core/config.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/core/config.h Tue Nov 07 08:03:42 2023 -0500 @@ -33,7 +33,13 @@ #define WIDEIFY_EX(x) L##x #define WIDEIFY(x) WIDEIFY_EX(x) + +#if (defined(WIN32) || defined(MACOSX)) +#define CONFIG_DIR "Minori" +#else #define CONFIG_DIR "minori" +#endif + #define CONFIG_WDIR WIDEIFY(CONFIG_DIR) #define CONFIG_NAME "config.ini" #define CONFIG_WNAME WIDEIFY(CONFIG_NAME) diff -r 32afe0e940bf -r ab191e28e69d include/core/strings.h --- a/include/core/strings.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/core/strings.h Tue Nov 07 08:03:42 2023 -0500 @@ -4,72 +4,12 @@ #include #include #include +#include class QString; namespace Strings { -templatestruct seq{using type=seq;}; -template -struct gen_seq_x : gen_seq_x{}; -template -struct gen_seq_x<0, Is...> : seq{}; -template -using gen_seq=typename gen_seq_x::type; - -template -using size=std::integral_constant; - -template -constexpr size length( T const(&)[N] ) { return {}; } -template -constexpr size length( std::array const& ) { return {}; } - -template -using length_t = decltype(length(std::declval())); - -constexpr size_t string_size() { return 0; } -template -constexpr size_t string_size( size_t i, Ts... ts ) { - return (i?i-1:0) + string_size(ts...); -} -template -using string_length=size< string_size( length_t{}... )>; - -template -using combined_string = std::array{}+1>; - -template -constexpr const combined_string -concat_impl( Lhs const& lhs, Rhs const& rhs, seq, seq) -{ - return {{ lhs[I1]..., rhs[I2]..., '\0' }}; -} - -template -constexpr const combined_string -concat(Lhs const& lhs, Rhs const& rhs) -{ - return concat_impl(lhs, rhs, gen_seq{}>{}, gen_seq{}>{}); -} - -template -constexpr const combined_string -concat(T0 const&t0, T1 const&t1, Ts const&...ts) -{ - return concat(t0, concat(t1, ts...)); -} - -template -constexpr const combined_string -concat(T const&t) { - return concat(t, ""); -} -constexpr const combined_string<> -concat() { - return concat(""); -} - /* Implode function: takes a vector of strings and turns it into a string, separated by delimiters. */ std::string Implode(const std::vector& vector, const std::string& delimiter); @@ -98,6 +38,11 @@ /* arithmetic :) */ int ToInt(const std::string& str, int def = 0); +uint64_t HumanReadableSizeToBytes(const std::string& str); + +std::string RemoveLeadingChars(std::string s, const char c); +std::string RemoveTrailingChars(std::string s, const char c); + bool BeginningMatchesSubstring(const std::string& str, const std::string& sub); }; // namespace Strings diff -r 32afe0e940bf -r ab191e28e69d include/core/torrent.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/core/torrent.h Tue Nov 07 08:03:42 2023 -0500 @@ -0,0 +1,56 @@ +#ifndef __core__torrent_h +#define __core__torrent_h + +#include +#include + +/* this will be moved into its own namespace if + it's deemed necessary */ +class Torrent { + public: + std::string GetTitle() const { return _title; }; + std::string GetEpisode() const { return _episode; }; + std::string GetGroup() const { return _group; }; + size_t GetSize() const { return _size; }; + std::string GetResolution() const { return _resolution; }; + int GetSeeders() const { return _seeders; }; + int GetLeechers() const { return _leechers; }; + int GetDownloaders() const { return _downloaders; }; + std::string GetDescription() const { return _description; }; + std::string GetFilename() const { return _filename; }; + std::string GetLink() const { return _link; }; + std::string GetGuid() const { return _guid; }; + QDateTime GetDate() const { return _date; }; + + void SetTitle(const std::string& title) { _title = title; }; + void SetEpisode(const std::string& episode) { _episode = episode; }; + void SetGroup(const std::string& group) { _group = group; }; + void SetSize(const size_t size) { _size = size; }; + void SetResolution(const std::string& resolution) { _resolution = resolution; }; + void SetSeeders(const int seeders) { _seeders = seeders; }; + void SetLeechers(const int leechers) { _leechers = leechers; }; + void SetDownloaders(const int downloaders) { _downloaders = downloaders; }; + void SetDescription(const std::string& description) { _description = description; }; + void SetFilename(const std::string& filename) { _filename = filename; }; + void SetLink(const std::string& link) { _link = link; }; + void SetGuid(const std::string& guid) { _guid = guid; }; + void SetDate(const QDateTime& date) { _date = date; }; + + private: + std::string _title; + std::string _episode; + std::string _group; + size_t _size = 0; + std::string _resolution; /* technically should be an int, + but std::string is more useful */ + int _seeders = 0; + int _leechers = 0; + int _downloaders = 0; + std::string _description; + std::string _filename; + std::string _link; + std::string _guid; + QDateTime _date; +}; + +#endif // __core__torrent_h \ No newline at end of file diff -r 32afe0e940bf -r ab191e28e69d include/gui/pages/anime_list.h --- a/include/gui/pages/anime_list.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/gui/pages/anime_list.h Tue Nov 07 08:03:42 2023 -0500 @@ -41,7 +41,7 @@ NB_COLUMNS }; - AnimeListPageModel(QWidget* parent, Anime::ListStatus _status); + AnimeListPageModel(QObject* parent, Anime::ListStatus _status); ~AnimeListPageModel() override = default; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; @@ -63,7 +63,6 @@ public: AnimeListPage(QWidget* parent); void Refresh(); - void Reset(); protected: void paintEvent(QPaintEvent*) override; diff -r 32afe0e940bf -r ab191e28e69d include/gui/pages/torrents.h --- a/include/gui/pages/torrents.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/gui/pages/torrents.h Tue Nov 07 08:03:42 2023 -0500 @@ -1,13 +1,77 @@ #ifndef __gui__pages__torrents_h #define __gui__pages__torrents_h -#include +#include "core/torrent.h" +#include +#include +#include + +class TorrentModelItem : public Torrent { + public: + bool GetChecked() const { return _checked; }; + void SetChecked(bool checked) { _checked = checked; }; + + private: + bool _checked = false; +}; + +class TorrentsPageListSortFilter final : public QSortFilterProxyModel { + Q_OBJECT + + public: + TorrentsPageListSortFilter(QObject* parent = nullptr); + + protected: + bool lessThan(const QModelIndex& l, const QModelIndex& r) const override; +}; + +class TorrentsPageListModel final : public QAbstractListModel { + Q_OBJECT -class TorrentsPage final : public QWidget { + public: + enum columns { + TL_TITLE, + TL_EPISODE, + TL_GROUP, + TL_SIZE, + TL_RESOLUTION, + TL_SEEDERS, + TL_LEECHERS, + TL_DOWNLOADERS, + TL_DESCRIPTION, + TL_FILENAME, + TL_RELEASEDATE, + + NB_COLUMNS + }; + + TorrentsPageListModel(QObject* parent); + ~TorrentsPageListModel() override = default; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QByteArray DownloadTorrentList(); + void ParseTorrentList(const QByteArray& ba); + void RefreshTorrentList(); + + private: + std::vector list; +}; + +class TorrentsPage final : public QFrame { Q_OBJECT public: TorrentsPage(QWidget* parent = nullptr); + void Refresh(); + + private: + TorrentsPageListModel* model = nullptr; + TorrentsPageListSortFilter* sort_model = nullptr; }; #endif // __gui__pages__torrents_h diff -r 32afe0e940bf -r ab191e28e69d include/gui/window.h --- a/include/gui/window.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/gui/window.h Tue Nov 07 08:03:42 2023 -0500 @@ -2,11 +2,15 @@ #define __window_h #include "core/config.h" #include +#include -class QWidget; -class QStackedWidget; -class QCloseEvent; -class SideBar; +/* *could* be forward-declared, but this causes + any file that #includes this to have to #include + these as well due to unique_ptr */ +#include +#include +#include +#include "gui/widgets/sidebar.h" class MainWindow final : public QMainWindow { Q_OBJECT @@ -23,9 +27,9 @@ void closeEvent(QCloseEvent* event) override; private: - QWidget* main_widget = nullptr; - QStackedWidget* stack = nullptr; - SideBar* sidebar = nullptr; + std::unique_ptr main_widget = nullptr; + std::unique_ptr stack = nullptr; + std::unique_ptr sidebar = nullptr; }; #endif // __window_h diff -r 32afe0e940bf -r ab191e28e69d include/track/media.h --- a/include/track/media.h Mon Nov 06 13:48:11 2023 -0500 +++ b/include/track/media.h Tue Nov 07 08:03:42 2023 -0500 @@ -7,6 +7,7 @@ namespace Media { Filesystem::Path GetCurrentPlaying(); +std::unordered_map GetFileElements(std::string basename); std::unordered_map GetFileElements(Filesystem::Path path); } // namespace Media diff -r 32afe0e940bf -r ab191e28e69d rc/icons.qrc --- a/rc/icons.qrc Mon Nov 06 13:48:11 2023 -0500 +++ b/rc/icons.qrc Tue Nov 07 08:03:42 2023 -0500 @@ -1,13 +1,17 @@ favicon.png + icons/16x16/arrow-circle-315.png icons/16x16/calendar.png icons/16x16/chart.png icons/16x16/clock-history-frame.png + icons/16x16/cross-button.png icons/16x16/document-list.png icons/16x16/feed.png icons/16x16/film.png + icons/16x16/gear.png icons/16x16/magnifier.png + icons/16x16/navigation-270-button.png icons/24x24/application-export.png icons/24x24/application-sidebar-list.png icons/24x24/arrow-circle-double-135.png diff -r 32afe0e940bf -r ab191e28e69d rc/icons/16x16/arrow-circle-315.png Binary file rc/icons/16x16/arrow-circle-315.png has changed diff -r 32afe0e940bf -r ab191e28e69d rc/icons/16x16/cross-button.png Binary file rc/icons/16x16/cross-button.png has changed diff -r 32afe0e940bf -r ab191e28e69d rc/icons/16x16/gear.png Binary file rc/icons/16x16/gear.png has changed diff -r 32afe0e940bf -r ab191e28e69d rc/icons/16x16/navigation-270-button.png Binary file rc/icons/16x16/navigation-270-button.png has changed diff -r 32afe0e940bf -r ab191e28e69d src/core/strings.cc --- a/src/core/strings.cc Mon Nov 06 13:48:11 2023 -0500 +++ b/src/core/strings.cc Tue Nov 07 08:03:42 2023 -0500 @@ -41,7 +41,17 @@ } std::string SanitizeLineEndings(const std::string& string) { - return ReplaceAll(ReplaceAll(ReplaceAll(string, "\r\n", "\n"), "
", "\n"), "\n\n\n", "\n\n"); + /* LOL */ + return + ReplaceAll( + ReplaceAll( + ReplaceAll( + ReplaceAll( + ReplaceAll(string, "\r\n", "\n"), + "

", "\n"), + "
", "\n"), + "
", "\n"), + "\n\n\n", "\n\n"); } /* removes dumb HTML tags because anilist is aids and @@ -146,6 +156,39 @@ return tmp; } +uint64_t HumanReadableSizeToBytes(const std::string& str) { + const std::unordered_map bytes_map = { + {"KB", 1 << 10}, + {"MB", 1 << 20}, + {"GB", 1 << 30}, + {"TB", 1 << 40}, + {"PB", 1 << 50} /* surely we won't need more than this */ + }; + + for (const auto& suffix : bytes_map) { + if (str.find(suffix.first) != std::string::npos) { + try { + uint64_t size = std::stod(str) * suffix.second; + return size; + } catch (std::invalid_argument const& ex) { + continue; + } + } + } + + return ToInt(str, 0); +} + +std::string RemoveLeadingChars(std::string s, const char c) { + s.erase(0, std::min(s.find_first_not_of(c), s.size() - 1)); + return s; +} + +std::string RemoveTrailingChars(std::string s, const char c) { + s.erase(s.find_last_not_of(c) + 1, std::string::npos); + return s; +} + bool BeginningMatchesSubstring(const std::string& str, const std::string& sub) { for (unsigned long long i = 0; i < str.length() && i < sub.length(); i++) if (str[i] != sub[i]) diff -r 32afe0e940bf -r ab191e28e69d src/gui/pages/anime_list.cc --- a/src/gui/pages/anime_list.cc Mon Nov 06 13:48:11 2023 -0500 +++ b/src/gui/pages/anime_list.cc Tue Nov 07 08:03:42 2023 -0500 @@ -47,7 +47,9 @@ } } -AnimeListPageModel::AnimeListPageModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) { +/* -------------------------------------------------- */ + +AnimeListPageModel::AnimeListPageModel(QObject* parent, Anime::ListStatus _status) : QAbstractListModel(parent) { status = _status; return; } @@ -180,6 +182,8 @@ endResetModel(); } +/* ----------------------------------------------------------------- */ + int AnimeListPage::VisibleColumnsCount() const { int count = 0; @@ -316,24 +320,24 @@ dialog->activateWindow(); } -void AnimeListPage::paintEvent(QPaintEvent*) { - QStylePainter p(this); - - QStyleOptionTabWidgetFrame opt; - InitStyle(&opt); - opt.rect = panelRect; - p.drawPrimitive(QStyle::PE_FrameTabWidget, opt); +void AnimeListPage::RefreshList() { + for (unsigned int i = 0; i < sort_models.size(); i++) + reinterpret_cast(sort_models[i]->sourceModel())->RefreshList(); } -void AnimeListPage::resizeEvent(QResizeEvent* e) { - QWidget::resizeEvent(e); - SetupLayout(); +void AnimeListPage::RefreshTabs() { + for (unsigned int i = 0; i < sort_models.size(); i++) + tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" + + QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")"); } -void AnimeListPage::showEvent(QShowEvent*) { - SetupLayout(); +void AnimeListPage::Refresh() { + RefreshList(); + RefreshTabs(); } +/* -------- QTabWidget replication begin --------- */ + void AnimeListPage::InitBasicStyle(QStyleOptionTabWidgetFrame* option) const { if (!option) return; @@ -378,6 +382,26 @@ tree_view->parentWidget()->setGeometry(contentsRect); } +void AnimeListPage::paintEvent(QPaintEvent*) { + QStylePainter p(this); + + QStyleOptionTabWidgetFrame opt; + InitStyle(&opt); + opt.rect = panelRect; + p.drawPrimitive(QStyle::PE_FrameTabWidget, opt); +} + +void AnimeListPage::resizeEvent(QResizeEvent* e) { + QWidget::resizeEvent(e); + SetupLayout(); +} + +void AnimeListPage::showEvent(QShowEvent*) { + SetupLayout(); +} + +/* --------- QTabWidget replication end ---------- */ + AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent) { /* Tab bar */ tab_bar = new QTabBar(this); @@ -438,30 +462,4 @@ Refresh(); } -void AnimeListPage::RefreshList() { - for (unsigned int i = 0; i < sort_models.size(); i++) - reinterpret_cast(sort_models[i]->sourceModel())->RefreshList(); -} - -void AnimeListPage::RefreshTabs() { - for (unsigned int i = 0; i < sort_models.size(); i++) - tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" + - QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")"); -} - -void AnimeListPage::Refresh() { - RefreshList(); - RefreshTabs(); -} - -/* This function, really, really should not be called. - Ever. Why would you ever need to clear the anime list? - Also, this sucks. */ -void AnimeListPage::Reset() { - while (tab_bar->count()) - tab_bar->removeTab(0); - for (unsigned int i = 0; i < sort_models.size(); i++) - delete sort_models[i]; -} - #include "gui/pages/moc_anime_list.cpp" diff -r 32afe0e940bf -r ab191e28e69d src/gui/pages/torrents.cc --- a/src/gui/pages/torrents.cc Mon Nov 06 13:48:11 2023 -0500 +++ b/src/gui/pages/torrents.cc Tue Nov 07 08:03:42 2023 -0500 @@ -1,6 +1,331 @@ #include "gui/pages/torrents.h" +#include "core/strings.h" +#include "core/http.h" +#include "core/session.h" +#include "gui/widgets/text.h" +#include "track/media.h" +#include "pugixml.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TorrentsPageListSortFilter::TorrentsPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) { +} + +bool TorrentsPageListSortFilter::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; + } +} + +/* -------------------------------------------- */ + +TorrentsPageListModel::TorrentsPageListModel(QObject* parent) : QAbstractListModel(parent) { +} + +QByteArray TorrentsPageListModel::DownloadTorrentList() { + return HTTP::Get("https://www.tokyotosho.info/rss.php?filter=1,11&zwnj=0"); +} + +void TorrentsPageListModel::ParseTorrentList(const QByteArray& ba) { + std::istringstream stdstream(Strings::ToUtf8String(ba)); + + pugi::xml_document doc; + if (!doc.load(stdstream)) + return; // peace out + + /* my extra special dumb hack. */ + if (!rowCount(index(0))) { + beginInsertRows(QModelIndex(), 0, 0); + endInsertRows(); + } + + beginResetModel(); + + list.clear(); + /* this is just an rss parser; it should be in a separate class... */ + for (pugi::xml_node item : doc.child("rss").child("channel").children("item")) { + TorrentModelItem torrent; + torrent.SetFilename(item.child_value("title")); /* "title" == filename */ + { + /* Use Anitomy to parse the file's elements (we should *really* not be doing this this way!) */ + std::unordered_map elements = Track::Media::GetFileElements(torrent.GetFilename()); + torrent.SetTitle(elements["title"]); + torrent.SetEpisode(Strings::RemoveLeadingChars(elements["episode"], '0')); + torrent.SetGroup(elements["group"]); + torrent.SetResolution(elements["resolution"]); + } + torrent.SetDescription(Strings::TextifySynopsis(item.child_value("description"))); + { + /* Parse size from description */ + std::istringstream descstream(torrent.GetDescription()); + + for (std::string line; std::getline(descstream, line);) { + const std::string match = "Size: "; + size_t pos = line.find(match); + + if (!pos) { + const std::string size = line.substr(pos + match.length()); + torrent.SetSize(Strings::HumanReadableSizeToBytes(size)); + } + } + } + torrent.SetLink(item.child_value("link")); + torrent.SetGuid(item.child_value("guid")); + { + const QString date_str = Strings::ToQString(item.child_value("pubDate")); + torrent.SetDate(QDateTime::fromString(date_str, "ddd, dd MMM yyyy HH:mm:ss t")); + } + list.push_back(torrent); + } + + endResetModel(); +} + +void TorrentsPageListModel::RefreshTorrentList() { + ParseTorrentList(DownloadTorrentList()); +} + +int TorrentsPageListModel::rowCount(const QModelIndex& parent) const { + return list.size(); + (void)(parent); +} + +int TorrentsPageListModel::columnCount(const QModelIndex& parent) const { + return NB_COLUMNS; + (void)(parent); +} + +QVariant TorrentsPageListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const { + switch (role) { + case Qt::DisplayRole: { + switch (section) { + case TL_TITLE: return tr("Anime title"); + case TL_EPISODE: return tr("Episode"); + case TL_GROUP: return tr("Group"); + case TL_SIZE: return tr("Size"); + case TL_RESOLUTION: return tr("Resolution"); /* this is named "Video" in Taiga */ + case TL_SEEDERS: return tr("Seeding"); /* named "S" in Taiga */ + case TL_LEECHERS: return tr("Leeching"); /* named "L" in Taiga */ + case TL_DOWNLOADERS: return tr("Downloading"); /* named "D" in Taiga */ + case TL_DESCRIPTION: return tr("Description"); + case TL_FILENAME: return tr("Filename"); + case TL_RELEASEDATE: return tr("Release date"); + default: return {}; + } + break; + } + case Qt::TextAlignmentRole: { + switch (section) { + case TL_FILENAME: + case TL_GROUP: + case TL_DESCRIPTION: + case TL_RESOLUTION: + case TL_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case TL_SEEDERS: + case TL_LEECHERS: + case TL_DOWNLOADERS: + case TL_SIZE: + case TL_EPISODE: + case TL_RELEASEDATE: return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: return {}; + } + break; + } + } + return QAbstractListModel::headerData(section, orientation, role); +} + +bool TorrentsPageListModel::setData(const QModelIndex& index, const QVariant& value, int role) { + TorrentModelItem& item = list.at(index.row()); -TorrentsPage::TorrentsPage(QWidget* parent) : QWidget(parent) { + if (index.column() == 0) { + switch (role) { + case Qt::EditRole: + return false; + case Qt::CheckStateRole: + item.SetChecked(value.toBool()); + emit dataChanged(index, index); + return true; + } + } + + return QAbstractItemModel::setData(index, value, role); +} + +QVariant TorrentsPageListModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) + return QVariant(); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case TL_TITLE: return Strings::ToQString(list.at(index.row()).GetTitle()); + case TL_EPISODE: return Strings::ToQString(list.at(index.row()).GetEpisode()); + case TL_GROUP: return Strings::ToQString(list.at(index.row()).GetGroup()); + case TL_SIZE: return session.config.locale.GetLocale().formattedDataSize(list.at(index.row()).GetSize()); + case TL_RESOLUTION: return Strings::ToQString(list.at(index.row()).GetResolution()); + case TL_SEEDERS: return list.at(index.row()).GetSeeders(); + case TL_LEECHERS: return list.at(index.row()).GetLeechers(); + case TL_DOWNLOADERS: return list.at(index.row()).GetDownloaders(); + case TL_DESCRIPTION: return Strings::ToQString(list.at(index.row()).GetDescription()); + case TL_FILENAME: return Strings::ToQString(list.at(index.row()).GetFilename()); + case TL_RELEASEDATE: return list.at(index.row()).GetDate(); + default: return ""; + } + break; + case Qt::UserRole: + switch (index.column()) { + case TL_EPISODE: return Strings::ToInt(list.at(index.row()).GetEpisode(), -1); + case TL_SIZE: return list.at(index.row()).GetSize(); + default: return data(index, Qt::DisplayRole); + } + break; + case Qt::CheckStateRole: + switch (index.column()) { + case 0: return list.at(index.row()).GetChecked() ? Qt::Checked : Qt::Unchecked; + default: return {}; + } + case Qt::SizeHintRole: { + switch (index.column()) { + default: { + const QString d = data(index, Qt::DisplayRole).toString(); + const QFontMetrics metric = QFontMetrics(QFont()); + + return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height()); + } + } + break; + } + case Qt::TextAlignmentRole: + switch (index.column()) { + case TL_FILENAME: + case TL_GROUP: + case TL_DESCRIPTION: + case TL_RESOLUTION: + case TL_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case TL_SEEDERS: + case TL_LEECHERS: + case TL_DOWNLOADERS: + case TL_SIZE: + case TL_EPISODE: + case TL_RELEASEDATE: return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: return {}; + } + break; + } + return QVariant(); +} + +Qt::ItemFlags TorrentsPageListModel::flags(const QModelIndex& index) const { + if (!index.isValid()) + return Qt::NoItemFlags; + + const TorrentModelItem& item = list.at(index.row()); + + if (item.GetChecked() || index.column() == 0) + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; + else + return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; +} + +TorrentsPage::TorrentsPage(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->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toolbar->setIconSize(QSize(16, 16)); + toolbar->setMovable(false); + + { + /* this needs to be stored somewhere to replicate Taiga's + "timer" feature */ + QAction* action = toolbar->addAction(QIcon(":/icons/16x16/arrow-circle-315.png"), tr("&Check new torrents"), [this]{ + QThreadPool::globalInstance()->start([this] { + Refresh(); + }); + }); + } + + toolbar->addSeparator(); + + { + QAction* action = toolbar->addAction(QIcon(":/icons/16x16/navigation-270-button.png"), tr("Download &marked torrents")); + } + + { + QAction* action = toolbar->addAction(QIcon(":/icons/16x16/cross-button.png"), tr("&Discard all")); + } + + toolbar->addSeparator(); + + { + QAction* action = toolbar->addAction(QIcon(":/icons/16x16/gear.png"), tr("&Settings")); + } + + layout->addWidget(toolbar); + } + + { + QFrame* line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + line->setLineWidth(1); + layout->addWidget(line); + } + + { + QTreeView* 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 TorrentsPageListSortFilter(treeview); + model = new TorrentsPageListModel(treeview); + sort_model->setSourceModel(model); + sort_model->setSortRole(Qt::UserRole); + sort_model->setSortCaseSensitivity(Qt::CaseInsensitive); + treeview->setModel(sort_model); + } + + layout->addWidget(treeview); + } +} + +void TorrentsPage::Refresh() { + if (!model) + return; + model->RefreshTorrentList(); } #include "gui/pages/moc_torrents.cpp" diff -r 32afe0e940bf -r ab191e28e69d src/gui/window.cc --- a/src/gui/window.cc Mon Nov 06 13:48:11 2023 -0500 +++ b/src/gui/window.cc Tue Nov 07 08:03:42 2023 -0500 @@ -53,12 +53,12 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowIcon(QIcon(":/favicon.png")); - main_widget = new QWidget(this); - /*QHBoxLayout* layout = */new QHBoxLayout(main_widget); + main_widget.reset(new QWidget(this)); + /*QHBoxLayout* layout = */new QHBoxLayout(main_widget.get()); AddMainWidgets(); - setCentralWidget(main_widget); + setCentralWidget(main_widget.get()); CreateBars(); @@ -81,18 +81,17 @@ void MainWindow::AddMainWidgets() { int page = static_cast(Pages::ANIME_LIST); - if (sidebar) { - main_widget->layout()->removeWidget(sidebar); - delete sidebar; + if (sidebar.get()) { + main_widget->layout()->removeWidget(sidebar.get()); + sidebar.reset(); } - if (stack) { + if (stack.get()) { page = stack->currentIndex(); - main_widget->layout()->removeWidget(stack); - delete stack; + main_widget->layout()->removeWidget(stack.get()); } - sidebar = new SideBar(main_widget); + sidebar.reset(new SideBar(main_widget.get())); sidebar->setFixedWidth(128); sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); @@ -106,182 +105,284 @@ sidebar->AddItem(tr("Seasons"), SideBar::CreateIcon(":/icons/16x16/calendar.png")); sidebar->AddItem(tr("Torrents"), SideBar::CreateIcon(":/icons/16x16/feed.png")); - stack = new QStackedWidget(main_widget); - stack->addWidget(new NowPlayingPage(main_widget)); - stack->addWidget(new AnimeListPage(main_widget)); - stack->addWidget(new HistoryPage(main_widget)); - stack->addWidget(new StatisticsPage(main_widget)); - stack->addWidget(new SearchPage(main_widget)); - stack->addWidget(new SeasonsPage(main_widget)); - stack->addWidget(new TorrentsPage(main_widget)); + stack.reset(new QStackedWidget(main_widget.get())); + stack->addWidget(new NowPlayingPage(main_widget.get())); + stack->addWidget(new AnimeListPage(main_widget.get())); + stack->addWidget(new HistoryPage(main_widget.get())); + stack->addWidget(new StatisticsPage(main_widget.get())); + stack->addWidget(new SearchPage(main_widget.get())); + stack->addWidget(new SeasonsPage(main_widget.get())); + stack->addWidget(new TorrentsPage(main_widget.get())); - connect(sidebar, &SideBar::CurrentItemChanged, stack, &QStackedWidget::setCurrentIndex); + connect(sidebar.get(), &SideBar::CurrentItemChanged, stack.get(), &QStackedWidget::setCurrentIndex); sidebar->SetCurrentItem(page); - main_widget->layout()->addWidget(sidebar); - main_widget->layout()->addWidget(stack); + main_widget->layout()->addWidget(sidebar.get()); + main_widget->layout()->addWidget(stack.get()); } void MainWindow::CreateBars() { - /* Menu Bar */ - QAction* action; + /* Menu Bar + The notation of these might seem ugly at first, but it's actually very nice + (just trust me). It makes it much easier to edit the lists and makes it clear + if you're in submenu or not. */ QMenuBar* menubar = new QMenuBar(this); - QMenu* menu = menubar->addMenu(tr("&File")); - QMenu* submenu = menu->addMenu(tr("&Library folders")); - action = submenu->addAction(tr("&Add new folder...")); + { + /* File */ + QMenu* menu = menubar->addMenu(tr("&File")); - action = menu->addAction(tr("&Scan available episodes")); - - menu->addSeparator(); + { + QMenu* submenu = menu->addMenu(tr("&Library folders")); + { + QAction* action = submenu->addAction(tr("&Add new folder...")); + } + } - action = menu->addAction(tr("Play &next episode")); - action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N)); - action = menu->addAction(tr("Play &random episode")); - action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R)); + { + QAction* action = menu->addAction(tr("&Scan available episodes")); + } - menu->addSeparator(); + menu->addSeparator(); - action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit); +// { +// QAction* action = menu->addAction(tr("Play &next episode")); +// action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N)); +// } +// +// { +// QAction* action = menu->addAction(tr("Play &random episode")); +// action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R)); +// } - menu = menubar->addMenu(tr("&Services")); - action = menu->addAction(tr("Synchronize &list"), [this] { AsyncSynchronize(stack); }); - action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S)); - - menu->addSeparator(); + menu->addSeparator(); - submenu = menu->addMenu(tr("&AniList")); - action = submenu->addAction(tr("Go to my &profile")); - action = submenu->addAction(tr("Go to my &stats")); + { + QAction* action = menu->addAction(tr("E&xit"), qApp, &QApplication::quit); + } + } - submenu = menu->addMenu(tr("&Kitsu")); - action = submenu->addAction(tr("Go to my &feed")); - action = submenu->addAction(tr("Go to my &library")); - action = submenu->addAction(tr("Go to my &profile")); - - submenu = menu->addMenu(tr("&MyAnimeList")); - action = submenu->addAction(tr("Go to my p&anel")); - action = submenu->addAction(tr("Go to my &profile")); - action = submenu->addAction(tr("Go to my &history")); + { + /* Services */ + QMenu* menu = menubar->addMenu(tr("&Services")); + { + { + QAction* action = menu->addAction(tr("Synchronize &list"), [this] { AsyncSynchronize(stack.get()); }); + action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S)); + } - menu = menubar->addMenu(tr("&Tools")); - submenu = menu->addMenu(tr("&Export anime list")); - action = submenu->addAction(tr("Export as &Markdown...")); - action = submenu->addAction(tr("Export as MyAnimeList &XML...")); - - menu->addSeparator(); - - action = menu->addAction(tr("Enable anime &recognition")); - action->setCheckable(true); - action = menu->addAction(tr("Enable auto &sharing")); - action->setCheckable(true); - action = menu->addAction(tr("Enable &auto synchronization")); - action->setCheckable(true); - - menu->addSeparator(); +// menu->addSeparator(); +// +// { +// /* AniList */ +// QMenu* submenu = menu->addMenu(tr("&AniList")); +// QAction* action = submenu->addAction(tr("Go to my &profile")); +// action = submenu->addAction(tr("Go to my &stats")); +// } +// +// { +// /* Kitsu */ +// QMenu* submenu = menu->addMenu(tr("&Kitsu")); +// QAction* action = submenu->addAction(tr("Go to my &feed")); +// action = submenu->addAction(tr("Go to my &library")); +// action = submenu->addAction(tr("Go to my &profile")); +// } +// { +// QMenu* submenu = menu->addMenu(tr("&MyAnimeList")); +// QAction* action = submenu->addAction(tr("Go to my p&anel")); +// action = submenu->addAction(tr("Go to my &profile")); +// action = submenu->addAction(tr("Go to my &history")); +// } + } + } - action = menu->addAction(tr("&Settings"), [this] { - SettingsDialog dialog(this); - dialog.exec(); - }); - action->setMenuRole(QAction::PreferencesRole); - - menu = menubar->addMenu(tr("&View")); - - std::map page_to_index_map = {}; - - QActionGroup* pages_group = new QActionGroup(this); - pages_group->setExclusive(true); + { + /* Tools */ + QMenu* menu = menubar->addMenu(tr("&Tools")); +// { +// /* Export anime list */ +// QMenu* submenu = menu->addMenu(tr("&Export anime list")); +// +// { +// /* Markdown export */ +// QAction* action = submenu->addAction(tr("Export as &Markdown...")); +// } +// +// { +// /* XML export */ +// QAction* action = submenu->addAction(tr("Export as MyAnimeList &XML...")); +// } +// } +// menu->addSeparator(); +// +// { +// QAction* action = menu->addAction(tr("Enable anime &recognition")); +// action->setCheckable(true); +// } +// +// { +// QAction* action = menu->addAction(tr("Enable auto &sharing")); +// action->setCheckable(true); +// } +// +// { +// QAction* action = menu->addAction(tr("Enable &auto synchronization")); +// action->setCheckable(true); +// } +// +// menu->addSeparator(); - action = pages_group->addAction(menu->addAction(tr("&Now Playing"))); - action->setCheckable(true); - page_to_index_map[action] = 0; + { + QAction* action = menu->addAction(tr("&Settings"), [this] { + SettingsDialog dialog(this); + dialog.exec(); + }); + action->setMenuRole(QAction::PreferencesRole); + } + } + + { + /* View */ + QMenu* menu = menubar->addMenu(tr("&View")); - action = pages_group->addAction(menu->addAction(tr("&Anime List"))); - page_to_index_map[action] = 1; - action->setCheckable(true); - action->setChecked(true); + { + /* Pages... */ + std::map page_to_index_map = {}; - action = pages_group->addAction(menu->addAction(tr("&History"))); - action->setCheckable(true); - page_to_index_map[action] = 2; + QActionGroup* pages_group = new QActionGroup(this); + pages_group->setExclusive(true); - action = pages_group->addAction(menu->addAction(tr("&Statistics"))); - action->setCheckable(true); - page_to_index_map[action] = 3; + { + QAction* action = pages_group->addAction(menu->addAction(tr("&Now Playing"))); + action->setCheckable(true); + page_to_index_map[action] = 0; + } - action = pages_group->addAction(menu->addAction(tr("S&earch"))); - action->setCheckable(true); - page_to_index_map[action] = 4; + { + QAction* action = pages_group->addAction(menu->addAction(tr("&Anime List"))); + action->setCheckable(true); + action->setChecked(true); + page_to_index_map[action] = 1; + } + + { + QAction* action = pages_group->addAction(menu->addAction(tr("&History"))); + action->setCheckable(true); + page_to_index_map[action] = 2; + } - action = pages_group->addAction(menu->addAction(tr("Se&asons"))); - action->setCheckable(true); - page_to_index_map[action] = 5; + { + QAction* action = pages_group->addAction(menu->addAction(tr("&Statistics"))); + action->setCheckable(true); + page_to_index_map[action] = 3; + } - action = pages_group->addAction(menu->addAction(tr("&Torrents"))); - action->setCheckable(true); - page_to_index_map[action] = 6; + { + QAction* action = pages_group->addAction(menu->addAction(tr("S&earch"))); + action->setCheckable(true); + page_to_index_map[action] = 4; + } - connect(sidebar, &SideBar::CurrentItemChanged, this, - [pages_group](int index) { pages_group->actions()[index]->setChecked(true); }); + { + QAction* action = pages_group->addAction(menu->addAction(tr("Se&asons"))); + action->setCheckable(true); + page_to_index_map[action] = 5; + } - connect(pages_group, &QActionGroup::triggered, this, - [this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); }); + { + QAction* action = pages_group->addAction(menu->addAction(tr("&Torrents"))); + action->setCheckable(true); + page_to_index_map[action] = 6; + } - menu->addSeparator(); - menu->addAction(tr("Show sidebar")); + /* pain in my ass */ + connect(sidebar.get(), &SideBar::CurrentItemChanged, this, + [pages_group](int index) { pages_group->actions()[index]->setChecked(true); }); + + connect(pages_group, &QActionGroup::triggered, this, + [this, page_to_index_map](QAction* action) { sidebar->SetCurrentItem(page_to_index_map.at(action)); }); + } + + menu->addSeparator(); - menu = menubar->addMenu(tr("&Help")); - action = menu->addAction(tr("&About Minori"), this, [this] { - AboutWindow dialog(this); - dialog.exec(); - }); - action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); - action->setMenuRole(QAction::AboutQtRole); +// { +// QAction* action = menu->addAction(tr("Show sidebar")); +// } + } + + { + /* Help */ + QMenu* menu = menubar->addMenu(tr("&Help")); + { + /* About Minori */ + menu->addAction(tr("&About Minori"), this, [this] { + AboutWindow dialog(this); + dialog.exec(); + }); + } + + { + /* About Qt */ + QAction* action = menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); + action->setMenuRole(QAction::AboutQtRole); + } + } /* QMainWindow will delete the old one for us, according to the docs */ setMenuBar(menubar); /* Toolbar */ + /* remove old toolbar(s) */ - QList toolbars = findChildren(); - for (auto& t : toolbars) { + for (QToolBar*& t : findChildren(Qt::FindDirectChildrenOnly)) { removeToolBar(t); delete t; } - QToolBar* toolbar = new QToolBar(this); - toolbar->addAction(QIcon(":/icons/24x24/arrow-circle-double-135.png"), tr("&Synchronize"), - [this] { AsyncSynchronize(stack); }); - toolbar->addSeparator(); + { + /* Toolbar */ + QToolBar* toolbar = new QToolBar(this); + toolbar->addAction(QIcon(":/icons/24x24/arrow-circle-double-135.png"), tr("&Synchronize"), + [this] { AsyncSynchronize(stack.get()); }); - QToolButton* button = new QToolButton(toolbar); + toolbar->addSeparator(); - menu = new QMenu(button); - action = menu->addAction(tr("Add new folder...")); + { + QToolButton* button = new QToolButton(toolbar); + { + QMenu* menu = new QMenu(button); + QAction* action = menu->addAction(tr("...")); - button->setMenu(menu); - button->setIcon(QIcon(":/icons/24x24/folder-open.png")); - button->setPopupMode(QToolButton::InstantPopup); - toolbar->addWidget(button); + button->setMenu(menu); + } + button->setIcon(QIcon(":/icons/24x24/folder-open.png")); + button->setPopupMode(QToolButton::InstantPopup); + toolbar->addWidget(button); + } - button = new QToolButton(toolbar); + { + QToolButton* button = new QToolButton(toolbar); - menu = new QMenu(button); - action = menu->addAction(tr("Placeholder")); + { + QMenu* menu = new QMenu(button); + QAction* action = menu->addAction(tr("...")); + + button->setMenu(menu); + } - button->setMenu(menu); - button->setIcon(QIcon(":/icons/24x24/application-export.png")); - button->setPopupMode(QToolButton::InstantPopup); - toolbar->addWidget(button); + button->setIcon(QIcon(":/icons/24x24/application-export.png")); + button->setPopupMode(QToolButton::InstantPopup); + toolbar->addWidget(button); + } - toolbar->addSeparator(); - toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] { - SettingsDialog dialog(this); - dialog.exec(); - }); - addToolBar(toolbar); + toolbar->addSeparator(); + toolbar->addAction(QIcon(":/icons/24x24/gear.png"), tr("S&ettings"), [this] { + SettingsDialog dialog(this); + dialog.exec(); + }); + addToolBar(toolbar); + } } void MainWindow::SetActivePage(QWidget* page) { diff -r 32afe0e940bf -r ab191e28e69d src/track/media.cc --- a/src/track/media.cc Mon Nov 06 13:48:11 2023 -0500 +++ b/src/track/media.cc Tue Nov 07 08:03:42 2023 -0500 @@ -40,10 +40,18 @@ ret["language"] = Strings::ToUtf8String(elements.get(anitomy::kElementLanguage)); ret["group"] = Strings::ToUtf8String(elements.get(anitomy::kElementReleaseGroup)); ret["episode"] = Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber)); + ret["resolution"] = Strings::ToUtf8String(elements.get(anitomy::kElementVideoResolution)); return ret; } +std::unordered_map GetFileElements(std::string basename) { + anitomy::Anitomy anitomy; + anitomy.Parse(Strings::ToWstring(basename)); + + return GetMapFromElements(anitomy.elements()); +} + std::unordered_map GetFileElements(Filesystem::Path path) { anitomy::Anitomy anitomy; anitomy.Parse(Strings::ToWstring(path.Basename()));