From fb1f45817c2224e872e48e44e3566940e1a9d1cb Mon Sep 17 00:00:00 2001 From: blankie Date: Thu, 23 Nov 2023 23:20:49 +1100 Subject: [PATCH] Add media support --- models.cpp | 15 +++++++++++++++ models.h | 12 +++++++++++- routes/css.cpp | 34 ++++++++++++++++++++++++++++++++++ routes/user.cpp | 4 ++-- servehelper.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 106 insertions(+), 5 deletions(-) diff --git a/models.cpp b/models.cpp index 2251923..f07d9b9 100644 --- a/models.cpp +++ b/models.cpp @@ -52,6 +52,20 @@ void from_json(const json& j, Account& account) { account.server = sm.str(1); } +void from_json(const json& j, Media& media) { + j.at("type").get_to(media.type); + j.at("url").get_to(media.url); + if (!j.at("preview_url").is_null()) { + media.preview_url = j["preview_url"].get(); + } + if (!j.at("remote_url").is_null()) { + media.remote_url = j["remote_url"].get(); + } + if (!j.at("description").is_null()) { + media.description = j["description"].get(); + } +} + void from_json(const json& j, Post& post) { j.at("id").get_to(post.id); post.created_at = parse_rfc3339(j.at("created_at").get_ref()); @@ -77,6 +91,7 @@ void from_json(const json& j, Post& post) { from_json(j["reblog"].get(), *post.reblog.get()); } j.at("account").get_to(post.account); + j.at("media_attachments").get_to(post.media_attachments); j.at("emojis").get_to(post.emojis); } diff --git a/models.h b/models.h index f720d09..ea11cf1 100644 --- a/models.h +++ b/models.h @@ -10,7 +10,7 @@ enum PostSortingMethod { Posts = 0, PostsAndReplies, - Media, + MediaOnly, }; struct Emoji { @@ -50,6 +50,14 @@ struct Account { } }; +struct Media { + std::string type; + std::string url; + std::optional preview_url; + std::optional remote_url; + std::optional description; +}; + struct Post { std::string id; time_t created_at; @@ -64,10 +72,12 @@ struct Post { blankie::html::HTMLString content; std::unique_ptr reblog; Account account; + std::vector media_attachments; std::vector emojis; }; void from_json(const nlohmann::json& j, Emoji& emoji); void from_json(const nlohmann::json& j, AccountField& field); void from_json(const nlohmann::json& j, Account& account); +void from_json(const nlohmann::json& j, Media& media); void from_json(const nlohmann::json& j, Post& post); diff --git a/routes/css.cpp b/routes/css.cpp index 4ae4a70..9bc0f45 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -28,11 +28,19 @@ html { font-size: var(--font-size); font-family: sans-serif; padding: 10px; + overflow-wrap: break-word; } p, details { margin-top: 1em; margin-bottom: 1em; } +details[open] { + margin-bottom: 0; +} + +img { + object-fit: cover; +} a { color: var(--accent-color); @@ -76,6 +84,32 @@ a:hover { margin-left: auto; } +.post-attachments:not(:empty) { + margin-top: 1em; + margin-bottom: 1em; +} +/* https://stackoverflow.com/a/7354648 */ +@media (min-width: 801px) { + .post-attachments { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 0.5em; + width: fit-content; + } + + .post-attachments * { + margin: initial; + } +} +.post-attachments :is(img, video, audio) { + width: 320px; + height: 180px; +} +/* do not ask why not fit-content or min-content. i spent one hour on this, and that is one hour too long to be spending on this god damn grid */ +.post-attachments audio { + height: 40px; +} + /* ERROR PAGE */ .error { text-align: center; diff --git a/routes/user.cpp b/routes/user.cpp index 25f8362..3abc3c9 100644 --- a/routes/user.cpp +++ b/routes/user.cpp @@ -62,7 +62,7 @@ void user_route(const httplib::Request& req, httplib::Response& res) { body.nodes.push_back(Element("p", {{"class", "user_page-more_posts"}}, {"There are no more posts"})); } - serve(req, res, account->display_name + " (" + account->acct() + ')', std::move(body)); + serve(req, res, account->display_name + " (@" + account->acct() + ')', std::move(body)); } @@ -112,7 +112,7 @@ static inline Element user_header(const httplib::Request& req, const std::string Element("nav", {{"class", "user_page-user_posts_nav"}, {"id", "user_posts_nav"}}, { sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Posts), sorting_method_link(req, server, account, sorting_method, PostSortingMethod::PostsAndReplies), - sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Media), + sorting_method_link(req, server, account, sorting_method, PostSortingMethod::MediaOnly), }), }); return header; diff --git a/servehelper.cpp b/servehelper.cpp index b33fec2..015eb7e 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -17,6 +17,7 @@ static inline void preprocess_link(const httplib::Request& req, const std::strin static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& cls); static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector& emojis); static inline std::vector emojify(lxb_dom_document_t* document, std::string str, const std::vector& emojis); +static inline Element serialize_media(const Media& media); class CurlUrlException : public std::exception { public: @@ -34,7 +35,7 @@ void serve(const httplib::Request& req, httplib::Response& res, std::string titl using namespace std::string_literals; std::string css_url = get_origin(req) + "/style.css"; - res.set_header("Content-Security-Policy", "default-src 'none'; img-src https:; media-src: https:; style-src "s + css_url); + res.set_header("Content-Security-Policy", "default-src 'none'; img-src https:; media-src https:; style-src "s + css_url); Element head("head", { Element("meta", {{"charset", "utf-8"}}, {}), @@ -176,7 +177,14 @@ Element serialize_post(const httplib::Request& req, const std::string& server, c : "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at); const char* time_badge = post.edited_at < 0 ? "" : " (edited)"; - Node contents = preprocess_html(req, server, post.emojis, post.content); + Element contents("div", {preprocess_html(req, server, post.emojis, post.content)}); + Element post_attachments("div", {{"class", "post-attachments"}}, {}); + post_attachments.nodes.reserve(post.media_attachments.size()); + for (const Media& media : post.media_attachments) { + post_attachments.nodes.push_back(serialize_media(media)); + } + contents.nodes.push_back(std::move(post_attachments)); + if (post.sensitive) { std::string spoiler_text = !post.spoiler_text.empty() ? post.spoiler_text : "See more"; contents = Element("details", { @@ -366,3 +374,37 @@ static inline std::vector emojify(lxb_dom_document_t* document, return res; } + +static inline Element serialize_media(const Media& media) { + Element element = [&]() { + if (media.type == "image") { + return Element("a", {{"href", media.url}}, { + Element("img", {{"src", media.preview_url.value_or(media.url)}}, {}), + }); + } else if (media.type == "video") { + Element video("video", {{"controls", ""}, {"src", media.url}}, {}); + if (media.preview_url) { + video.attributes.push_back({"poster", *media.preview_url}); + } + return video; + } else if (media.type == "audio") { + return Element("audio", {{"controls", ""}, {"src", media.url}}, {}); + } else if (media.type == "unknown" && media.remote_url) { + if (media.remote_url) { + // https://botsin.space/@lina@vt.social/111053598696451525 + return Element("a", {{"href", *media.remote_url}}, {"Media is not available from this instance, view externally"}); + } else { + return Element("p", {"Media is not available from this instance"}); + } + } else { + return Element("p", {"Unsupported media type: ", media.type}); + } + }(); + + if (media.description) { + element.attributes.push_back({"alt", *media.description}); + element.attributes.push_back({"title", *media.description}); + } + + return element; +}