From b0e32b6717558e1f83f4133e6fa9691f76ee2dac Mon Sep 17 00:00:00 2001 From: Vyn Date: Thu, 21 Nov 2024 11:59:39 +0100 Subject: [PATCH] Can now import anime from Anilist by ID in Custom list --- {src => external}/httplib.h | 0 src/AddAnimeWindow.slint | 37 ++++++++++++++++ src/Anilist.h | 40 +++++++++++++++++ src/AppWindow.h | 85 +++++++++++++++++++++++-------------- src/AppWindow.slint | 15 ++++++- src/main.cpp | 2 +- src/state.slint | 11 ++++- src/ui.slint | 3 +- 8 files changed, 156 insertions(+), 37 deletions(-) rename {src => external}/httplib.h (100%) create mode 100644 src/AddAnimeWindow.slint diff --git a/src/httplib.h b/external/httplib.h similarity index 100% rename from src/httplib.h rename to external/httplib.h diff --git a/src/AddAnimeWindow.slint b/src/AddAnimeWindow.slint new file mode 100644 index 0000000..7673634 --- /dev/null +++ b/src/AddAnimeWindow.slint @@ -0,0 +1,37 @@ +import { State } from "./state.slint"; +import { VText, VTextInput , VButton, ToggleButton, VActionButton, Svg, Palette } from "@selenite"; +import { ComboBox } from "std-widgets.slint"; + +export component AddAnimeWindow inherits Window { + title: "Lali - Add List"; + + background: Palette.background; + default-font-size: 16px; + padding: 0px; + + width: 200px; + + private property show-add-list-form: false; + in-out property anilist-anime-id; + + callback import-anilist-anime(string); + + VerticalLayout { + alignment: start; + padding: 16px; + spacing: 16px; + source := ComboBox { + model: ["Anilist", "Custom"]; + } + + if source.current-value == "Anilist" : VTextInput { + label: "Anilist anime ID"; + text <=> anilist-anime-id; + } + + VButton { + text: "Import"; + clicked => { import-anilist-anime(anilist-anime-id) } + } + } +} diff --git a/src/Anilist.h b/src/Anilist.h index c338a6b..e4f02fa 100644 --- a/src/Anilist.h +++ b/src/Anilist.h @@ -62,6 +62,46 @@ public: return std::get(rei::json::parse(res->body)); } + static std::optional fetchAnimeById(int id) { + httplib::Client cli("https://graphql.anilist.co"); + cli.set_follow_location(true); + + rei::json::JsonObject body{}; + rei::json::JsonObject variables{}; + const std::string graphqlQuery = "query Query($mediaId: Int) {\\n Media(id: $mediaId) {\\n id\\n title {\\n romaji\\n }\\n description\\n episodes\\n genres\\n status\\n startDate {\\n year\\n month\\n day\\n }\\n endDate {\\n year\\n month\\n day\\n }\\n coverImage {\\n extraLarge\\n }\\n idMal\\n isAdult\\n siteUrl\\n bannerImage\\n }\\n}"; + + variables.set("mediaId", id); + body.set("query", graphqlQuery).addObject("variables", variables); + auto res = cli.Post("/", rei::json::toString(body), "application/json"); + + if (res.error() != httplib::Error::Success) { + std::cerr << res.error() << std::endl; + return std::nullopt; + } + + if (!res->has_header("Content-Type")) { + std::cerr << "Missing 'Content-Type'" << std::endl; + return std::nullopt; + } + + const std::string contentType = res->get_header_value("Content-Type"); + if (!contentType.starts_with("application/json")) { + std::cerr << "Wrong content type" << " (" << contentType << ")" << std::endl; + return std::nullopt; + } + + if (res.error() != httplib::Error::Success) { + std::cerr << res.error() << std::endl; + return std::nullopt; + } + + if (res->status != 200) { + std::println("Status: {}\nBody: {}", res->status, res->body); + return std::nullopt; + } + return std::get(rei::json::parse(res->body)); + } + static std::optional fetchImage(const std::string& path) { httplib::Client cli("https://s4.anilist.co"); cli.set_follow_location(true); diff --git a/src/AppWindow.h b/src/AppWindow.h index 5447ada..edd5114 100644 --- a/src/AppWindow.h +++ b/src/AppWindow.h @@ -3,6 +3,8 @@ #include #include #include +#include "rei-json/Field.h" +#include "rei-json/Object.h" #include "ui.h" #include "rei-json/Array.h" #include "rei-json/json.h" @@ -106,6 +108,7 @@ public: animesSlint = std::make_shared>(); ui->global().set_current_list({ + .index = 0, .name = "ok", .animes = animesSlint }); @@ -137,31 +140,25 @@ public: addLocalList(std::string(params.name)); }); - ui->global().on_sync_list([&](slint::SharedString name) { - //std::println("{}",res->body); - try { - //addAnilistList("defaut", name, "Completed"); - + ui->global().on_open_add_anime_window([&]() { + addAnimeWindow->show(); + }); - /*std::println("0000");*/ - /*selectList(saveJson.getArray("lists").getObject(0).getArray("animes"), animesSlint);*/ - /*std::println("1111");*/ - /*imgsToLoad.clear();*/ - /*std::println("2222");*/ - /*saveJson.getArray("lists").getObject(0).getArray("animes").forEach([&] (rei::json::FieldValue& element, int index) {*/ - /*std::println("IN");*/ - /*imgsToLoad.push_back(element.asObject().getObject("coverImage").getString("extraLarge"));*/ - /*std::println("OUT");*/ - /*});*/ - /*std::println("3333");*/ - /*readLinks.unlock();*/ - - } catch (std::exception& e) { - std::println(std::cerr, "Error: {}", e.what()); - } catch (char const* e) { - std::println(std::cerr, "Error str: {}", e); - } + addAnimeWindow->on_import_anilist_anime([&] (slint::SharedString animeIdStr) { + int animeId = stoi(std::string(animeIdStr)); + int currentListIndex = ui->global().get_current_list().index; + addAnimeWindow->hide(); + addAnimeWindow->set_anilist_anime_id(""); + addAnilistAnime(currentListIndex, animeId); + }); + ui->global().on_sync_list([&]() { + int currentListIndex = ui->global().get_current_list().index; + auto& listJson = json.getArray("lists").getObject(currentListIndex); + const std::string listName = listJson.getString("name"); + const std::string anilistUserName = listJson.getString("anilistUserName"); + const std::string anilistListName = listJson.getString("anilistListName"); + addAnilistList(listName, anilistUserName, anilistListName); }); } @@ -201,6 +198,8 @@ public: } ui->global().set_current_list({ + .index = listIndex, + .source = list.getString("source") == "anilist" ? ui::ListSource::Anilist : ui::ListSource::Local, .name = slint::SharedString(list.getString("name")), .animes = animesSlint }); @@ -215,9 +214,6 @@ public: } auto anilistJson = anilistJsonResult.value(); - rei::json::JsonObject saveJson{}; - saveJson.set("version", 1); - rei::json::JsonObject listJson{}; anilistJson.getObject("data").getObject("MediaListCollection").getArray("lists") .forEach([&](rei::json::FieldValue& field, int index) { @@ -238,19 +234,27 @@ public: listJson.addArray("animes", animesJson); }); - json.getArray("lists").pushObject(listJson); - listsSlint->push_back(ui::List{ - .name = slint::SharedString(listName), - .selected = false + std::optional existingListIndex; + json.getArray("lists").forEach([&](rei::json::FieldValue& value, int index) { + if (value.asObject().getString("name") == listName) { + existingListIndex = index; + } }); + if (existingListIndex.has_value()) { + json.getArray("lists").getObject(existingListIndex.value()) = listJson; + selectList(existingListIndex.value()); + } else { + json.getArray("lists").pushObject(listJson); + listsSlint->push_back(ui::List{ + .name = slint::SharedString(listName), + .selected = false + }); + } save(); } void addLocalList(const std::string& listName) { - rei::json::JsonObject saveJson{}; - saveJson.set("version", 1); - rei::json::JsonObject listJson{}; listJson.set("name", listName); listJson.set("source", "local"); @@ -265,6 +269,20 @@ public: save(); } + void addAnilistAnime(int currentListIndex, int animeId) { + auto res = Anilist::fetchAnimeById(animeId); + if (!res.has_value()) { + std::println(std::cerr, "Cannot fetch anime id {}", animeId); + return; + } + std::println("{}", rei::json::toString(res.value())); + + rei::json::JsonArray& listJson = json.getArray("lists").getObject(currentListIndex).getArray("animes"); + listJson.pushObject(res.value().getObject("data").getObject("Media")); + selectList(currentListIndex); + save(); + } + slint::Image getAnimeImage(int animeId, std::string imageUrl) { //std::println("Get image for anime {}", animeId); std::string imagePath = "./imgs/" + std::to_string(animeId); @@ -322,6 +340,7 @@ private: rei::json::JsonObject json; slint::ComponentHandle ui; + slint::ComponentHandle addAnimeWindow = ui::AddAnimeWindow::create(); std::shared_ptr> animesSlint; std::shared_ptr> listsSlint; std::mutex animeListLock; diff --git a/src/AppWindow.slint b/src/AppWindow.slint index 3d8bb88..68d83ae 100644 --- a/src/AppWindow.slint +++ b/src/AppWindow.slint @@ -1,4 +1,4 @@ -import { State } from "./state.slint"; +import { State, ListSource } from "./state.slint"; import { VText, VTextInput , VButton, ToggleButton, VActionButton, Svg, Palette } from "@selenite"; import { ComboBox } from "std-widgets.slint"; @@ -117,6 +117,19 @@ export component AppWindow inherits Window { text: "\{State.current-list.animes.length} animes"; font-size: 1rem; } + + HorizontalLayout { + alignment: start; + if State.current-list.source == ListSource.Local : VButton { + icon-svg: Svg.plus; + text: "Add"; + clicked => { State.open-add-anime-window() } + } + if State.current-list.source == ListSource.Anilist : VButton { + text: "Sync"; + clicked => { State.sync-list() } + } + } } animes-grid-background := Rectangle { diff --git a/src/main.cpp b/src/main.cpp index 727a7f0..9e23b9d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include struct Config { std::string timerEndingScript = ""; diff --git a/src/state.slint b/src/state.slint index 99327e7..92ae0a5 100644 --- a/src/state.slint +++ b/src/state.slint @@ -1,3 +1,8 @@ +export enum ListSource { + Local, + Anilist +} + export struct Anime { id: int, title: string, @@ -12,6 +17,8 @@ export struct List { } export struct CurrentList { + index: int, + source: ListSource, name: string, animes: [Anime] } @@ -32,9 +39,11 @@ export global State { in-out property current-list; callback select-list(int); - callback sync-list(string); + callback sync-list(); callback add-anilist-list(AddAnilistListParams); callback add-local-list(AddLocalListParams); + callback open-add-anime-window(); + callback config-changed(); } diff --git a/src/ui.slint b/src/ui.slint index f46c1dd..a5a3442 100644 --- a/src/ui.slint +++ b/src/ui.slint @@ -1,4 +1,5 @@ import { AppWindow } from "./AppWindow.slint"; +import { AddAnimeWindow } from "./AddAnimeWindow.slint"; import { State } from "./state.slint"; -export { State, AppWindow } +export { State, AppWindow, AddAnimeWindow }