Mercurial > minori
changeset 2:23d0d9319a00
Update
Also converted everything to LF from CRLF
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Sat, 12 Aug 2023 03:16:26 -0400 |
parents | 1ae666fdf9e2 |
children | 190ded9438c0 |
files | CMakeLists.txt rc/icons.qrc rc/icons/README.md src/anilist.cpp src/anime.cpp src/config.cpp src/date.cpp src/dialog/information.cpp src/filesystem.cpp src/include/anilist.h src/include/anime.h src/include/config.h src/include/date.h src/include/filesystem.h src/include/information.h src/include/json.h src/include/now_playing.h src/include/statistics.h src/include/string_utils.h src/include/sys/osx/dark_theme.h src/include/sys/win32/dark_theme.h src/include/time_utils.h src/include/ui_utils.h src/include/window.h src/json.cpp src/main.cpp src/pages/now_playing.cpp src/pages/statistics.cpp src/string_utils.cpp src/sys/osx/dark_theme.mm src/sys/win32/dark_theme.cpp src/time.cpp src/ui_utils.cpp src/window.cpp |
diffstat | 34 files changed, 2318 insertions(+), 1997 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Tue Aug 08 19:49:15 2023 -0400 +++ b/CMakeLists.txt Sat Aug 12 03:16:26 2023 -0400 @@ -1,33 +1,36 @@ -cmake_minimum_required(VERSION 3.5) -project(weeaboo) - -set(SRC_FILES - src/main.cpp - src/config.cpp - src/filesystem.cpp - src/anilist.cpp - src/anime.cpp -# src/pages/statistics.cpp -# src/pages/now_playing.cpp -# src/dialog/information.cpp -# src/ui_utils.cpp - src/string_utils.cpp - rc/icons.qrc - dep/darkstyle/darkstyle.qrc -) - -if(APPLE) - list(APPEND SRC_FILES src/sys/osx/dark_theme.mm) -elseif(WIN32) - list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp) -endif() - -add_executable(weeaboo WIN32 MACOSX_BUNDLE ${SRC_FILES}) -set_property(TARGET weeaboo PROPERTY CXX_STANDARD 20) -set_property(TARGET weeaboo PROPERTY AUTOMOC ON) -set_property(TARGET weeaboo PROPERTY AUTORCC ON) - -find_package(Qt5 COMPONENTS Widgets REQUIRED) -find_package(CURL REQUIRED) -target_include_directories(weeaboo PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} PRIVATE src/include src/icons dep/json) -target_link_libraries(weeaboo ${Qt5Widgets_LIBRARIES} ${CURL_LIBRARIES}) +cmake_minimum_required(VERSION 3.5) +project(weeaboo) + +set(SRC_FILES + src/main.cpp + src/config.cpp + src/filesystem.cpp + src/anilist.cpp + src/anime.cpp + src/json.cpp + src/date.cpp + src/time.cpp + src/dialog/information.cpp + src/ui_utils.cpp + src/string_utils.cpp + rc/icons.qrc + dep/darkstyle/darkstyle.qrc +# src/pages/statistics.cpp +# src/pages/now_playing.cpp +) + +if(APPLE) + list(APPEND SRC_FILES src/sys/osx/dark_theme.mm) +elseif(WIN32) + list(APPEND SRC_FILES src/sys/win32/dark_theme.cpp) +endif() + +add_executable(weeaboo WIN32 MACOSX_BUNDLE ${SRC_FILES}) +set_property(TARGET weeaboo PROPERTY CXX_STANDARD 20) +set_property(TARGET weeaboo PROPERTY AUTOMOC ON) +set_property(TARGET weeaboo PROPERTY AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(CURL REQUIRED) +target_include_directories(weeaboo PUBLIC ${Qt5Widgets_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} PRIVATE src/include src/icons) +target_link_libraries(weeaboo ${Qt5Widgets_LIBRARIES} ${CURL_LIBRARIES})
--- a/rc/icons.qrc Tue Aug 08 19:49:15 2023 -0400 +++ b/rc/icons.qrc Sat Aug 12 03:16:26 2023 -0400 @@ -1,14 +1,14 @@ -<!DOCTYPE rcc><RCC version="1.0"> - <qresource> - <file>icons/16x16/calendar.png</file> - <file>icons/16x16/chart.png</file> - <file>icons/16x16/clock-history-frame.png</file> - <file>icons/16x16/document-list.png</file> - <file>icons/16x16/feed.png</file> - <file>icons/16x16/film.png</file> - <file>icons/16x16/magnifier.png</file> - <file>icons/24x24/arrow-circle-double-135.png</file> - <file>icons/24x24/folder-open.png</file> - <file>icons/24x24/gear.png</file> - </qresource> +<!DOCTYPE rcc><RCC version="1.0"> + <qresource> + <file>icons/16x16/calendar.png</file> + <file>icons/16x16/chart.png</file> + <file>icons/16x16/clock-history-frame.png</file> + <file>icons/16x16/document-list.png</file> + <file>icons/16x16/feed.png</file> + <file>icons/16x16/film.png</file> + <file>icons/16x16/magnifier.png</file> + <file>icons/24x24/arrow-circle-double-135.png</file> + <file>icons/24x24/folder-open.png</file> + <file>icons/24x24/gear.png</file> + </qresource> </RCC> \ No newline at end of file
--- a/rc/icons/README.md Tue Aug 08 19:49:15 2023 -0400 +++ b/rc/icons/README.md Sat Aug 12 03:16:26 2023 -0400 @@ -1,2 +1,2 @@ -# Icons +# Icons These icons are from Yusuke Kamiyamane's [[Fugue Icons]](https://p.yusukekamiyamane.com/) pack. The original files have been left intact alongside bin2h variants (for easy inclusion in source code). \ No newline at end of file
--- a/src/anilist.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/anilist.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,235 +1,234 @@ -#include "window.h" -#include "json.h" -#include <curl/curl.h> -#include <chrono> -#include <exception> -#include <format> -#include "anilist.h" -#include "anime.h" -#include "config.h" -#include "string_utils.h" -#define CLIENT_ID "13706" - -size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) { - ((std::string*)userdata)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -std::string AniList::SendRequest(std::string data) { - struct curl_slist *list = NULL; - std::string userdata; - curl = curl_easy_init(); - if (curl) { - list = curl_slist_append(list, "Accept: application/json"); - list = curl_slist_append(list, "Content-Type: application/json"); - std::string bearer = "Authorization: Bearer " + session.config.anilist.auth_token; - list = curl_slist_append(list, bearer.c_str()); - curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co"); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); - /* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer - is actually valid. I assume the best way to go about this would be to bundle a - certificate file, and if it's not found we should *prompt the user* and ask them - if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this - flag will be set. If not, we should abort mission. - - For this program, it's probably fine to just contact AniList without - HTTPS verification. However it should still be in the list of things to do... */ - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE); - res = curl_easy_perform(curl); - curl_slist_free_all(list); - if (res != CURLE_OK) { - QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res))); - box.exec(); - curl_easy_cleanup(curl); - return ""; - } - curl_easy_cleanup(curl); - return userdata; - } - return ""; -} - -int AniList::GetUserId(std::string name) { -#define QUERY "query ($name: String) {\n" \ - " User (name: $name) {\n" \ - " id\n" \ - " }\n" \ - "}\n" - nlohmann::json json = { - {"query", QUERY}, - {"variables", { - {"name", name} - }} - }; - auto ret = nlohmann::json::parse(SendRequest(json.dump())); - return ret["data"]["User"]["id"].get<int>(); -#undef QUERY -} - -/* Maps to convert string forms to our internal enums */ - -std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = { - {"CURRENT", CURRENT}, - {"PLANNING", PLANNING}, - {"COMPLETED", COMPLETED}, - {"DROPPED", DROPPED}, - {"PAUSED", PAUSED}, - {"REPEATING", REPEATING} -}; - -std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = { - {"FINISHED", FINISHED}, - {"RELEASING", RELEASING}, - {"NOT_YET_RELEASED", NOT_YET_RELEASED}, - {"CANCELLED", CANCELLED}, - {"HIATUS", HIATUS} -}; - -std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = { - {"WINTER", WINTER}, - {"SPRING", SPRING}, - {"SUMMER", SUMMER}, - {"FALL", FALL} -}; - -std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = { - {"TV", TV}, - {"TV_SHORT", TV_SHORT}, - {"MOVIE", MOVIE}, - {"SPECIAL", SPECIAL}, - {"OVA", OVA}, - {"ONA", ONA}, - {"MUSIC", MUSIC}, - {"MANGA", MANGA}, - {"NOVEL", NOVEL}, - {"ONE_SHOT", ONE_SHOT} -}; - -int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) { -#define QUERY "query ($id: Int) {\n" \ -" MediaListCollection (userId: $id, type: ANIME) {\n" \ -" lists {\n" \ -" name\n" \ -" entries {\n" \ -" score\n" \ -" notes\n" \ -" progress\n" \ -" startedAt {\n" \ -" year\n" \ -" month\n" \ -" day\n" \ -" }\n" \ -" completedAt {\n" \ -" year\n" \ -" month\n" \ -" day\n" \ -" }\n" \ -" media {\n" \ -" id\n" \ -" title {\n" \ -" userPreferred\n" \ -" }\n" \ -" format\n" \ -" status\n" \ -" averageScore\n" \ -" season\n" \ -" startDate {\n" \ -" year\n" \ -" month\n" \ -" day\n" \ -" }\n" \ -" genres\n" \ -" episodes\n" \ -" duration\n" \ -" synonyms\n" \ -" description(asHtml: false)\n" \ -" }\n" \ -" }\n" \ -" }\n" \ -" }\n" \ -"}\n" - nlohmann::json json = { - {"query", QUERY}, - {"variables", { - {"id", id} - }} - }; - /* TODO: do a try catch here, catch any json errors and then call - Authorize() if needed */ - auto res = nlohmann::json::parse(SendRequest(json.dump())); - /* TODO: make sure that we actually need the wstring converter and see - if we can just get wide strings back from nlohmann::json */ - for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) { - /* why are the .key() values strings?? */ - int list_key = std::stoi(list.key()); - AnimeList anime_list; - anime_list.name = StringUtils::Utf8ToWstr(list.value()["name"].get<std::string>()); - for (const auto& entry : list.value()["entries"].items()) { - int entry_key = std::stoi(entry.key()); - Anime anime; - anime.score = entry.value()["score"].get<int>(); - anime.progress = entry.value()["progress"].get<int>(); - if (entry.value()["status"].is_string()) - anime.status = StringToAnimeWatchingMap[entry.value()["status"].get<std::string>()]; - if (entry.value()["notes"].is_string()) - anime.notes = StringUtils::Utf8ToWstr(entry.value()["notes"].get<std::string>()); - - if (ANILIST_DATE_IS_VALID(entry.value()["startedAt"])) - anime.started = ANILIST_DATE_TO_YMD(entry.value()["startedAt"]); - if (ANILIST_DATE_IS_VALID(entry.value()["completedAt"])) - anime.completed = ANILIST_DATE_TO_YMD(entry.value()["completedAt"]); - - anime.title = StringUtils::Utf8ToWstr(entry.value()["media"]["title"]["userPreferred"].get<std::string>()); - anime.id = entry.value()["media"]["id"].get<int>(); - if (!entry.value()["media"]["episodes"].is_null()) - anime.episodes = entry.value()["media"]["episodes"].get<int>(); - else // hasn't aired yet - anime.episodes = 0; - - if (!entry.value()["media"]["format"].is_null()) - anime.type = StringToAnimeFormatMap[entry.value()["media"]["format"].get<std::string>()]; - - anime.airing = StringToAnimeAiringMap[entry.value()["media"]["status"].get<std::string>()]; - - if (ANILIST_DATE_IS_VALID(entry.value()["media"]["startDate"])) - anime.air_date = ANILIST_DATE_TO_YMD(entry.value()["media"]["startDate"]); - - if (entry.value()["media"]["averageScore"].is_number()) - anime.audience_score = entry.value()["media"]["averageScore"].get<int>(); - - if (entry.value()["media"]["season"].is_string()) - anime.season = StringToAnimeSeasonMap[entry.value()["media"]["season"].get<std::string>()]; - - if (entry.value()["media"]["duration"].is_number()) - anime.duration = entry.value()["media"]["duration"].get<int>(); - else - anime.duration = 0; - - if (entry.value()["media"]["genres"].is_array()) - anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>(); - if (entry.value()["media"]["description"].is_string()) - anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(entry.value()["media"]["description"].get<std::string>())); - anime_list.Add(anime); - } - anime_lists->push_back(anime_list); - } - return 1; -} - -int AniList::Authorize() { - if (session.config.anilist.auth_token.empty()) { - /* Prompt for PIN */ - QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token")); - bool ok; - QString token = QInputDialog::getText(0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok); - if (ok && !token.isEmpty()) { - session.config.anilist.auth_token = token.toStdString(); - } else { // fail - return 0; - } - } - return 1; -} +#include "window.h" +#include "json.h" +#include <curl/curl.h> +#include <chrono> +#include <exception> +#include <format> +#include "anilist.h" +#include "anime.h" +#include "config.h" +#include "string_utils.h" +#define CLIENT_ID "13706" + +size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) { + ((std::string*)userdata)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +std::string AniList::SendRequest(std::string data) { + struct curl_slist *list = NULL; + std::string userdata; + curl = curl_easy_init(); + if (curl) { + list = curl_slist_append(list, "Accept: application/json"); + list = curl_slist_append(list, "Content-Type: application/json"); + std::string bearer = "Authorization: Bearer " + session.config.anilist.auth_token; + list = curl_slist_append(list, bearer.c_str()); + curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); + /* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer + is actually valid. I assume the best way to go about this would be to bundle a + certificate file, and if it's not found we should *prompt the user* and ask them + if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this + flag will be set. If not, we should abort mission. + + For this program, it's probably fine to just contact AniList without + HTTPS verification. However it should still be in the list of things to do... */ + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE); + res = curl_easy_perform(curl); + curl_slist_free_all(list); + if (res != CURLE_OK) { + QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res))); + box.exec(); + curl_easy_cleanup(curl); + return ""; + } + curl_easy_cleanup(curl); + return userdata; + } + return ""; +} + +int AniList::GetUserId(std::string name) { +#define QUERY "query ($name: String) {\n" \ + " User (name: $name) {\n" \ + " id\n" \ + " }\n" \ + "}\n" + nlohmann::json json = { + {"query", QUERY}, + {"variables", { + {"name", name} + }} + }; + auto ret = nlohmann::json::parse(SendRequest(json.dump())); + return ret["data"]["User"]["id"].get<int>(); +#undef QUERY +} + +/* Maps to convert string forms to our internal enums */ + +std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = { + {"CURRENT", CURRENT}, + {"PLANNING", PLANNING}, + {"COMPLETED", COMPLETED}, + {"DROPPED", DROPPED}, + {"PAUSED", PAUSED}, + {"REPEATING", REPEATING} +}; + +std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = { + {"FINISHED", FINISHED}, + {"RELEASING", RELEASING}, + {"NOT_YET_RELEASED", NOT_YET_RELEASED}, + {"CANCELLED", CANCELLED}, + {"HIATUS", HIATUS} +}; + +std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = { + {"WINTER", WINTER}, + {"SPRING", SPRING}, + {"SUMMER", SUMMER}, + {"FALL", FALL} +}; + +std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = { + {"TV", TV}, + {"TV_SHORT", TV_SHORT}, + {"MOVIE", MOVIE}, + {"SPECIAL", SPECIAL}, + {"OVA", OVA}, + {"ONA", ONA}, + {"MUSIC", MUSIC}, + {"MANGA", MANGA}, + {"NOVEL", NOVEL}, + {"ONE_SHOT", ONE_SHOT} +}; + +int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) { +/* NOTE: these should be in the qrc file */ +#define QUERY "query ($id: Int) {\n" \ +" MediaListCollection (userId: $id, type: ANIME) {\n" \ +" lists {\n" \ +" name\n" \ +" entries {\n" \ +" score\n" \ +" notes\n" \ +" progress\n" \ +" startedAt {\n" \ +" year\n" \ +" month\n" \ +" day\n" \ +" }\n" \ +" completedAt {\n" \ +" year\n" \ +" month\n" \ +" day\n" \ +" }\n" \ +" updatedAt\n" \ +" media {\n" \ +" id\n" \ +" title {\n" \ +" romaji\n" \ +" english\n" \ +" native\n" \ +" }\n" \ +" format\n" \ +" status\n" \ +" averageScore\n" \ +" season\n" \ +" startDate {\n" \ +" year\n" \ +" month\n" \ +" day\n" \ +" }\n" \ +" genres\n" \ +" episodes\n" \ +" duration\n" \ +" synonyms\n" \ +" description(asHtml: false)\n" \ +" }\n" \ +" }\n" \ +" }\n" \ +" }\n" \ +"}\n" + nlohmann::json json = { + {"query", QUERY}, + {"variables", { + {"id", id} + }} + }; + /* TODO: do a try catch here, catch any json errors and then call + Authorize() if needed */ + auto res = nlohmann::json::parse(SendRequest(json.dump())); + /* TODO: make sure that we actually need the wstring converter and see + if we can just get wide strings back from nlohmann::json */ + for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) { + /* why are the .key() values strings?? */ + int list_key = std::stoi(list.key()); + AnimeList anime_list; + anime_list.name = StringUtils::Utf8ToWstr(JSON::GetString(list.value(), "name")); + for (const auto& entry : list.value()["entries"].items()) { + int entry_key = std::stoi(entry.key()); + Anime anime; + anime.score = JSON::GetInt(entry.value(), "score"); + anime.progress = JSON::GetInt(entry.value(), "progress"); + anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")]; + anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes")); + + anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year")); + anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month")); + anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day")); + + anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year")); + anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month")); + anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day")); + + anime.updated = JSON::GetInt(entry.value(), "updatedAt"); + + anime.title.native = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native")); + anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english")); + anime.title.romaji = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji")); + + anime.id = JSON::GetInt(entry.value()["media"], "id"); + anime.episodes = JSON::GetInt(entry.value()["media"], "episodes"); + anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")]; + + anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")]; + + anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year")); + anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month")); + anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day")); + + anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore"); + anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")]; + anime.duration = JSON::GetInt(entry.value()["media"], "duration"); + anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration"))); + + if (entry.value()["media"]["genres"].is_array()) + anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>(); + anime_list.Add(anime); + } + anime_lists->push_back(anime_list); + } + return 1; +#undef QUERY +} + +int AniList::Authorize() { + if (session.config.anilist.auth_token.empty()) { + /* Prompt for PIN */ + QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token")); + bool ok; + QString token = QInputDialog::getText(0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok); + if (ok && !token.isEmpty()) { + session.config.anilist.auth_token = token.toStdString(); + } else { // fail + return 0; + } + } + return 1; +}
--- a/src/anime.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/anime.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,513 +1,539 @@ -#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" +#include <chrono> +#include <string> +#include <vector> +#include <cmath> +#include "window.h" +#include "anilist.h" +#include "config.h" +#include "anime.h" +#include "date.h" +#include "time_utils.h" +#include "information.h" +#include "ui_utils.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 = { + {UNKNOWN, "Unknown"}, + {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; + updated = a.updated; + 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); +} + +int AnimeList::GetAnimeIndex(Anime& anime) const { + for (int i = 0; i < Size(); i++) { + if (&anime_list.at(i) == &anime) { // lazy + return i; + } + } + return -1; +} + +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.english.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(list[index.row()].air_date.GetYear()); + case AL_AVG_SCORE: + return 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::fromStdString(duration.AsRelativeString()); + } + 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: + case AL_UPDATED: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: + break; + } + } + return QVariant(); +} + +void AnimeListWidgetModel::UpdateAnime(Anime& anime) { + int i = list.GetAnimeIndex(anime); + emit dataChanged(index(i), index(i)); +} + +/* 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; + + InformationDialog* dialog = new InformationDialog(*anime, model, this); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); +} + +AnimeListWidget::AnimeListWidget(QWidget* parent, AnimeList* alist) + : QTreeView(parent) { + model = new AnimeListWidgetModel(parent, alist); + setModel(model); + setObjectName("listwidget"); + setStyleSheet("QTreeView#listwidget{border-top:0px;}"); + 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(false); + 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"
--- a/src/config.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/config.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,76 +1,76 @@ -/** - * config.cpp: - * parses the config - * - * much of this is similar to the code used in - * wgsdk... - * maybe some of this will be C++-ified someday ;) -**/ -#include <filesystem> /* Sorry, C++17 is just sexy. if you have boost you can probably change this */ -#ifdef MACOSX -#include <NSSystemDirectories.h> -#endif -#include <limits.h> -#include <cstdlib> -#include <cstring> -#include <fstream> -#include "json.h" -#include "config.h" -#include "window.h" -#include "filesystem.h" - -std::map<std::string, enum Themes> StringToTheme = { - {"Default", OS}, - {"Light", LIGHT}, - {"Dark", DARK} -}; - -std::map<enum Themes, std::string> ThemeToString = { - {OS, "Default"}, - {LIGHT, "Light"}, - {DARK, "Dark"} -}; - -int Config::Load() { - std::filesystem::path cfg_path = get_config_path(); - if (!std::filesystem::exists(cfg_path)) - return 0; - std::ifstream config_in(cfg_path.string().c_str(), std::ifstream::in); - auto config_js = nlohmann::json::parse(config_in); -/* this macro will make it easier to edit these in the future, if needed */ -#define GET_CONFIG_VALUE(pointer, location, struct, default) \ - struct = (config_js.contains(pointer)) ? (location) : (default) - GET_CONFIG_VALUE("/General/Service"_json_pointer, (enum AnimeListServices)config_js["General"]["Service"].get<int>(), service, NONE); - GET_CONFIG_VALUE("/Authorization/AniList/Auth Token"_json_pointer, config_js["Authorization"]["AniList"]["Auth Token"].get<std::string>(), anilist.auth_token, ""); - GET_CONFIG_VALUE("/Authorization/AniList/Username"_json_pointer, config_js["Authorization"]["AniList"]["Username"].get<std::string>(), anilist.username, ""); - GET_CONFIG_VALUE("/Authorization/AniList/User ID"_json_pointer, config_js["Authorization"]["AniList"]["User ID"].get<int>(), anilist.user_id, 0); - GET_CONFIG_VALUE("/Appearance/Theme"_json_pointer, StringToTheme[config_js["Appearance"]["Theme"].get<std::string>()], theme, OS); -#undef GET_CONFIG_VALUE - config_in.close(); - return 0; -} - -int Config::Save() { - std::filesystem::path cfg_path = get_config_path(); - if (!std::filesystem::exists(cfg_path.parent_path())) - std::filesystem::create_directories(cfg_path.parent_path()); - std::ofstream config_out(cfg_path.string().c_str(), std::ofstream::out | std::ofstream::trunc); - nlohmann::json config_js = { - {"General", { - {"Service", service} - }}, - {"Authorization", { - {"AniList", { - {"Auth Token", anilist.auth_token}, - {"Username", anilist.username}, - {"User ID", anilist.user_id} - }} - }}, - {"Appearance", { - {"Theme", ThemeToString[theme]} - }} - }; - config_out << std::setw(4) << config_js << std::endl; - config_out.close(); - return 0; -} +/** + * config.cpp: + * parses the config + * + * much of this is similar to the code used in + * wgsdk... + * maybe some of this will be C++-ified someday ;) +**/ +#include <filesystem> /* Sorry, C++17 is just sexy. if you have boost you can probably change this */ +#ifdef MACOSX +#include <NSSystemDirectories.h> +#endif +#include <limits.h> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include "json.h" +#include "config.h" +#include "window.h" +#include "filesystem.h" + +std::map<std::string, enum Themes> StringToTheme = { + {"Default", OS}, + {"Light", LIGHT}, + {"Dark", DARK} +}; + +std::map<enum Themes, std::string> ThemeToString = { + {OS, "Default"}, + {LIGHT, "Light"}, + {DARK, "Dark"} +}; + +int Config::Load() { + std::filesystem::path cfg_path = get_config_path(); + if (!std::filesystem::exists(cfg_path)) + return 0; + std::ifstream config_in(cfg_path.string().c_str(), std::ifstream::in); + auto config_js = nlohmann::json::parse(config_in); +/* this macro will make it easier to edit these in the future, if needed */ +#define GET_CONFIG_VALUE(pointer, location, struct, default) \ + struct = (config_js.contains(pointer)) ? (location) : (default) + GET_CONFIG_VALUE("/General/Service"_json_pointer, (enum AnimeListServices)config_js["General"]["Service"].get<int>(), service, NONE); + GET_CONFIG_VALUE("/Authorization/AniList/Auth Token"_json_pointer, config_js["Authorization"]["AniList"]["Auth Token"].get<std::string>(), anilist.auth_token, ""); + GET_CONFIG_VALUE("/Authorization/AniList/Username"_json_pointer, config_js["Authorization"]["AniList"]["Username"].get<std::string>(), anilist.username, ""); + GET_CONFIG_VALUE("/Authorization/AniList/User ID"_json_pointer, config_js["Authorization"]["AniList"]["User ID"].get<int>(), anilist.user_id, 0); + GET_CONFIG_VALUE("/Appearance/Theme"_json_pointer, StringToTheme[config_js["Appearance"]["Theme"].get<std::string>()], theme, OS); +#undef GET_CONFIG_VALUE + config_in.close(); + return 0; +} + +int Config::Save() { + std::filesystem::path cfg_path = get_config_path(); + if (!std::filesystem::exists(cfg_path.parent_path())) + std::filesystem::create_directories(cfg_path.parent_path()); + std::ofstream config_out(cfg_path.string().c_str(), std::ofstream::out | std::ofstream::trunc); + nlohmann::json config_js = { + {"General", { + {"Service", service} + }}, + {"Authorization", { + {"AniList", { + {"Auth Token", anilist.auth_token}, + {"Username", anilist.username}, + {"User ID", anilist.user_id} + }} + }}, + {"Appearance", { + {"Theme", ThemeToString[theme]} + }} + }; + config_out << std::setw(4) << config_js << std::endl; + config_out.close(); + return 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/date.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,54 @@ +#include "date.h" +#include <QDate> +#include <cstdint> + +#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) + +#define CLAMP(x, low, high) ({\ + __typeof__(x) __x = (x); \ + __typeof__(low) __low = (low);\ + __typeof__(high) __high = (high);\ + __x > __high ? __high : (__x < __low ? __low : __x);\ + }) + +Date::Date() { +} + +Date::Date(int32_t y) { + year = MAX(0, y); +} + +Date::Date(int32_t y, int8_t m, int8_t d) { + year = MAX(0, y); + month = CLAMP(m, 1, 12); + day = CLAMP(d, 1, 31); +} + +void Date::SetYear(int32_t y) { + year = MAX(0, y); +} + +void Date::SetMonth(int8_t m) { + month = CLAMP(m, 1, 12); +} + +void Date::SetDay(int8_t d) { + day = CLAMP(d, 1, 31); +} + +int32_t Date::GetYear() { + return year; +} + +int8_t Date::GetMonth() { + return month; +} + +int8_t Date::GetDay() { + return day; +} + +QDate Date::GetAsQDate() { + return QDate(year, month, day); +}
--- a/src/dialog/information.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/dialog/information.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,39 +1,67 @@ -#include "window.h" -#include "anime.h" -#include "information.h" -#include "ui_utils.h" -#include "string_utils.h" - -InformationDialog::InformationDialog(wxWindow* parent, wxWindowID id, const wxString& title, const Anime& anime, - const wxPoint& position, long style) - : wxDialog(parent, id, title, position, wxSize(840, 613), style) { - wxFont font; - this->SetBackgroundColour(*wxWHITE); - wxPanel* left_panel = new wxPanel(this, wxID_ANY, wxPoint(0, 12), wxSize(170, 518)); - wxPanel* right_panel = new wxPanel(this, wxID_ANY, wxPoint(170, 12), wxSize(840-186, 518)); - right_panel->SetBackgroundColour(*wxWHITE); - wxTextCtrl* anime_title = new wxTextCtrl(right_panel, wxID_ANY, anime.title, wxPoint(0, 0), wxSize(840-186, 28), wxTE_LEFT | wxBORDER_NONE | wxTE_BESTWRAP | wxTE_READONLY | wxTE_MULTILINE | wxTE_NO_VSCROLL); - anime_title->SetForegroundColour(wxTheColourDatabase->Find("STEEL BLUE")); - font = anime_title->GetFont(); - font.SetPointSize(12); - anime_title->SetFont(font); - wxNotebook* notebook = new wxNotebook(right_panel, wxID_ANY, wxPoint(0, 35), wxSize(840-186, 518-35)); - notebook->SetBackgroundColour(*wxWHITE); - wxPanel* main_information_panel = new wxPanel(notebook, wxID_ANY, wxPoint(6, 6), wxDefaultSize); - UiUtils::CreateSelectableTextParagraph(main_information_panel, L"Alternative titles", L"-", 840-186-6-6, 15); - std::wstringstream details_data; - details_data << AnimeFormatToStringMap[anime.type] << "\n" - << anime.episodes << "\n" - << AnimeAiringToStringMap[anime.airing] << "\n" - << AnimeSeasonToStringMap[anime.season] << " " << int(anime.air_date.year()) << "\n" - << StringUtils::Implode(anime.genres, ", ") << "\n" - << anime.audience_score << "%\n"; - UiUtils::CreateTextParagraphWithLabels(main_information_panel, L"Details", L"Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data.str().c_str(), 840-186-6-6, 94, 0, 50); - UiUtils::CreateSelectableTextParagraph(main_information_panel, L"Synopsis", anime.synopsis.c_str(), 840-186-6-6, 200, 0, 200); - - wxPanel* data_input_panel = new wxPanel(notebook, wxID_ANY, wxPoint(6, 6), wxDefaultSize); - - notebook->AddPage(main_information_panel, "Main information"); - notebook->AddPage(data_input_panel, "Anime list and settings"); - -} +#include "window.h" +#include "anime.h" +#include "information.h" +#include "ui_utils.h" +#include "string_utils.h" + +#include <QDialogButtonBox> + +void InformationDialog::OnOK() { + model->UpdateAnime(*anime); + QDialog::accept(); +} + +InformationDialog::InformationDialog(Anime& a, AnimeListWidgetModel* model, QWidget* parent) + : QDialog(parent) + { + this->model = model; + this->anime = &a; + setFixedSize(842, 613); + setWindowTitle(tr("Anime Information")); + setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + setObjectName("infodiag"); + QWidget* widget = new QWidget(this); + widget->resize(842-175, 530); + widget->move(175, 0); + widget->setStyleSheet(UiUtils::IsInDarkMode() ? "" : "background-color: white"); + widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + QPlainTextEdit* anime_title = new QPlainTextEdit(QString::fromWCharArray(anime->title.english.c_str()), widget); + anime_title->setReadOnly(true); + anime_title->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + anime_title->setWordWrapMode(QTextOption::NoWrap); + anime_title->setFrameShape(QFrame::NoFrame); + anime_title->resize(636, 28); + anime_title->move(0, 12); + anime_title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + anime_title->setStyleSheet("font-size: 16px; color: blue"); + QTabWidget* tabbed_widget = new QTabWidget(widget); + tabbed_widget->resize(636, 485); + tabbed_widget->move(0, 45); + tabbed_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + QWidget* main_information_widget = new QWidget(tabbed_widget); + UiUtils::CreateSelectableTextParagraph(main_information_widget, "Alternative titles", "-", QPoint(6, 6), QSize(636-18, 56)); + QString details_data(""); + QTextStream details_data_s(&details_data); + details_data_s << AnimeFormatToStringMap[anime->type].c_str() << "\n" + << anime->episodes << "\n" + << AnimeAiringToStringMap[anime->airing].c_str() << "\n" + << AnimeSeasonToStringMap[anime->season].c_str() << " " << anime->air_date.GetYear() << "\n" + << StringUtils::Implode(anime->genres, ", ").c_str() << "\n" + << anime->audience_score << "%\n"; + UiUtils::CreateTextParagraphWithLabels(main_information_widget, "Details", "Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:", details_data, QPoint(6, 62), QSize(636-18, 142)); + UiUtils::CreateSelectableTextParagraph(main_information_widget, "Synopsis", QString::fromWCharArray(anime->synopsis.c_str()), QPoint(6, 202), QSize(636-18, 253)); + tabbed_widget->addTab(main_information_widget, "Main information"); + QWidget* settings_widget = new QWidget(tabbed_widget); + + QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(button_box, &QDialogButtonBox::accepted, this, &OnOK); + connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + QVBoxLayout* buttons_layout = new QVBoxLayout(widget); + buttons_layout->addWidget(widget, 0, Qt::AlignTop); + buttons_layout->addWidget(button_box, 0, Qt::AlignBottom); + // this should probably be win32-only + setStyleSheet(UiUtils::IsInDarkMode() ? "" : "QDialog#infodiag{background-color: white;}"); + setLayout(buttons_layout); +} + +#include "moc_information.cpp"
--- a/src/filesystem.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/filesystem.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,25 +1,25 @@ -#ifdef _WIN32 -#include <shlobj.h> -#elif defined(APPLE) -#include <NSSearchPathForDirectoriesInDomains.h> -#endif -#include <filesystem> -#include <limits.h> -#include "config.h" -#include "filesystem.h" - -std::filesystem::path get_config_path(void) { - std::filesystem::path cfg_path; -#ifdef _WIN32 - char buf[PATH_MAX+1]; - if (SHGetFolderPathAndSubDir(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, CONFIG_DIR, buf) == S_OK) - cfg_path = std::filesystem::path(buf) / CONFIG_NAME; -#elif defined(MACOSX) - /* hope and pray that std::filesystem can handle tildes... */ - CFString string = (CFString)NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true); - cfg_path = std::filesystem::path(StringUtils::Utf8ToWstr(std::string(CFStringGetCStringPtr(string, UTF8)))); -#else // just assume POSIX - cfg_path = std::filesystem::path(getenv("HOME")) / ".config" / CONFIG_DIR / CONFIG_NAME; -#endif - return cfg_path; -} +#ifdef _WIN32 +#include <shlobj.h> +#elif defined(APPLE) +#include <NSSearchPathForDirectoriesInDomains.h> +#endif +#include <filesystem> +#include <limits.h> +#include "config.h" +#include "filesystem.h" + +std::filesystem::path get_config_path(void) { + std::filesystem::path cfg_path; +#ifdef _WIN32 + char buf[PATH_MAX+1]; + if (SHGetFolderPathAndSubDir(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, CONFIG_DIR, buf) == S_OK) + cfg_path = std::filesystem::path(buf) / CONFIG_NAME; +#elif defined(MACOSX) + /* hope and pray that std::filesystem can handle tildes... */ + CFString string = (CFString)NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true); + cfg_path = std::filesystem::path(StringUtils::Utf8ToWstr(std::string(CFStringGetCStringPtr(string, UTF8)))); +#else // just assume POSIX + cfg_path = std::filesystem::path(getenv("HOME")) / ".config" / CONFIG_DIR / CONFIG_NAME; +#endif + return cfg_path; +}
--- a/src/include/anilist.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/anilist.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,33 +1,22 @@ -#ifndef __anilist_h -#define __anilist_h -#include <curl/curl.h> -#include "anime.h" -class AniList { - public: - int Authorize(); - int GetUserId(std::string name); - int UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id); - - private: - static size_t CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata); - enum AnimeWatchingStatus ConvertWatchingStatusToEnum(std::string status); - enum AnimeAiringStatus ConvertAiringStatusToEnum(std::string status); - enum AnimeFormat ConvertFormatToEnum(std::string format); - enum AnimeSeason ConvertSeasonToEnum(std::string season); - std::string SendRequest(std::string data); - CURL* curl; - CURLcode res; -}; - -/* FIXME: at some point, we have to add a separate Date class (which IIRC - Kitsu actually does as well), because the standard library functions do - not support any null values. Internally, we could represent null or undefined - values as... -1?. Also, anything anime-related should probably be in an - Anime namespace. */ -#define ANILIST_DATE_IS_VALID(a) \ - (a["year"].is_number() && a["month"].is_number() && a["day"].is_number()) -#define ANILIST_DATE_TO_YMD(a) \ - std::chrono::year_month_day(std::chrono::year(a["year"].get<int>()), \ - std::chrono::month(a["month"].get<int>()), \ - std::chrono::day(a["day"].get<int>())) -#endif // __anilist_h +#ifndef __anilist_h +#define __anilist_h +#include <curl/curl.h> +#include "anime.h" +#include "json.h" +class AniList { + public: + int Authorize(); + int GetUserId(std::string name); + int UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id); + + private: + static size_t CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata); + enum AnimeWatchingStatus ConvertWatchingStatusToEnum(std::string status); + enum AnimeAiringStatus ConvertAiringStatusToEnum(std::string status); + enum AnimeFormat ConvertFormatToEnum(std::string format); + enum AnimeSeason ConvertSeasonToEnum(std::string season); + std::string SendRequest(std::string data); + CURL* curl; + CURLcode res; +}; +#endif // __anilist_h
--- a/src/include/anime.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/anime.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,168 +1,175 @@ -#ifndef __anime_h -#define __anime_h -#include <vector> -#include <chrono> -#include <map> -#include "window.h" - -enum AnimeWatchingStatus { - CURRENT, - PLANNING, - COMPLETED, - DROPPED, - PAUSED, - REPEATING -}; - -enum AnimeAiringStatus { - FINISHED, - RELEASING, - NOT_YET_RELEASED, - CANCELLED, - HIATUS -}; - -enum AnimeFormat { - TV, - TV_SHORT, - MOVIE, - SPECIAL, - OVA, - ONA, - MUSIC, - MANGA, - NOVEL, - ONE_SHOT -}; - -enum AnimeSeason { - WINTER, - SPRING, - SUMMER, - FALL -}; - -class Anime { - public: - Anime(); - Anime(const Anime& a); - /* List-specific data */ - enum AnimeWatchingStatus status; - int progress; - int score; - std::chrono::year_month_day started; - std::chrono::year_month_day completed; - std::wstring notes; - - /* Useful information */ - int id; - std::wstring title; - int episodes; - enum AnimeAiringStatus airing; - std::chrono::year_month_day air_date; - std::vector<std::string> genres; - std::vector<std::string> producers; - enum AnimeFormat type; - enum AnimeSeason season; - int audience_score; - std::wstring synopsis; - int duration; -}; - -/* This is a simple wrapper on a vector that provides - methods to make it easier to search the list. */ -class AnimeList { - public: - AnimeList(); - AnimeList(const AnimeList& l); - ~AnimeList(); - void Add(Anime& anime); - void Insert(size_t pos, Anime& anime); - void Delete(size_t index); - void Clear(); - std::vector<Anime>::iterator begin() noexcept; - std::vector<Anime>::iterator end() noexcept; - std::vector<Anime>::const_iterator cbegin() noexcept; - std::vector<Anime>::const_iterator cend() noexcept; - size_t Size() const; - Anime* AnimeById(int id); - bool AnimeInList(int id); - Anime& operator[](size_t index); - const Anime& operator[](size_t index) const; - std::wstring name; - - private: - std::vector<Anime> anime_list; - std::map<int, Anime*> anime_id_to_anime; -}; - -class AnimeListWidgetModel : public QAbstractListModel { - Q_OBJECT - public: - enum columns { - AL_TITLE, - AL_PROGRESS, - AL_SCORE, - AL_AVG_SCORE, - AL_TYPE, - AL_SEASON, - AL_STARTED, - AL_COMPLETED, - AL_UPDATED, - AL_NOTES, - - NB_COLUMNS - }; - - AnimeListWidgetModel(QWidget* parent, AnimeList* alist); - ~AnimeListWidgetModel() override = default; - //QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const; - int rowCount(const QModelIndex& parent = QModelIndex()) const; - int columnCount(const QModelIndex& parent = QModelIndex()) const; - QVariant data(const QModelIndex& index, int role) const; - QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const; - Anime* GetAnimeFromIndex(const QModelIndex& index); - void Update(); - - private: - //void AddAnime(AnimeList& list); - AnimeList& list; -}; - -class AnimeListWidget : public QTreeView { - Q_OBJECT - public: - AnimeListWidget(QWidget* parent, AnimeList* alist); - - private slots: - void DisplayColumnHeaderMenu(); - void DisplayListMenu(); - void ItemDoubleClicked(); - void SetColumnDefaults(); - int VisibleColumnsCount() const; - - private: - AnimeListWidgetModel* model = nullptr; -}; - -class AnimeListPage : public QTabWidget { - public: - AnimeListPage(QWidget* parent = nullptr); - void SyncAnimeList(); - void FreeAnimeList(); - int GetTotalAnimeAmount(); - int GetTotalEpisodeAmount(); - int GetTotalWatchedAmount(); - int GetTotalPlannedAmount(); - double GetAverageScore(); - double GetScoreDeviation(); - - private: - std::vector<AnimeList> anime_lists; -}; - -extern std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap; -extern std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap; -extern std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap; -extern std::map<enum AnimeAiringStatus, std::string> AnimeAiringToStringMap; +#ifndef __anime_h +#define __anime_h +#include <vector> +#include <map> +#include "date.h" +#include "window.h" + +enum AnimeWatchingStatus { + CURRENT, + PLANNING, + COMPLETED, + DROPPED, + PAUSED, + REPEATING +}; + +enum AnimeAiringStatus { + FINISHED, + RELEASING, + NOT_YET_RELEASED, + CANCELLED, + HIATUS +}; + +enum AnimeFormat { + TV, + TV_SHORT, + MOVIE, + SPECIAL, + OVA, + ONA, + MUSIC, + MANGA, + NOVEL, + ONE_SHOT +}; + +enum AnimeSeason { + UNKNOWN, + WINTER, + SPRING, + SUMMER, + FALL +}; + +class Anime { + public: + Anime(); + Anime(const Anime& a); + /* List-specific data */ + enum AnimeWatchingStatus status; + int progress; + int score; + Date started; + Date completed; + int updated; /* this should be 64-bit */ + std::wstring notes; + + /* Useful information */ + int id; + struct { + std::wstring romaji; + std::wstring english; + std::wstring native; + } title; + int episodes; + enum AnimeAiringStatus airing; + Date air_date; + std::vector<std::string> genres; + std::vector<std::string> producers; + enum AnimeFormat type; + enum AnimeSeason season; + int audience_score; + std::wstring synopsis; + int duration; +}; + +/* This is a simple wrapper on a vector that provides + methods to make it easier to search the list. */ +class AnimeList { + public: + AnimeList(); + AnimeList(const AnimeList& l); + ~AnimeList(); + void Add(Anime& anime); + void Insert(size_t pos, Anime& anime); + void Delete(size_t index); + void Clear(); + std::vector<Anime>::iterator begin() noexcept; + std::vector<Anime>::iterator end() noexcept; + std::vector<Anime>::const_iterator cbegin() noexcept; + std::vector<Anime>::const_iterator cend() noexcept; + size_t Size() const; + Anime* AnimeById(int id); + int GetAnimeIndex(Anime& anime) const; + bool AnimeInList(int id); + Anime& operator[](size_t index); + const Anime& operator[](size_t index) const; + std::wstring name; + + private: + std::vector<Anime> anime_list; + std::map<int, Anime*> anime_id_to_anime; +}; + +class AnimeListWidgetModel : public QAbstractListModel { + Q_OBJECT + public: + enum columns { + AL_TITLE, + AL_PROGRESS, + AL_SCORE, + AL_AVG_SCORE, + AL_TYPE, + AL_SEASON, + AL_STARTED, + AL_COMPLETED, + AL_UPDATED, + AL_NOTES, + + NB_COLUMNS + }; + + AnimeListWidgetModel(QWidget* parent, AnimeList* alist); + ~AnimeListWidgetModel() override = default; + //QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(const int section, const Qt::Orientation orientation, const int role) const; + Anime* GetAnimeFromIndex(const QModelIndex& index); + void UpdateAnime(Anime& anime); + + private: + //void AddAnime(AnimeList& list); + AnimeList& list; +}; + +class AnimeListWidget : public QTreeView { + Q_OBJECT + public: + AnimeListWidget(QWidget* parent, AnimeList* alist); + + private slots: + void DisplayColumnHeaderMenu(); + void DisplayListMenu(); + void ItemDoubleClicked(); + void SetColumnDefaults(); + int VisibleColumnsCount() const; + + private: + AnimeListWidgetModel* model = nullptr; +}; + +class AnimeListPage : public QTabWidget { + public: + AnimeListPage(QWidget* parent = nullptr); + void SyncAnimeList(); + void FreeAnimeList(); + int GetTotalAnimeAmount(); + int GetTotalEpisodeAmount(); + int GetTotalWatchedAmount(); + int GetTotalPlannedAmount(); + double GetAverageScore(); + double GetScoreDeviation(); + + private: + std::vector<AnimeList> anime_lists; +}; + +extern std::map<enum AnimeSeason, std::string> AnimeSeasonToStringMap; +extern std::map<enum AnimeFormat, std::string> AnimeFormatToStringMap; +extern std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap; +extern std::map<enum AnimeAiringStatus, std::string> AnimeAiringToStringMap; #endif // __anime_h \ No newline at end of file
--- a/src/include/config.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/config.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,34 +1,34 @@ -#ifndef __config_h -#define __config_h -/* This should be moved to anime_list.h, but unfortunately - #include-ing anime_list.h in this file causes a shitstorm for - whatever reason, so I'll just leave it here */ -enum AnimeListServices { - NONE, - ANILIST -}; - -/* todo: make this a class enum */ -enum Themes { - LIGHT, - DARK, - OS -}; - -class Config { - public: - int Load(); - int Save(); - - enum AnimeListServices service; - enum Themes theme; - struct { - std::string auth_token; - std::string username; - int user_id; - } anilist; -}; -#define CONFIG_DIR "weeaboo" -#define CONFIG_NAME "config.json" -#define MAX_LINE_LENGTH 256 -#endif // __config_h +#ifndef __config_h +#define __config_h +/* This should be moved to anime_list.h, but unfortunately + #include-ing anime_list.h in this file causes a shitstorm for + whatever reason, so I'll just leave it here */ +enum AnimeListServices { + NONE, + ANILIST +}; + +/* todo: make this a class enum */ +enum Themes { + LIGHT, + DARK, + OS +}; + +class Config { + public: + int Load(); + int Save(); + + enum AnimeListServices service; + enum Themes theme; + struct { + std::string auth_token; + std::string username; + int user_id; + } anilist; +}; +#define CONFIG_DIR "weeaboo" +#define CONFIG_NAME "config.json" +#define MAX_LINE_LENGTH 256 +#endif // __config_h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/include/date.h Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,23 @@ +#ifndef __date_h +#define __date_h +#include <cstdint> +#include <QDate> +class Date { + public: + Date(); + Date(int32_t y); + Date(int32_t y, int8_t m, int8_t d); + void SetYear(int32_t y); + void SetMonth(int8_t m); + void SetDay(int8_t d); + int32_t GetYear(); + int8_t GetMonth(); + int8_t GetDay(); + QDate GetAsQDate(); + + private: + int32_t year = -1; + int8_t month = -1; + int8_t day = -1; +}; +#endif // __date_h \ No newline at end of file
--- a/src/include/filesystem.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/filesystem.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,5 +1,5 @@ -#ifndef __filesystem_h -#define __filesystem_h -#include <filesystem> -std::filesystem::path get_config_path(void); +#ifndef __filesystem_h +#define __filesystem_h +#include <filesystem> +std::filesystem::path get_config_path(void); #endif // __filesystem_h \ No newline at end of file
--- a/src/include/information.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/information.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,11 +1,14 @@ -#ifndef __information_h -#define __information_h -class InformationDialog: public wxDialog { - public: - InformationDialog(wxWindow* parent, wxWindowID id, const wxString& title, - const Anime& anime, const wxPoint& pos = wxDefaultPosition, - long style = wxDEFAULT_DIALOG_STYLE); - private: - wxNotebook* dialogText; -}; +#ifndef __information_h +#define __information_h +#include "anime.h" +class InformationDialog: public QDialog { + Q_OBJECT + public: + InformationDialog(Anime& a, AnimeListWidgetModel* model, QWidget* parent = nullptr); + + private: + void OnOK(); + Anime* anime; + AnimeListWidgetModel* model; +}; #endif // __information_h \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/include/json.h Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,9 @@ +#include "../../dep/json/json.h" + +namespace JSON { + std::string GetString(nlohmann::json const& json, std::string const& key); + int GetInt(nlohmann::json const& json, std::string const& key); + int64_t GetInt64(nlohmann::json const& json, std::string const& key); + bool GetBoolean(nlohmann::json const& json, std::string const& key); + double GetDouble(nlohmann::json const& json, std::string const& key); +}
--- a/src/include/now_playing.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/now_playing.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,9 +1,9 @@ -#ifndef __now_playing_h -#define __now_playing_h - -class NowPlaying { - public: - NowPlaying(page_t* page, wxPanel* frame); -}; - -#endif // __now_playing_h +#ifndef __now_playing_h +#define __now_playing_h + +class NowPlaying { + public: + NowPlaying(page_t* page, wxPanel* frame); +}; + +#endif // __now_playing_h
--- a/src/include/statistics.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/statistics.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,40 +1,40 @@ -#ifndef __statistics_h -#define __statistics_h -class Statistics; - -class StatisticsTimer : public wxTimer { - public: - StatisticsTimer(Statistics* caller); - virtual void Notify(); - - private: - Statistics* statistics; -}; - -class Statistics { - public: - Statistics(page_t* page, wxPanel* frame); - void UpdateStatistics(); - - private: - std::string MinutesToDateString(int minutes); - - wxPanel* panel; - AnimeListPage* anime_list; - wxStaticText* anime_list_data; - - wxStaticText* score_distribution_title; - wxStaticText* score_distribution_labels; - //wxStaticText* score_distribution_graph; // how am I gonna do this - - /* we don't HAVE a local database (yet ;)) */ - //wxStaticText* local_database_title; - //wxStaticText* local_database_labels; - //wxStaticText* local_database_data; - - wxStaticText* application_title; - wxStaticText* application_labels; - wxStaticText* application_data; - StatisticsTimer* timer; -}; +#ifndef __statistics_h +#define __statistics_h +class Statistics; + +class StatisticsTimer : public wxTimer { + public: + StatisticsTimer(Statistics* caller); + virtual void Notify(); + + private: + Statistics* statistics; +}; + +class Statistics { + public: + Statistics(page_t* page, wxPanel* frame); + void UpdateStatistics(); + + private: + std::string MinutesToDateString(int minutes); + + wxPanel* panel; + AnimeListPage* anime_list; + wxStaticText* anime_list_data; + + wxStaticText* score_distribution_title; + wxStaticText* score_distribution_labels; + //wxStaticText* score_distribution_graph; // how am I gonna do this + + /* we don't HAVE a local database (yet ;)) */ + //wxStaticText* local_database_title; + //wxStaticText* local_database_labels; + //wxStaticText* local_database_data; + + wxStaticText* application_title; + wxStaticText* application_labels; + wxStaticText* application_data; + StatisticsTimer* timer; +}; #endif // __statistics_h \ No newline at end of file
--- a/src/include/string_utils.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/string_utils.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,33 +1,33 @@ -#ifndef __string_utils_h -#define __string_utils_h -#include <string> -#include <vector> -namespace StringUtils { - /* Implode function: takes a vector of strings and turns it - into a string, separated by delimiters. */ - std::string Implode(const std::vector<std::string>& vector, - const std::string& delimiter); - std::wstring Implode(const std::vector<std::wstring>& vector, - const std::wstring& delimiter); - - /* Conversion from UTF-8 to std::wstring and vice versa */ - std::string WstrToUtf8(const std::wstring& string); - std::wstring Utf8ToWstr(const std::string& string); - - /* Substring removal functions */ - std::string ReplaceAll(const std::string& string, - const std::string& find, - const std::string& replace); - std::wstring ReplaceAll(const std::wstring& string, - const std::wstring& find, - const std::wstring& replace); - std::string SanitizeLineEndings(const std::string& string); - std::wstring SanitizeLineEndings(const std::wstring& string); - std::wstring RemoveHtmlTags(const std::wstring& string); - std::string RemoveHtmlTags(const std::string& string); - - /* stupid HTML bullshit */ - std::string TextifySynopsis(const std::string& string); - std::wstring TextifySynopsis(const std::wstring& string); -}; +#ifndef __string_utils_h +#define __string_utils_h +#include <string> +#include <vector> +namespace StringUtils { + /* Implode function: takes a vector of strings and turns it + into a string, separated by delimiters. */ + std::string Implode(const std::vector<std::string>& vector, + const std::string& delimiter); + std::wstring Implode(const std::vector<std::wstring>& vector, + const std::wstring& delimiter); + + /* Conversion from UTF-8 to std::wstring and vice versa */ + std::string WstrToUtf8(const std::wstring& string); + std::wstring Utf8ToWstr(const std::string& string); + + /* Substring removal functions */ + std::string ReplaceAll(const std::string& string, + const std::string& find, + const std::string& replace); + std::wstring ReplaceAll(const std::wstring& string, + const std::wstring& find, + const std::wstring& replace); + std::string SanitizeLineEndings(const std::string& string); + std::wstring SanitizeLineEndings(const std::wstring& string); + std::wstring RemoveHtmlTags(const std::wstring& string); + std::string RemoveHtmlTags(const std::string& string); + + /* stupid HTML bullshit */ + std::string TextifySynopsis(const std::string& string); + std::wstring TextifySynopsis(const std::wstring& string); +}; #endif // __string_utils_h \ No newline at end of file
--- a/src/include/sys/osx/dark_theme.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/sys/osx/dark_theme.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,10 +1,10 @@ -#ifndef __sys__osx__dark_theme_h -#define __sys__osx__dark_theme_h -namespace osx { - bool DarkThemeAvailable(); - bool IsInDarkTheme(); - void SetToDarkTheme(); - void SetToLightTheme(); - void SetToAutoTheme(); -} +#ifndef __sys__osx__dark_theme_h +#define __sys__osx__dark_theme_h +namespace osx { + bool DarkThemeAvailable(); + bool IsInDarkTheme(); + void SetToDarkTheme(); + void SetToLightTheme(); + void SetToAutoTheme(); +} #endif // __sys__osx__dark_theme_h \ No newline at end of file
--- a/src/include/sys/win32/dark_theme.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/sys/win32/dark_theme.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,7 +1,7 @@ -#ifndef __sys__win32__dark_theme_h -#define __sys__win32__dark_theme_h -namespace win32 { - bool DarkThemeAvailable(); - bool IsInDarkTheme(); -} +#ifndef __sys__win32__dark_theme_h +#define __sys__win32__dark_theme_h +namespace win32 { + bool DarkThemeAvailable(); + bool IsInDarkTheme(); +} #endif // __sys__win32__dark_theme_h \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/include/time_utils.h Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,20 @@ +#ifndef __duration_h +#define __duration_h +#include <string> +#include <cstdint> +namespace Time { + class Duration { + public: + Duration(int64_t l); + int64_t InSeconds(); + int64_t InMinutes(); + int64_t InHours(); + int64_t InDays(); + std::string AsRelativeString(); + + private: + int64_t length; + }; + int64_t GetSystemTime(); +}; +#endif // __duration_h \ No newline at end of file
--- a/src/include/ui_utils.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/ui_utils.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,14 +1,16 @@ -#ifndef __ui_utils_h -#define __ui_utils_h -namespace UiUtils { - /* Creates a text paragraph with a title header. - Note: the `height` parameters for both of these functions is actually - the height of the data on its own. - Returns a pointer to the data wxStaticText element. */ - wxStaticText* CreateTextParagraph(wxWindow* parent, const wchar_t* title, const wchar_t* data, int width, int height, int x = 0, int y = 0); - - wxStaticText* CreateTextParagraphWithLabels(wxWindow* parent, const wchar_t* title, const wchar_t* label, const wchar_t* data, int width, int height, int x = 0, int y = 0); - wxTextCtrl* CreateSelectableTextParagraph(wxWindow* parent, const wchar_t* title, const wchar_t* data, int width, int height, int x = 0, int y = 0); - void CreateTextHeader(wxWindow* parent, const wchar_t* title, int width, int x = 0, int y = 0); -}; +#ifndef __ui_utils_h +#define __ui_utils_h +#include <QWidget> +#include <QString> +#include <QPoint> +#include <QSize> +#include <QDateTime> +namespace UiUtils { + bool IsInDarkMode(); + std::string GetLengthFromQDateTime(QDateTime stamp); + QPlainTextEdit* CreateTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size); + QPlainTextEdit* CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data, QPoint point, QSize size); + QPlainTextEdit* CreateSelectableTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size); + void CreateTextHeader(QWidget* parent, QString title, QPoint point, QSize size); +}; #endif // __ui_utils_h \ No newline at end of file
--- a/src/include/window.h Tue Aug 08 19:49:15 2023 -0400 +++ b/src/include/window.h Sat Aug 12 03:16:26 2023 -0400 @@ -1,41 +1,45 @@ -#ifndef __window_h -# define __window_h -/* FIXME: include these in specific .cpp files */ -# include <QApplication> -# include <QMainWindow> -# include <QToolBar> -# include <QMenuBar> -# include <QWidget> -# include <QTreeView> -# include <QMessageBox> -# include <QDesktopServices> -# include <QUrl> -# include <QInputDialog> -# include <QDate> -# include <QHeaderView> -# include <QShortcut> -# include <QFile> -# include <QTextStream> -# include <QCloseEvent> -# include "config.h" -//# include "statistics.h" -//# include "now_playing.h" - -class MainWindow : public QMainWindow { - public: - MainWindow(QWidget* parent = nullptr); - void SetActivePage(QWidget* page); - void SetStyleSheet(enum Themes theme); - void ThemeChanged(); - void closeEvent(QCloseEvent* event); - - private: - QWidget* anime_list_page; -}; - -struct Session { - Config config; -}; - -extern Session session; -#endif // __window_h +#ifndef __window_h +# define __window_h +/* FIXME: include these in specific .cpp files */ +# include <QApplication> +# include <QMainWindow> +# include <QToolBar> +# include <QMenuBar> +# include <QWidget> +# include <QTreeView> +# include <QMessageBox> +# include <QDesktopServices> +# include <QUrl> +# include <QInputDialog> +# include <QDate> +# include <QHeaderView> +# include <QShortcut> +# include <QFile> +# include <QTextStream> +# include <QCloseEvent> +# include <QPlainTextEdit> +# include <QLabel> +# include <QHBoxLayout> +# include <QTextStream> +# include "config.h" +//# include "statistics.h" +//# include "now_playing.h" + +class MainWindow : public QMainWindow { + public: + MainWindow(QWidget* parent = nullptr); + void SetActivePage(QWidget* page); + void SetStyleSheet(enum Themes theme); + void ThemeChanged(); + void closeEvent(QCloseEvent* event); + + private: + QWidget* anime_list_page; +}; + +struct Session { + Config config; +}; + +extern Session session; +#endif // __window_h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/json.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,34 @@ +#include "json.h" + +namespace JSON { + +std::string GetString(nlohmann::json const& json, std::string const& key) { + auto item = json.find(key); + if (item != json.end() && item->is_string()) + return item->get<std::string>(); + else return ""; +} + +int GetInt(nlohmann::json const& json, std::string const& key) { + auto item = json.find(key); + if (item != json.end() && item->is_number()) + return item->get<int>(); + else return 0; +} + +bool GetBoolean(nlohmann::json const& json, std::string const& key) { + auto item = json.find(key); + if (item != json.end() && item->is_boolean()) + return item->get<bool>(); + else return false; +} + +double GetDouble(nlohmann::json const& json, std::string const& key) { + auto item = json.find(key); + if (item != json.end() && item->is_number()) + return item->get<double>(); + else return 0; +} + +} +
--- a/src/main.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/main.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,205 +1,205 @@ -#include "window.h" -#include "config.h" -#include "anime.h" -#if APPLE -#include "sys/osx/dark_theme.h" -#elif WIN32 -#include "sys/win32/dark_theme.h" -#endif - -Session session = { - .config = Config() -}; - -/* note that this code was originally created for use in - wxWidgets, but I thought the API was a little meh, so - I switched to Qt. */ - -MainWindow::MainWindow(QWidget* parent) : - QMainWindow(parent) { - /* Menu Bar */ - QAction* action; - QMenuBar* menubar = new QMenuBar(parent); - QMenu* menu = menubar->addMenu("&File"); - QMenu* submenu = menu->addMenu("&Library folders"); - action = new QAction("&Add new folder..."); - submenu->addAction(action); - action = new QAction("&Scan available episodes"); - menu->addAction(action); - - menu->addSeparator(); - - action = menu->addAction("Play &next episode"); - action = menu->addAction("Play &random episode"); - menu->addSeparator(); - action = menu->addAction("E&xit", qApp, &QApplication::quit); - - menu = menubar->addMenu("&Services"); - action = new QAction("Synchronize &list"); - - menu->addSeparator(); - - submenu = menu->addMenu("&AniList"); - action = menu->addAction("Go to my &profile"); - action = menu->addAction("Go to my &stats"); - - submenu = menu->addMenu("&Kitsu"); - action = submenu->addAction("Go to my &feed"); - action = submenu->addAction("Go to my &library"); - action = submenu->addAction("Go to my &profile"); - - submenu = menu->addMenu("&MyAnimeList"); - action = submenu->addAction("Go to my p&anel"); - action = submenu->addAction("Go to my &profile"); - action = submenu->addAction("Go to my &history"); - - menu = menubar->addMenu("&Tools"); - submenu = menu->addMenu("&Export anime list"); - action = submenu->addAction("Export as &Markdown..."); - action = submenu->addAction("Export as MyAnimeList &XML..."); - - menu->addSeparator(); - - action = menu->addAction("Enable anime &recognition"); - action->setCheckable(true); - action = menu->addAction("Enable auto &sharing"); - action->setCheckable(true); - action = menu->addAction("Enable &auto synchronization"); - action->setCheckable(true); - - menu->addSeparator(); - - action = menu->addAction("&Settings"); - - setMenuBar(menubar); - - /* Side toolbar */ - QToolBar* toolbar = new QToolBar(parent); - QActionGroup* tb_action_group = new QActionGroup(toolbar); - - action = toolbar->addAction("Now Playing"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - - toolbar->addSeparator(); - - action = toolbar->addAction("Anime List", [this]() { - setCentralWidget(anime_list_page); - }); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - action->setChecked(true); - anime_list_page = new AnimeListPage(parent); - SetActivePage(anime_list_page); - action = toolbar->addAction("History"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - action = toolbar->addAction("Statistics"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - - toolbar->addSeparator(); - - action = toolbar->addAction("Search"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - action = toolbar->addAction("Seasons"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - action = toolbar->addAction("Torrents"); - action->setActionGroup(tb_action_group); - action->setCheckable(true); - - toolbar->setMovable(false); - toolbar->setFloatable(false); - - addToolBar(Qt::LeftToolBarArea, toolbar); - - ThemeChanged(); -} - -void MainWindow::SetStyleSheet(enum Themes theme) { - switch (theme) { - case DARK: { - QFile f(":qdarkstyle/dark/darkstyle.qss"); - if (!f.exists()) - return; // fail - f.open(QFile::ReadOnly | QFile::Text); - QTextStream ts(&f); - setStyleSheet(ts.readAll()); - break; - } - default: - setStyleSheet(""); - break; - } -} - -void MainWindow::ThemeChanged() { - switch (session.config.theme) { - case LIGHT: { -#if APPLE - if (osx::DarkThemeAvailable()) - osx::SetToLightTheme(); - else - SetStyleSheet(LIGHT); -#else - SetStyleSheet(LIGHT); -#endif - break; - } - case DARK: { -#if APPLE - if (osx::DarkThemeAvailable()) - osx::SetToDarkTheme(); - else - SetStyleSheet(DARK); -#else - SetStyleSheet(DARK); -#endif - break; - } - case OS: { -#if APPLE - if (osx::DarkThemeAvailable()) - osx::SetToAutoTheme(); - else - SetStyleSheet(LIGHT); -#elif defined(WIN32) - if (win32::DarkThemeAvailable()) { - if (win32::IsInDarkTheme()) { - SetStyleSheet(DARK); - } else { - SetStyleSheet(LIGHT); - } - } -#else - SetStyleSheet(LIGHT); -#endif - break; - } - } -} - -void MainWindow::SetActivePage(QWidget* page) { - this->setCentralWidget(page); -} - -void MainWindow::closeEvent(QCloseEvent* event) { - session.config.Save(); - event->accept(); -} - -int main(int argc, char** argv) { - QApplication app(argc, argv); - - session.config.Load(); - - MainWindow window; - - window.resize(941, 750); - window.setWindowTitle("Weeaboo"); - window.show(); - - return app.exec(); -} +#include "window.h" +#include "config.h" +#include "anime.h" +#if APPLE +#include "sys/osx/dark_theme.h" +#elif WIN32 +#include "sys/win32/dark_theme.h" +#endif + +Session session = { + .config = Config() +}; + +/* note that this code was originally created for use in + wxWidgets, but I thought the API was a little meh, so + I switched to Qt. */ + +MainWindow::MainWindow(QWidget* parent) : + QMainWindow(parent) { + /* Menu Bar */ + QAction* action; + QMenuBar* menubar = new QMenuBar(parent); + QMenu* menu = menubar->addMenu("&File"); + QMenu* submenu = menu->addMenu("&Library folders"); + action = new QAction("&Add new folder..."); + submenu->addAction(action); + action = new QAction("&Scan available episodes"); + menu->addAction(action); + + menu->addSeparator(); + + action = menu->addAction("Play &next episode"); + action = menu->addAction("Play &random episode"); + menu->addSeparator(); + action = menu->addAction("E&xit", qApp, &QApplication::quit); + + menu = menubar->addMenu("&Services"); + action = new QAction("Synchronize &list"); + + menu->addSeparator(); + + submenu = menu->addMenu("&AniList"); + action = menu->addAction("Go to my &profile"); + action = menu->addAction("Go to my &stats"); + + submenu = menu->addMenu("&Kitsu"); + action = submenu->addAction("Go to my &feed"); + action = submenu->addAction("Go to my &library"); + action = submenu->addAction("Go to my &profile"); + + submenu = menu->addMenu("&MyAnimeList"); + action = submenu->addAction("Go to my p&anel"); + action = submenu->addAction("Go to my &profile"); + action = submenu->addAction("Go to my &history"); + + menu = menubar->addMenu("&Tools"); + submenu = menu->addMenu("&Export anime list"); + action = submenu->addAction("Export as &Markdown..."); + action = submenu->addAction("Export as MyAnimeList &XML..."); + + menu->addSeparator(); + + action = menu->addAction("Enable anime &recognition"); + action->setCheckable(true); + action = menu->addAction("Enable auto &sharing"); + action->setCheckable(true); + action = menu->addAction("Enable &auto synchronization"); + action->setCheckable(true); + + menu->addSeparator(); + + action = menu->addAction("&Settings"); + + setMenuBar(menubar); + + /* Side toolbar */ + QToolBar* toolbar = new QToolBar(parent); + QActionGroup* tb_action_group = new QActionGroup(toolbar); + + action = toolbar->addAction("Now Playing"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + + toolbar->addSeparator(); + + action = toolbar->addAction("Anime List", [this]() { + setCentralWidget(anime_list_page); + }); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + action->setChecked(true); + anime_list_page = new AnimeListPage(parent); + SetActivePage(anime_list_page); + action = toolbar->addAction("History"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + action = toolbar->addAction("Statistics"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + + toolbar->addSeparator(); + + action = toolbar->addAction("Search"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + action = toolbar->addAction("Seasons"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + action = toolbar->addAction("Torrents"); + action->setActionGroup(tb_action_group); + action->setCheckable(true); + + toolbar->setMovable(false); + toolbar->setFloatable(false); + + addToolBar(Qt::LeftToolBarArea, toolbar); + + ThemeChanged(); +} + +void MainWindow::SetStyleSheet(enum Themes theme) { + switch (theme) { + case DARK: { + QFile f(":qdarkstyle/dark/darkstyle.qss"); + if (!f.exists()) + return; // fail + f.open(QFile::ReadOnly | QFile::Text); + QTextStream ts(&f); + setStyleSheet(ts.readAll()); + break; + } + default: + setStyleSheet(""); + break; + } +} + +void MainWindow::ThemeChanged() { + switch (session.config.theme) { + case LIGHT: { +#if APPLE + if (osx::DarkThemeAvailable()) + osx::SetToLightTheme(); + else + SetStyleSheet(LIGHT); +#else + SetStyleSheet(LIGHT); +#endif + break; + } + case DARK: { +#if APPLE + if (osx::DarkThemeAvailable()) + osx::SetToDarkTheme(); + else + SetStyleSheet(DARK); +#else + SetStyleSheet(DARK); +#endif + break; + } + case OS: { +#if APPLE + if (osx::DarkThemeAvailable()) + osx::SetToAutoTheme(); + else + SetStyleSheet(LIGHT); +#elif defined(WIN32) + if (win32::DarkThemeAvailable()) { + if (win32::IsInDarkTheme()) { + SetStyleSheet(DARK); + } else { + SetStyleSheet(LIGHT); + } + } +#else + SetStyleSheet(LIGHT); +#endif + break; + } + } +} + +void MainWindow::SetActivePage(QWidget* page) { + this->setCentralWidget(page); +} + +void MainWindow::closeEvent(QCloseEvent* event) { + session.config.Save(); + event->accept(); +} + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + session.config.Load(); + + MainWindow window; + + window.resize(941, 750); + window.setWindowTitle("Weeaboo"); + window.show(); + + return app.exec(); +}
--- a/src/pages/now_playing.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/pages/now_playing.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,6 +1,6 @@ -#include "window.h" - -NowPlaying::NowPlaying(page_t* page, wxPanel* frame) { - page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600)); - page->panel->Show(false); -} +#include "window.h" + +NowPlaying::NowPlaying(page_t* page, wxPanel* frame) { + page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600)); + page->panel->Show(false); +}
--- a/src/pages/statistics.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/pages/statistics.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,63 +1,63 @@ -#include "window.h" -#include "ui_utils.h" -#include <sstream> - -StatisticsTimer::StatisticsTimer(Statistics* caller) { - statistics = caller; -} - -void StatisticsTimer::Notify() { - if (status.current_page == PAGE_STATISTICS) - statistics->UpdateStatistics(); -} - -Statistics::Statistics(page_t* page, wxPanel* frame) { - page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600)); - page->panel->Show(false); - panel = new wxPanel(page->panel, wxID_ANY, wxPoint(12, 12), wxSize(376, 576)); - anime_list = ((WeeabooFrame*)frame->GetParent())->GetAnimeList(); - - /* FIXME: this should be moved to a separate function, it's also used in information.cpp */ - // wxWindow* parent, const char* title, const char* label, const char* data, int width, int height, int x = 0, int y = 0, int selectable = 0 - anime_list_data = UiUtils::CreateTextParagraphWithLabels(panel, L"Anime list", L"Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", L"", 376, 94); - - UpdateStatistics(); // load in statistics as soon as possible - timer = new StatisticsTimer(this); - timer->Start(1000); // update statistics every second -} - -std::string Statistics::MinutesToDateString(int minutes) { - /* NOTE: these duration_casts may not be needed... */ - std::chrono::duration<int, std::ratio<60>> int_total_mins(minutes); - auto int_years = std::chrono::duration_cast<std::chrono::years>(int_total_mins); - auto int_months = std::chrono::duration_cast<std::chrono::months>(int_total_mins-int_years); - auto int_days = std::chrono::duration_cast<std::chrono::days>(int_total_mins-int_years-int_months); - auto int_hours = std::chrono::duration_cast<std::chrono::hours>(int_total_mins-int_years-int_months-int_days); - auto int_minutes = std::chrono::duration_cast<std::chrono::minutes>(int_total_mins-int_years-int_months-int_days-int_hours); - std::ostringstream return_stream; - if (int_years.count() > 0) { - return_stream << int_years.count() << " years "; - } - if (int_months.count() > 0) { - return_stream << int_months.count() << " months "; - } - if (int_days.count() > 0) { - return_stream << int_days.count() << " days "; - } - if (int_hours.count() > 0) { - return_stream << int_hours.count() << " hours "; - } - return_stream << int_minutes.count() << " minutes"; // return minutes anyway - return return_stream.str(); -} - -void Statistics::UpdateStatistics() { - wxString string = ""; - string << anime_list->GetTotalAnimeAmount() << '\n'; - string << anime_list->GetTotalEpisodeAmount() << '\n'; - string << MinutesToDateString(anime_list->GetTotalWatchedAmount()) << '\n'; - string << MinutesToDateString(anime_list->GetTotalPlannedAmount()) << '\n'; - string << anime_list->GetAverageScore() << '\n'; - string << anime_list->GetScoreDeviation() << '\n'; - anime_list_data->SetLabel(string); -} +#include "window.h" +#include "ui_utils.h" +#include <sstream> + +StatisticsTimer::StatisticsTimer(Statistics* caller) { + statistics = caller; +} + +void StatisticsTimer::Notify() { + if (status.current_page == PAGE_STATISTICS) + statistics->UpdateStatistics(); +} + +Statistics::Statistics(page_t* page, wxPanel* frame) { + page->panel = new wxPanel(frame, wxID_ANY, wxPoint(0, 0), wxSize(400, 600)); + page->panel->Show(false); + panel = new wxPanel(page->panel, wxID_ANY, wxPoint(12, 12), wxSize(376, 576)); + anime_list = ((WeeabooFrame*)frame->GetParent())->GetAnimeList(); + + /* FIXME: this should be moved to a separate function, it's also used in information.cpp */ + // wxWindow* parent, const char* title, const char* label, const char* data, int width, int height, int x = 0, int y = 0, int selectable = 0 + anime_list_data = UiUtils::CreateTextParagraphWithLabels(panel, L"Anime list", L"Anime count:\nEpisode count:\nTime spent watching:\nTime to complete:\nAverage score:\nScore deviation:", L"", 376, 94); + + UpdateStatistics(); // load in statistics as soon as possible + timer = new StatisticsTimer(this); + timer->Start(1000); // update statistics every second +} + +std::string Statistics::MinutesToDateString(int minutes) { + /* NOTE: these duration_casts may not be needed... */ + std::chrono::duration<int, std::ratio<60>> int_total_mins(minutes); + auto int_years = std::chrono::duration_cast<std::chrono::years>(int_total_mins); + auto int_months = std::chrono::duration_cast<std::chrono::months>(int_total_mins-int_years); + auto int_days = std::chrono::duration_cast<std::chrono::days>(int_total_mins-int_years-int_months); + auto int_hours = std::chrono::duration_cast<std::chrono::hours>(int_total_mins-int_years-int_months-int_days); + auto int_minutes = std::chrono::duration_cast<std::chrono::minutes>(int_total_mins-int_years-int_months-int_days-int_hours); + std::ostringstream return_stream; + if (int_years.count() > 0) { + return_stream << int_years.count() << " years "; + } + if (int_months.count() > 0) { + return_stream << int_months.count() << " months "; + } + if (int_days.count() > 0) { + return_stream << int_days.count() << " days "; + } + if (int_hours.count() > 0) { + return_stream << int_hours.count() << " hours "; + } + return_stream << int_minutes.count() << " minutes"; // return minutes anyway + return return_stream.str(); +} + +void Statistics::UpdateStatistics() { + wxString string = ""; + string << anime_list->GetTotalAnimeAmount() << '\n'; + string << anime_list->GetTotalEpisodeAmount() << '\n'; + string << MinutesToDateString(anime_list->GetTotalWatchedAmount()) << '\n'; + string << MinutesToDateString(anime_list->GetTotalPlannedAmount()) << '\n'; + string << anime_list->GetAverageScore() << '\n'; + string << anime_list->GetScoreDeviation() << '\n'; + anime_list_data->SetLabel(string); +}
--- a/src/string_utils.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/string_utils.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,131 +1,131 @@ -/** - * string_utils.cpp: Useful functions for manipulating strings - * - * Every function in here *should* have a working wstring equivalent. -**/ -#include <vector> -#include <string> -#include <codecvt> -#include <locale> -#include "string_utils.h" - -/* It's actually pretty insane how the standard library still doesn't - have a function for this. Look at how simple this is. */ -std::string StringUtils::Implode(const std::vector<std::string>& vector, - const std::string& delimiter) { - std::string out = ""; - for (int i = 0; i < vector.size(); i++) { - out.append(vector.at(i)); - if (i < vector.size()-1) - out.append(delimiter); - } - return out; -} - -std::wstring StringUtils::Implode(const std::vector<std::wstring>& vector, - const std::wstring& delimiter) { - std::wstring out = L""; - for (int i = 0; i < vector.size(); i++) { - out.append(vector.at(i)); - if (i < vector.size()-1) - out.append(delimiter); - } - return out; -} - -std::string StringUtils::WstrToUtf8(const std::wstring& string) { - std::wstring_convert<std::codecvt_utf8<wchar_t>> convert; - return convert.to_bytes(string); -} - -std::wstring StringUtils::Utf8ToWstr(const std::string& string) { - std::wstring_convert<std::codecvt_utf8<wchar_t>> convert; - return convert.from_bytes(string); -} - -std::string StringUtils::ReplaceAll(const std::string& string, - const std::string& find, - const std::string& replace) { - std::string result; - size_t pos, find_len = find.size(), from = 0; - while ((pos=string.find(find,from)) != std::string::npos) { - result.append(string, from, pos - from); - result.append(replace); - from = pos + find_len; - } - result.append(string, from, std::string::npos); - return result; -} - -std::wstring StringUtils::ReplaceAll(const std::wstring& string, - const std::wstring& find, - const std::wstring& replace) { - std::wstring result; - size_t pos, find_len = find.size(), from = 0; - while ((pos=string.find(find,from)) != std::wstring::npos) { - result.append(string, from, pos - from); - result.append(replace); - from = pos + find_len; - } - result.append(string, from, std::wstring::npos); - return result; -} - -/* this function probably fucks your RAM but whatevs */ -std::string StringUtils::SanitizeLineEndings(const std::string& string) { - std::string result(string); - result = ReplaceAll(result, "\r\n", "\n"); - result = ReplaceAll(result, "<br>", "\n"); - result = ReplaceAll(result, "\n\n\n", "\n\n"); - return result; -} - -std::wstring StringUtils::SanitizeLineEndings(const std::wstring& string) { - std::wstring result(string); - result = ReplaceAll(result, L"\r\n", L"\n"); - result = ReplaceAll(result, L"<br>", L"\n"); - result = ReplaceAll(result, L"\n\n\n", L"\n\n"); - return result; -} - -std::string StringUtils::RemoveHtmlTags(const std::string& string) { - std::string html(string); - while (html.find("<") != std::string::npos) - { - auto startpos = html.find("<"); - auto endpos = html.find(">") + 1; - - if (endpos != std::string::npos) - { - html.erase(startpos, endpos - startpos); - } - } - return html; -} - -std::wstring StringUtils::RemoveHtmlTags(const std::wstring& string) { - std::wstring html(string); - while (html.find(L"<") != std::wstring::npos) - { - auto startpos = html.find(L"<"); - auto endpos = html.find(L">") + 1; - - if (endpos != std::wstring::npos) - { - html.erase(startpos, endpos - startpos); - } - } - return html; -} - -std::string StringUtils::TextifySynopsis(const std::string& string) { - std::string result = SanitizeLineEndings(string); - result = RemoveHtmlTags(string); - return result; -} - -std::wstring StringUtils::TextifySynopsis(const std::wstring& string) { - std::wstring result = SanitizeLineEndings(string); - result = RemoveHtmlTags(string); - return result; -} +/** + * string_utils.cpp: Useful functions for manipulating strings + * + * Every function in here *should* have a working wstring equivalent. +**/ +#include <vector> +#include <string> +#include <codecvt> +#include <locale> +#include "string_utils.h" + +/* It's actually pretty insane how the standard library still doesn't + have a function for this. Look at how simple this is. */ +std::string StringUtils::Implode(const std::vector<std::string>& vector, + const std::string& delimiter) { + std::string out = ""; + for (int i = 0; i < vector.size(); i++) { + out.append(vector.at(i)); + if (i < vector.size()-1) + out.append(delimiter); + } + return out; +} + +std::wstring StringUtils::Implode(const std::vector<std::wstring>& vector, + const std::wstring& delimiter) { + std::wstring out = L""; + for (int i = 0; i < vector.size(); i++) { + out.append(vector.at(i)); + if (i < vector.size()-1) + out.append(delimiter); + } + return out; +} + +std::string StringUtils::WstrToUtf8(const std::wstring& string) { + std::wstring_convert<std::codecvt_utf8<wchar_t>> convert; + return convert.to_bytes(string); +} + +std::wstring StringUtils::Utf8ToWstr(const std::string& string) { + std::wstring_convert<std::codecvt_utf8<wchar_t>> convert; + return convert.from_bytes(string); +} + +std::string StringUtils::ReplaceAll(const std::string& string, + const std::string& find, + const std::string& replace) { + std::string result; + size_t pos, find_len = find.size(), from = 0; + while ((pos=string.find(find,from)) != std::string::npos) { + result.append(string, from, pos - from); + result.append(replace); + from = pos + find_len; + } + result.append(string, from, std::string::npos); + return result; +} + +std::wstring StringUtils::ReplaceAll(const std::wstring& string, + const std::wstring& find, + const std::wstring& replace) { + std::wstring result; + size_t pos, find_len = find.size(), from = 0; + while ((pos=string.find(find,from)) != std::wstring::npos) { + result.append(string, from, pos - from); + result.append(replace); + from = pos + find_len; + } + result.append(string, from, std::wstring::npos); + return result; +} + +/* this function probably fucks your RAM but whatevs */ +std::string StringUtils::SanitizeLineEndings(const std::string& string) { + std::string result(string); + result = ReplaceAll(result, "\r\n", "\n"); + result = ReplaceAll(result, "<br>", "\n"); + result = ReplaceAll(result, "\n\n\n", "\n\n"); + return result; +} + +std::wstring StringUtils::SanitizeLineEndings(const std::wstring& string) { + std::wstring result(string); + result = ReplaceAll(result, L"\r\n", L"\n"); + result = ReplaceAll(result, L"<br>", L"\n"); + result = ReplaceAll(result, L"\n\n\n", L"\n\n"); + return result; +} + +std::string StringUtils::RemoveHtmlTags(const std::string& string) { + std::string html(string); + while (html.find("<") != std::string::npos) + { + auto startpos = html.find("<"); + auto endpos = html.find(">") + 1; + + if (endpos != std::string::npos) + { + html.erase(startpos, endpos - startpos); + } + } + return html; +} + +std::wstring StringUtils::RemoveHtmlTags(const std::wstring& string) { + std::wstring html(string); + while (html.find(L"<") != std::wstring::npos) + { + auto startpos = html.find(L"<"); + auto endpos = html.find(L">") + 1; + + if (endpos != std::wstring::npos) + { + html.erase(startpos, endpos - startpos); + } + } + return html; +} + +std::string StringUtils::TextifySynopsis(const std::string& string) { + std::string result = SanitizeLineEndings(string); + result = RemoveHtmlTags(string); + return result; +} + +std::wstring StringUtils::TextifySynopsis(const std::wstring& string) { + std::wstring result = SanitizeLineEndings(string); + result = RemoveHtmlTags(string); + return result; +}
--- a/src/sys/osx/dark_theme.mm Tue Aug 08 19:49:15 2023 -0400 +++ b/src/sys/osx/dark_theme.mm Sat Aug 12 03:16:26 2023 -0400 @@ -1,44 +1,44 @@ -#include "sys/osx/dark_theme.h" -#import <Cocoa/Cocoa.h> - -bool osx::DarkThemeAvailable() -{ - return (__builtin_available(macOS 10.14, *)) ? true : false; -} - -bool osx::IsInDarkTheme() -{ - if (__builtin_available(macOS 10.14, *)) - { - auto appearance = [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames: - @[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]]; - return [appearance isEqualToString:NSAppearanceNameDarkAqua]; - } - return false; -} - -void osx::SetToDarkTheme() -{ - // https://stackoverflow.com/questions/55925862/how-can-i-set-my-os-x-application-theme-in-code - if (__builtin_available(macOS 10.14, *)) - { - [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]]; - } -} - -void osx::SetToLightTheme() -{ - // https://stackoverflow.com/questions/55925862/how-can-i-set-my-os-x-application-theme-in-code - if (__builtin_available(macOS 10.14, *)) - { - [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]]; - } -} - -void osx::SetToAutoTheme() -{ - if (__builtin_available(macOS 10.14, *)) - { - [NSApp setAppearance:nil]; - } -} +#include "sys/osx/dark_theme.h" +#import <Cocoa/Cocoa.h> + +bool osx::DarkThemeAvailable() +{ + return (__builtin_available(macOS 10.14, *)) ? true : false; +} + +bool osx::IsInDarkTheme() +{ + if (__builtin_available(macOS 10.14, *)) + { + auto appearance = [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames: + @[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]]; + return [appearance isEqualToString:NSAppearanceNameDarkAqua]; + } + return false; +} + +void osx::SetToDarkTheme() +{ + // https://stackoverflow.com/questions/55925862/how-can-i-set-my-os-x-application-theme-in-code + if (__builtin_available(macOS 10.14, *)) + { + [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]]; + } +} + +void osx::SetToLightTheme() +{ + // https://stackoverflow.com/questions/55925862/how-can-i-set-my-os-x-application-theme-in-code + if (__builtin_available(macOS 10.14, *)) + { + [NSApp setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameAqua]]; + } +} + +void osx::SetToAutoTheme() +{ + if (__builtin_available(macOS 10.14, *)) + { + [NSApp setAppearance:nil]; + } +}
--- a/src/sys/win32/dark_theme.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/sys/win32/dark_theme.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,26 +1,26 @@ -#include <QSettings> -#include <QOperatingSystemVersion> -#include "sys/win32/dark_theme.h" -bool win32::DarkThemeAvailable() -{ - // dark mode supported Windows 10 1809 10.0.17763 onward - // https://stackoverflow.com/questions/53501268/win10-dark-theme-how-to-use-in-winapi - if ( QOperatingSystemVersion::current().majorVersion() == 10 ) - { - return QOperatingSystemVersion::current().microVersion() >= 17763; - } - else if ( QOperatingSystemVersion::current().majorVersion() > 10 ) - { - return true; - } - else - { - return false; - } -} - -bool win32::IsInDarkTheme() -{ - QSettings settings( "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat ); - return settings.value( "AppsUseLightTheme", 1 ).toInt() == 0; -} +#include <QSettings> +#include <QOperatingSystemVersion> +#include "sys/win32/dark_theme.h" +bool win32::DarkThemeAvailable() +{ + // dark mode supported Windows 10 1809 10.0.17763 onward + // https://stackoverflow.com/questions/53501268/win10-dark-theme-how-to-use-in-winapi + if ( QOperatingSystemVersion::current().majorVersion() == 10 ) + { + return QOperatingSystemVersion::current().microVersion() >= 17763; + } + else if ( QOperatingSystemVersion::current().majorVersion() > 10 ) + { + return true; + } + else + { + return false; + } +} + +bool win32::IsInDarkTheme() +{ + QSettings settings( "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat ); + return settings.value( "AppsUseLightTheme", 1 ).toInt() == 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/time.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -0,0 +1,64 @@ +#include "time_utils.h" +#include <string> +#include <cstdint> +#include <cmath> +#include <ctime> +#include <cassert> + +namespace Time { + +Duration::Duration(int64_t l) { + length = l; +} + +std::string Duration::AsRelativeString() { + std::string result; + + auto get = [](int64_t val, const std::string& s, const std::string& p) { + return std::to_string(val) + " " + (val == 1 ? s : p); + }; + + if (InSeconds() < 60) + result = get(InSeconds(), "second", "seconds"); + else if (InMinutes() < 60) + result = get(InMinutes(), "minute", "minutes"); + else if (InHours() < 24) + result = get(InHours(), "hour", "hours"); + else if (InDays() < 28) + result = get(InDays(), "day", "days"); + else if (InDays() < 365) + result = get(InDays()/30, "month", "months"); + else + result = get(InDays()/365, "year", "years"); + + if (length < 0) + result = "In " + result; + else + result += " ago"; + + return result; +} + +int64_t Duration::InSeconds() { + return length; +} + +int64_t Duration::InMinutes() { + return std::llround((double)length / 60.0); +} + +int64_t Duration::InHours() { + return std::llround((double)length / 3600.0); +} + +int64_t Duration::InDays() { + return std::llround((double)length / 86400.0); +} + +int64_t GetSystemTime() { + assert(sizeof(int64_t) >= sizeof(time_t)); + time_t t = std::time(nullptr); + return *reinterpret_cast<int64_t*>(&t); +} + +} \ No newline at end of file
--- a/src/ui_utils.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/ui_utils.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,32 +1,88 @@ -#include "window.h" -#include "ui_utils.h" - -void UiUtils::CreateTextHeader(wxWindow* parent, const wchar_t* title, int width, int x, int y) { - wxStaticText* static_text_title = new wxStaticText(parent, wxID_ANY, title, wxPoint(x, y), wxSize(width, 15), wxALIGN_LEFT); - wxFont font = static_text_title->GetFont(); - font.SetWeight(wxFONTWEIGHT_BOLD); - static_text_title->SetFont(font); - //wxStaticLine* line = new wxStaticLine(parent, wxID_ANY, wxPoint(x,y+15), wxSize(width, 2), wxLI_HORIZONTAL); - wxClientDC dc(parent); - dc.DrawRectangle(x, y+15, width, 2); -} - -wxStaticText* UiUtils::CreateTextParagraph(wxWindow* parent, const wchar_t* title, const wchar_t* data, int width, int height, int x, int y) { - CreateTextHeader(parent, title, width, x, y); - return new wxStaticText(parent, wxID_ANY, data, wxPoint(x+23, y+30), wxSize(width-23, height), wxALIGN_LEFT); -} - -wxStaticText* UiUtils::CreateTextParagraphWithLabels(wxWindow* parent, const wchar_t* title, const wchar_t* label, const wchar_t* data, int width, int height, int x, int y) { - CreateTextHeader(parent, title, width, x, y); - new wxStaticText(parent, wxID_ANY, label, wxPoint(x+23, y+30), wxSize(width-23, height), wxALIGN_LEFT); - return new wxStaticText(parent, wxID_ANY, data, wxPoint(x+157, y+30), wxSize(width-157, height), wxALIGN_LEFT); -} - -/* As far as I can tell, this is identical to the way Taiga implements it. - Kind of cool, I didn't even look into the source code for it :p */ -wxTextCtrl* UiUtils::CreateSelectableTextParagraph(wxWindow* parent, const wchar_t* title, const wchar_t* data, int width, int height, int x, int y) { - CreateTextHeader(parent, title, width, x, y); - wxTextCtrl* textctrl = new wxTextCtrl(parent, wxID_ANY, "", wxPoint(x+23, y+30), wxSize(width-23, height), wxTE_LEFT | wxBORDER_NONE | wxTE_BESTWRAP | wxTE_READONLY | wxTE_MULTILINE | wxTE_NO_VSCROLL); - (*textctrl) << data; - return textctrl; -} +#include "window.h" +#include "ui_utils.h" +#ifdef MACOSX +#include "sys/osx/dark_theme.h" +#else +#include "sys/win32/dark_theme.h" +#endif + +bool UiUtils::IsInDarkMode() { + if (session.config.theme != OS) + return (session.config.theme == DARK); +#ifdef MACOSX + if (osx::DarkThemeAvailable()) { + if (osx::IsInDarkTheme()) { + return true; + } else { + return false; + } + } +#elif defined(WIN32) + if (win32::DarkThemeAvailable()) { + if (win32::IsInDarkTheme()) { + return true; + } else { + return false; + } + } +#endif + return (session.config.theme == DARK); +} + +void UiUtils::CreateTextHeader(QWidget* parent, QString title, QPoint point, QSize size) { + QLabel* static_text_title = new QLabel(title, parent); + static_text_title->setTextFormat(Qt::PlainText); + static_text_title->setStyleSheet("font-weight: bold"); + static_text_title->move(point.x(), point.y()); + static_text_title->resize(size.width(), 16); + + QFrame* static_text_line = new QFrame(parent); + static_text_line->setFrameShape(QFrame::HLine); + static_text_line->setFrameShadow(QFrame::Sunken); + static_text_line->resize(size.width(), 2); + static_text_line->move(point.x(), point.y()+18); +} + +QPlainTextEdit* UiUtils::CreateTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size) { + CreateTextHeader(parent, title, point, size); + + QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent); + paragraph->setTextInteractionFlags(Qt::NoTextInteraction); + paragraph->setWordWrapMode(QTextOption::NoWrap); + paragraph->setFrameShape(QFrame::NoFrame); + paragraph->move(point.x()+12, point.y()+32); + paragraph->resize(size.width()-12, size.height()-32); + return paragraph; +} + +QPlainTextEdit* UiUtils::CreateTextParagraphWithLabels(QWidget* parent, QString title, QString label, QString data, QPoint point, QSize size) { + CreateTextHeader(parent, title, point, size); + + QPlainTextEdit* label_t = new QPlainTextEdit(label, parent); + label_t->setTextInteractionFlags(Qt::NoTextInteraction); + label_t->setWordWrapMode(QTextOption::NoWrap); + label_t->setFrameShape(QFrame::NoFrame); + label_t->move(point.x()+12, point.y()+32); + label_t->resize(90, size.height()-32); + + QPlainTextEdit* paragraph = new QPlainTextEdit(data, parent); + paragraph->setTextInteractionFlags(Qt::NoTextInteraction); + paragraph->setWordWrapMode(QTextOption::NoWrap); + paragraph->setFrameShape(QFrame::NoFrame); + paragraph->move(point.x()+102, point.y()+32); + paragraph->resize(size.width()-102, size.height()-32); + return paragraph; +} + +/* As far as I can tell, this is identical to the way Taiga implements it. + Kind of cool, I didn't even look into the source code for it :p */ +QPlainTextEdit* UiUtils::CreateSelectableTextParagraph(QWidget* parent, QString title, QString data, QPoint point, QSize size) { + CreateTextHeader(parent, title, point, size); + + QPlainTextEdit* text_edit = new QPlainTextEdit(data, parent); + text_edit->setReadOnly(true); + text_edit->setFrameShape(QFrame::NoFrame); + text_edit->move(point.x()+12, point.y()+32); + text_edit->resize(size.width()-12, size.height()-32); + return text_edit; +}
--- a/src/window.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/window.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,158 +1,158 @@ -#include "window.h" -#include <curl/curl.h> -#include "page.h" -#include "config.h" -#include "anime.h" -#include "statistics.h" -#include "now_playing.h" -#include "16x16/document-list.png.h" -#include "16x16/film.png.h" -#include "16x16/chart.png.h" -#include "16x16/clock-history-frame.png.h" -#include "16x16/magnifier.png.h" -#include "16x16/calendar.png.h" -#include "16x16/feed.png.h" -#include "24x24/arrow-circle-double-135.png.h" -#include "24x24/folder-open.png.h" -#include "24x24/gear.png.h" - -Config Weeaboo::config = Config(); - -WeeabooFrame::WeeabooFrame(const wxString& title, const wxPoint& pos, const wxSize& size) - : wxFrame(NULL, wxID_ANY, title, pos, size) { - /* ---- Menu Bar ---- */ - wxMenu* library_folders_submenu = new wxMenu; - library_folders_submenu->Append(ID_AddLibraryFolder, "&Add library folder"); - library_folders_submenu->Append(ID_ScanLibraryFolders, "&Rescan library folders"); - - wxMenu* file_menu = new wxMenu; - file_menu->AppendSubMenu(library_folders_submenu, "&Library folders"); - file_menu->Append(ID_SyncAnimeList, "&Sync anime list\tCtrl+S"); - file_menu->AppendSeparator(); - file_menu->Append(ID_PlayNextEpisode, "Play &next episode\tCtrl+N"); - file_menu->Append(ID_PlayRandomEpisode, "Play &random episode\tCtrl+R"); - file_menu->AppendSeparator(); - file_menu->Append(wxID_EXIT); - - wxMenu* help_menu = new wxMenu; - help_menu->Append(wxID_ABOUT); - - wxMenuBar* menu_bar = new wxMenuBar; - menu_bar->Append(file_menu, "&File"); - menu_bar->Append(help_menu, "&Help"); - - SetMenuBar(menu_bar); - - /* Toolbar */ - wxToolBar* top_toolbar = CreateToolBar(); - top_toolbar->SetToolBitmapSize(wxSize(24,24)); - top_toolbar->AddTool(ID_ToolbarSync, wxT("Sync"), wxBITMAP_PNG_FROM_DATA(arrow_circle_double_135)); - top_toolbar->Realize(); - - /* ---- Sidebar ---- */ - /* This first panel is only for the sizer... */ - wxPanel* left_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(140, 600), wxTAB_TRAVERSAL, wxPanelNameStr); - wxPanel* left_panel_inside = new wxPanel(left_panel, wxID_ANY, wxPoint(6, 6), wxSize(128, 588), wxTAB_TRAVERSAL); - wxToolBar* left_toolbar = new wxToolBar(left_panel_inside, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_FLAT | wxTB_VERTICAL | wxTB_HORZ_TEXT | wxTB_NODIVIDER); - left_toolbar->SetMargins(6, 6); - left_toolbar->SetToolBitmapSize(wxSize(16,16)); - left_toolbar->AddRadioTool(ID_NowPlaying, "Now Playing", wxBITMAP_PNG_FROM_DATA(film)); - left_toolbar->AddRadioTool(ID_AnimeList, "Anime List", wxBITMAP_PNG_FROM_DATA(document_list)); - left_toolbar->AddRadioTool(ID_History, "History", wxBITMAP_PNG_FROM_DATA(clock_history_frame)); - left_toolbar->AddRadioTool(ID_Statistics, "Statistics", wxBITMAP_PNG_FROM_DATA(chart)); - left_toolbar->AddRadioTool(ID_Search, "Search", wxBITMAP_PNG_FROM_DATA(magnifier)); - left_toolbar->AddRadioTool(ID_Seasons, "Seasons", wxBITMAP_PNG_FROM_DATA(calendar)); - left_toolbar->AddRadioTool(ID_Torrents, "Torrents", wxBITMAP_PNG_FROM_DATA(feed)); - - /* ---- Initialize our pages ---- */ - wxPanel* right_panel = new wxPanel(this, wxID_ANY, wxPoint(140, 0), wxSize(460, 600), wxTAB_TRAVERSAL, wxPanelNameStr); - now_playing = new NowPlaying(&pages[PAGE_NOW_PLAYING], right_panel); - anime_list = new AnimeListPage(&pages[PAGE_ANIME_LIST], right_panel); - anime_list->SyncAnimeList(); - anime_list->LoadAnimeList(this); - statistics = new Statistics(&pages[PAGE_STATISTICS], right_panel); - - status.current_page = PAGE_ANIME_LIST; // The below function depends on this value being set? - set_page(PAGE_ANIME_LIST); - left_toolbar->ToggleTool(ID_AnimeList, true); - left_toolbar->Realize(); - - wxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(left_panel, 0, wxEXPAND, 10); - sizer->Add(right_panel, 1, wxEXPAND, 10); - sizer->SetMinSize(600, 600); - this->SetSizer(sizer); - sizer->SetSizeHints(this); - -} - -bool Weeaboo::OnInit() { - config.Load(); - if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { - wxMessageBox("libcurl failed to initialize!", - "Error", wxOK | wxICON_ERROR); - } - wxSystemOptions::SetOption("msw.remap", - wxSystemOptions::HasOption("msw.remap") - ? wxSystemOptions::GetOptionInt("msw.remap") - : wxDisplayDepth() <= 8 ? 1 : 2 - ); - wxImage::AddHandler(new wxPNGHandler); - frame = new WeeabooFrame("Weeaboo", wxPoint(50, 50), wxSize(450, 340)); - frame->Show(true); - return true; -} - -#define TOOLBAR_HANDLER(name, page) \ -void WeeabooFrame::name(wxCommandEvent& event) { \ - set_page(page); \ -} -TOOLBAR_HANDLER(OnNowPlaying, PAGE_NOW_PLAYING) -TOOLBAR_HANDLER(OnAnimeList, PAGE_ANIME_LIST) -TOOLBAR_HANDLER(OnHistory, PAGE_HISTORY) -TOOLBAR_HANDLER(OnStatistics, PAGE_STATISTICS) -TOOLBAR_HANDLER(OnSearch, PAGE_SEARCH) -TOOLBAR_HANDLER(OnSeasons, PAGE_SEASONS) -TOOLBAR_HANDLER(OnTorrents, PAGE_TORRENTS) -#undef TOOLBAR_HANDLER - -void WeeabooFrame::OnClose(wxCloseEvent& event) { - Weeaboo::config.Save(); - curl_global_cleanup(); - delete anime_list; - event.Skip(); -} - -void WeeabooFrame::OnExit(wxCommandEvent& event) { - Close(true); -} - -void WeeabooFrame::OnAbout(wxCommandEvent& event) { - wxMessageBox("To be written", - "About Weeaboo", wxOK | wxICON_INFORMATION); -} - -void WeeabooFrame::OnAddFolder(wxCommandEvent& event) { - wxLogMessage("OnAddFolder"); -} - -void WeeabooFrame::OnScanFolders(wxCommandEvent& event) { - wxLogMessage("OnScanFolders"); -} - -void WeeabooFrame::OnNextEpisode(wxCommandEvent& event) { - wxLogMessage("OnNextEpisode"); -} - -void WeeabooFrame::OnSyncList(wxCommandEvent& event) { - anime_list->SyncAnimeList(); - anime_list->LoadAnimeList(this); -} - -void WeeabooFrame::OnRandomEpisode(wxCommandEvent& event) { - wxLogMessage("OnRandomEpisode"); -} - -AnimeListPage* WeeabooFrame::GetAnimeList() { - return anime_list; -} +#include "window.h" +#include <curl/curl.h> +#include "page.h" +#include "config.h" +#include "anime.h" +#include "statistics.h" +#include "now_playing.h" +#include "16x16/document-list.png.h" +#include "16x16/film.png.h" +#include "16x16/chart.png.h" +#include "16x16/clock-history-frame.png.h" +#include "16x16/magnifier.png.h" +#include "16x16/calendar.png.h" +#include "16x16/feed.png.h" +#include "24x24/arrow-circle-double-135.png.h" +#include "24x24/folder-open.png.h" +#include "24x24/gear.png.h" + +Config Weeaboo::config = Config(); + +WeeabooFrame::WeeabooFrame(const wxString& title, const wxPoint& pos, const wxSize& size) + : wxFrame(NULL, wxID_ANY, title, pos, size) { + /* ---- Menu Bar ---- */ + wxMenu* library_folders_submenu = new wxMenu; + library_folders_submenu->Append(ID_AddLibraryFolder, "&Add library folder"); + library_folders_submenu->Append(ID_ScanLibraryFolders, "&Rescan library folders"); + + wxMenu* file_menu = new wxMenu; + file_menu->AppendSubMenu(library_folders_submenu, "&Library folders"); + file_menu->Append(ID_SyncAnimeList, "&Sync anime list\tCtrl+S"); + file_menu->AppendSeparator(); + file_menu->Append(ID_PlayNextEpisode, "Play &next episode\tCtrl+N"); + file_menu->Append(ID_PlayRandomEpisode, "Play &random episode\tCtrl+R"); + file_menu->AppendSeparator(); + file_menu->Append(wxID_EXIT); + + wxMenu* help_menu = new wxMenu; + help_menu->Append(wxID_ABOUT); + + wxMenuBar* menu_bar = new wxMenuBar; + menu_bar->Append(file_menu, "&File"); + menu_bar->Append(help_menu, "&Help"); + + SetMenuBar(menu_bar); + + /* Toolbar */ + wxToolBar* top_toolbar = CreateToolBar(); + top_toolbar->SetToolBitmapSize(wxSize(24,24)); + top_toolbar->AddTool(ID_ToolbarSync, wxT("Sync"), wxBITMAP_PNG_FROM_DATA(arrow_circle_double_135)); + top_toolbar->Realize(); + + /* ---- Sidebar ---- */ + /* This first panel is only for the sizer... */ + wxPanel* left_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(140, 600), wxTAB_TRAVERSAL, wxPanelNameStr); + wxPanel* left_panel_inside = new wxPanel(left_panel, wxID_ANY, wxPoint(6, 6), wxSize(128, 588), wxTAB_TRAVERSAL); + wxToolBar* left_toolbar = new wxToolBar(left_panel_inside, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_FLAT | wxTB_VERTICAL | wxTB_HORZ_TEXT | wxTB_NODIVIDER); + left_toolbar->SetMargins(6, 6); + left_toolbar->SetToolBitmapSize(wxSize(16,16)); + left_toolbar->AddRadioTool(ID_NowPlaying, "Now Playing", wxBITMAP_PNG_FROM_DATA(film)); + left_toolbar->AddRadioTool(ID_AnimeList, "Anime List", wxBITMAP_PNG_FROM_DATA(document_list)); + left_toolbar->AddRadioTool(ID_History, "History", wxBITMAP_PNG_FROM_DATA(clock_history_frame)); + left_toolbar->AddRadioTool(ID_Statistics, "Statistics", wxBITMAP_PNG_FROM_DATA(chart)); + left_toolbar->AddRadioTool(ID_Search, "Search", wxBITMAP_PNG_FROM_DATA(magnifier)); + left_toolbar->AddRadioTool(ID_Seasons, "Seasons", wxBITMAP_PNG_FROM_DATA(calendar)); + left_toolbar->AddRadioTool(ID_Torrents, "Torrents", wxBITMAP_PNG_FROM_DATA(feed)); + + /* ---- Initialize our pages ---- */ + wxPanel* right_panel = new wxPanel(this, wxID_ANY, wxPoint(140, 0), wxSize(460, 600), wxTAB_TRAVERSAL, wxPanelNameStr); + now_playing = new NowPlaying(&pages[PAGE_NOW_PLAYING], right_panel); + anime_list = new AnimeListPage(&pages[PAGE_ANIME_LIST], right_panel); + anime_list->SyncAnimeList(); + anime_list->LoadAnimeList(this); + statistics = new Statistics(&pages[PAGE_STATISTICS], right_panel); + + status.current_page = PAGE_ANIME_LIST; // The below function depends on this value being set? + set_page(PAGE_ANIME_LIST); + left_toolbar->ToggleTool(ID_AnimeList, true); + left_toolbar->Realize(); + + wxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(left_panel, 0, wxEXPAND, 10); + sizer->Add(right_panel, 1, wxEXPAND, 10); + sizer->SetMinSize(600, 600); + this->SetSizer(sizer); + sizer->SetSizeHints(this); + +} + +bool Weeaboo::OnInit() { + config.Load(); + if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { + wxMessageBox("libcurl failed to initialize!", + "Error", wxOK | wxICON_ERROR); + } + wxSystemOptions::SetOption("msw.remap", + wxSystemOptions::HasOption("msw.remap") + ? wxSystemOptions::GetOptionInt("msw.remap") + : wxDisplayDepth() <= 8 ? 1 : 2 + ); + wxImage::AddHandler(new wxPNGHandler); + frame = new WeeabooFrame("Weeaboo", wxPoint(50, 50), wxSize(450, 340)); + frame->Show(true); + return true; +} + +#define TOOLBAR_HANDLER(name, page) \ +void WeeabooFrame::name(wxCommandEvent& event) { \ + set_page(page); \ +} +TOOLBAR_HANDLER(OnNowPlaying, PAGE_NOW_PLAYING) +TOOLBAR_HANDLER(OnAnimeList, PAGE_ANIME_LIST) +TOOLBAR_HANDLER(OnHistory, PAGE_HISTORY) +TOOLBAR_HANDLER(OnStatistics, PAGE_STATISTICS) +TOOLBAR_HANDLER(OnSearch, PAGE_SEARCH) +TOOLBAR_HANDLER(OnSeasons, PAGE_SEASONS) +TOOLBAR_HANDLER(OnTorrents, PAGE_TORRENTS) +#undef TOOLBAR_HANDLER + +void WeeabooFrame::OnClose(wxCloseEvent& event) { + Weeaboo::config.Save(); + curl_global_cleanup(); + delete anime_list; + event.Skip(); +} + +void WeeabooFrame::OnExit(wxCommandEvent& event) { + Close(true); +} + +void WeeabooFrame::OnAbout(wxCommandEvent& event) { + wxMessageBox("To be written", + "About Weeaboo", wxOK | wxICON_INFORMATION); +} + +void WeeabooFrame::OnAddFolder(wxCommandEvent& event) { + wxLogMessage("OnAddFolder"); +} + +void WeeabooFrame::OnScanFolders(wxCommandEvent& event) { + wxLogMessage("OnScanFolders"); +} + +void WeeabooFrame::OnNextEpisode(wxCommandEvent& event) { + wxLogMessage("OnNextEpisode"); +} + +void WeeabooFrame::OnSyncList(wxCommandEvent& event) { + anime_list->SyncAnimeList(); + anime_list->LoadAnimeList(this); +} + +void WeeabooFrame::OnRandomEpisode(wxCommandEvent& event) { + wxLogMessage("OnRandomEpisode"); +} + +AnimeListPage* WeeabooFrame::GetAnimeList() { + return anime_list; +}