From ab79071908c6a244731d10c7b812a9a991bd830e Mon Sep 17 00:00:00 2001 From: dragonflylee Date: Thu, 30 May 2024 21:26:25 +0800 Subject: [PATCH] Support inbox message (#322) * Support inbox message * Support emote inbox chat * Support send chat msg * Refresh session list on dismiss * Fix share message ui * Fix panic on macOS * Cache avatar for inbox Chat * Inbox chat add message time * Fix cell focus when chat reload * Fix i18n in reply feed * Adjusting the size of the message icon and navigation rules * Update i18n * Fix user list * FeedCard: Show title when cover is unavailable * FeedCard: Simplify like list * Fix inbox chat fragment crash * Adjust the style at the bottom of the inbox chat page * Show animations when refreshing the chat list * RecyclingGrid: Fix blocking main thread * Avoid crashing due to closing the page too quickly although I have not encountered this problem before * Limit the maximum height of chat images * A better way to set the position of chat images * workaround where some messages exceed the box * FeedList: Automatically load next page * Support open activity page --------- Co-authored-by: xfangfang <2553041586@qq.com> --- resources/i18n/en-US/wiliwili.json | 23 ++ resources/i18n/zh-Hans/wiliwili.json | 23 ++ resources/i18n/zh-Hant/wiliwili.json | 23 ++ resources/svg/ico-inbox-activate.svg | 4 + resources/svg/ico-inbox.svg | 4 + resources/xml/activity/main.xml | 16 +- resources/xml/fragment/dynamic_detail.xml | 1 + resources/xml/fragment/inbox_chat.xml | 63 +++++ resources/xml/fragment/inbox_feed.xml | 16 ++ resources/xml/fragment/inbox_view.xml | 68 ++++++ resources/xml/views/feed_card.xml | 83 +++++++ resources/xml/views/inbox_msg.xml | 118 +++++++++ resources/xml/views/inbox_user.xml | 33 +++ resources/xml/views/user_info.xml | 3 +- .../include/activity/dynamic_activity.hpp | 29 +++ wiliwili/include/activity/main_activity.hpp | 1 + wiliwili/include/api/bilibili.h | 60 ++++- wiliwili/include/api/bilibili/api.h | 9 + .../api/bilibili/result/dynamic_article.h | 8 + .../api/bilibili/result/inbox_result.h | 223 ++++++++++++++++++ .../include/api/bilibili/result/mine_result.h | 1 + wiliwili/include/api/bilibili/util/http.hpp | 2 +- wiliwili/include/fragment/inbox_chat.hpp | 32 +++ wiliwili/include/fragment/inbox_feed.hpp | 32 +++ wiliwili/include/fragment/inbox_view.hpp | 30 +++ wiliwili/include/presenter/dynamic_video.hpp | 13 +- wiliwili/include/presenter/inbox_chat.hpp | 23 ++ wiliwili/include/presenter/inbox_feed.hpp | 31 +++ wiliwili/include/presenter/inbox_msg.hpp | 31 +++ wiliwili/include/utils/activity_helper.hpp | 6 + wiliwili/include/view/dynamic_article.hpp | 29 +++ wiliwili/include/view/inbox_msg_card.hpp | 31 +++ wiliwili/source/activity/main_activity.cpp | 37 ++- wiliwili/source/api/dynamic_api.cpp | 8 + wiliwili/source/api/mine_api.cpp | 98 ++++++++ wiliwili/source/api/video_detail_api.cpp | 2 +- wiliwili/source/fragment/inbox_chat.cpp | 176 ++++++++++++++ wiliwili/source/fragment/inbox_feed.cpp | 201 ++++++++++++++++ wiliwili/source/fragment/inbox_view.cpp | 167 +++++++++++++ wiliwili/source/presenter/dynamic_video.cpp | 23 ++ wiliwili/source/presenter/inbox_chat.cpp | 77 ++++++ wiliwili/source/presenter/inbox_feed.cpp | 77 ++++++ wiliwili/source/presenter/inbox_msg.cpp | 58 +++++ wiliwili/source/utils/activity_helper.cpp | 13 + wiliwili/source/utils/register_helper.cpp | 2 + wiliwili/source/view/dynamic_article.cpp | 149 ++++++------ wiliwili/source/view/inbox_msg_card.cpp | 146 ++++++++++++ wiliwili/source/view/recycling_grid.cpp | 8 + 48 files changed, 2232 insertions(+), 79 deletions(-) create mode 100644 resources/svg/ico-inbox-activate.svg create mode 100644 resources/svg/ico-inbox.svg create mode 100644 resources/xml/fragment/inbox_chat.xml create mode 100644 resources/xml/fragment/inbox_feed.xml create mode 100644 resources/xml/fragment/inbox_view.xml create mode 100644 resources/xml/views/feed_card.xml create mode 100644 resources/xml/views/inbox_msg.xml create mode 100644 resources/xml/views/inbox_user.xml create mode 100644 wiliwili/include/activity/dynamic_activity.hpp create mode 100644 wiliwili/include/api/bilibili/result/inbox_result.h create mode 100644 wiliwili/include/fragment/inbox_chat.hpp create mode 100644 wiliwili/include/fragment/inbox_feed.hpp create mode 100644 wiliwili/include/fragment/inbox_view.hpp create mode 100644 wiliwili/include/presenter/inbox_chat.hpp create mode 100644 wiliwili/include/presenter/inbox_feed.hpp create mode 100644 wiliwili/include/presenter/inbox_msg.hpp create mode 100644 wiliwili/include/view/inbox_msg_card.hpp create mode 100644 wiliwili/source/fragment/inbox_chat.cpp create mode 100644 wiliwili/source/fragment/inbox_feed.cpp create mode 100644 wiliwili/source/fragment/inbox_view.cpp create mode 100644 wiliwili/source/presenter/inbox_chat.cpp create mode 100644 wiliwili/source/presenter/inbox_feed.cpp create mode 100644 wiliwili/source/presenter/inbox_msg.cpp create mode 100644 wiliwili/source/view/inbox_msg_card.cpp diff --git a/resources/i18n/en-US/wiliwili.json b/resources/i18n/en-US/wiliwili.json index 8412b691..bc9bb6ae 100644 --- a/resources/i18n/en-US/wiliwili.json +++ b/resources/i18n/en-US/wiliwili.json @@ -245,6 +245,29 @@ "all": "All activities", "refresh": "Refresh list" }, + "inbox": { + "chat": { + "tab": "Chat List", + "hint": "Send a message to start chatting ~", + "unknown": "The message type is currently not supported" + }, + "tab": { + "like": "Likes", + "at": "@me", + "reply": "Replies" + }, + "type": { + "video": "video", + "reply": "comment", + "article": "article", + "dynamic": "activity", + "album": "activity" + }, + "more": "and {} people liked my {}", + "like": "liked my {}", + "at": "@me at {}", + "reply": "replied to my {}" + }, "mine": { "login": { "check": "Checking login info...", diff --git a/resources/i18n/zh-Hans/wiliwili.json b/resources/i18n/zh-Hans/wiliwili.json index 56612823..2d80a383 100644 --- a/resources/i18n/zh-Hans/wiliwili.json +++ b/resources/i18n/zh-Hans/wiliwili.json @@ -245,6 +245,29 @@ "all": "全部动态", "refresh": "刷新列表" }, + "inbox": { + "chat": { + "tab": "聊天列表", + "hint": "发个消息聊聊呗 ~", + "unknown": "暂不支持该消息" + }, + "tab": { + "like": "收到的赞", + "at": "@我的", + "reply": "回复我的" + }, + "type": { + "video": "视频", + "reply": "评论", + "article": "文章", + "dynamic": "动态", + "album": "动态" + }, + "more": "等 {} 人赞了我的{}", + "like": "赞了我的{}", + "at": "在{}中@了我", + "reply": "回复了我的{}" + }, "mine": { "login": { "check": "检查登录信息...", diff --git a/resources/i18n/zh-Hant/wiliwili.json b/resources/i18n/zh-Hant/wiliwili.json index bb815025..ae5236ce 100644 --- a/resources/i18n/zh-Hant/wiliwili.json +++ b/resources/i18n/zh-Hant/wiliwili.json @@ -245,6 +245,29 @@ "all": "全部動態", "refresh": "重新整理列表" }, + "inbox": { + "chat": { + "tab": "聊天列表", + "hint": "發個消息聊聊唄 ~", + "unknown": "暫不支持該消息" + }, + "tab": { + "like": "收到的贊", + "at": "@我的", + "reply": "回復我的" + }, + "type": { + "video": "視頻", + "reply": "評論", + "article": "文章", + "dynamic": "動態", + "album": "動態" + }, + "more": "等 {} 人贊了我的{}", + "like": "贊了我的{}", + "at": "在{}中@了我", + "reply": "回復我的{}" + }, "mine": { "login": { "check": "檢查登入資訊...", diff --git a/resources/svg/ico-inbox-activate.svg b/resources/svg/ico-inbox-activate.svg new file mode 100644 index 00000000..234bade6 --- /dev/null +++ b/resources/svg/ico-inbox-activate.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/svg/ico-inbox.svg b/resources/svg/ico-inbox.svg new file mode 100644 index 00000000..7542dadf --- /dev/null +++ b/resources/svg/ico-inbox.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/xml/activity/main.xml b/resources/xml/activity/main.xml index 8a6456a2..befcefaf 100644 --- a/resources/xml/activity/main.xml +++ b/resources/xml/activity/main.xml @@ -31,7 +31,7 @@ @@ -45,6 +45,19 @@ alignItems="center" positionType="absolute" positionBottom="20"> + + + + diff --git a/resources/xml/fragment/dynamic_detail.xml b/resources/xml/fragment/dynamic_detail.xml index 85d30018..96491ce7 100644 --- a/resources/xml/fragment/dynamic_detail.xml +++ b/resources/xml/fragment/dynamic_detail.xml @@ -16,6 +16,7 @@ id="dynamic/detail/grid"/> diff --git a/resources/xml/fragment/inbox_chat.xml b/resources/xml/fragment/inbox_chat.xml new file mode 100644 index 00000000..bbb23553 --- /dev/null +++ b/resources/xml/fragment/inbox_chat.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/xml/fragment/inbox_feed.xml b/resources/xml/fragment/inbox_feed.xml new file mode 100644 index 00000000..34d72a9b --- /dev/null +++ b/resources/xml/fragment/inbox_feed.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/resources/xml/fragment/inbox_view.xml b/resources/xml/fragment/inbox_view.xml new file mode 100644 index 00000000..191d9741 --- /dev/null +++ b/resources/xml/fragment/inbox_view.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/xml/views/feed_card.xml b/resources/xml/views/feed_card.xml new file mode 100644 index 00000000..699e9c96 --- /dev/null +++ b/resources/xml/views/feed_card.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/xml/views/inbox_msg.xml b/resources/xml/views/inbox_msg.xml new file mode 100644 index 00000000..80a863e7 --- /dev/null +++ b/resources/xml/views/inbox_msg.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/xml/views/inbox_user.xml b/resources/xml/views/inbox_user.xml new file mode 100644 index 00000000..078f0219 --- /dev/null +++ b/resources/xml/views/inbox_user.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/xml/views/user_info.xml b/resources/xml/views/user_info.xml index 78ede18d..001ea398 100644 --- a/resources/xml/views/user_info.xml +++ b/resources/xml/views/user_info.xml @@ -47,7 +47,8 @@ fontSize="16" grow="1" marginTop="4" - verticalAlign="top" + verticalAlign="center" + singleLine="true" wireframe="false" textColor="@theme/font/grey"/> diff --git a/wiliwili/include/activity/dynamic_activity.hpp b/wiliwili/include/activity/dynamic_activity.hpp new file mode 100644 index 00000000..8e1e5351 --- /dev/null +++ b/wiliwili/include/activity/dynamic_activity.hpp @@ -0,0 +1,29 @@ +// +// Created by fang on 2024/5/30. +// + +#pragma once + +#include +#include +#include +#include + +#include "view/dynamic_article.hpp" + +class DynamicActivity : public brls::Activity { +public: + explicit DynamicActivity(std::string id) : id(std::move(id)) {} + + brls::View* createContentView() override { + auto* detail = new DynamicArticleDetail(id); + auto* container = new brls::AppletFrame(detail); + container->setHeaderVisibility(brls::Visibility::GONE); + container->setFooterVisibility(brls::AppletFrame::HIDE_BOTTOM_BAR ? brls::Visibility::GONE + : brls::Visibility::VISIBLE); + return container; + } + +private: + std::string id; +}; \ No newline at end of file diff --git a/wiliwili/include/activity/main_activity.hpp b/wiliwili/include/activity/main_activity.hpp index 5cd3ae6c..234c8ceb 100644 --- a/wiliwili/include/activity/main_activity.hpp +++ b/wiliwili/include/activity/main_activity.hpp @@ -33,5 +33,6 @@ class MainActivity : public brls::Activity { private: BRLS_BIND(CustomButton, settingBtn, "main/setting"); + BRLS_BIND(CustomButton, inboxBtn, "main/inbox"); BRLS_BIND(AutoTabFrame, tabFrame, "main/tabFrame"); }; diff --git a/wiliwili/include/api/bilibili.h b/wiliwili/include/api/bilibili.h index 4764eed8..9bd858ca 100644 --- a/wiliwili/include/api/bilibili.h +++ b/wiliwili/include/api/bilibili.h @@ -21,6 +21,7 @@ template class DynamicListResultWrapper; // 动态页列表基类 class DynamicVideoResult; // 一条视频动态 class DynamicArticleResult; // 一条图文动态 +class DynamicArticleResultWrapper; typedef std::vector DynamicVideoListResult; // 视频动态列表 typedef std::vector DynamicArticleListResult; // 图文动态列表 typedef DynamicListResultWrapper DynamicVideoListResultWrapper; // 动态 全部关注的视频列表 @@ -28,6 +29,16 @@ typedef DynamicListResultWrapper DynamicArticleListResultWrapper; // 动态 全部或指定UP主图文列表 class DynamicUpListResultWrapper; // 动态 最近更新的UP主列表 class UserDynamicVideoResultWrapper; // 动态 单个up主视频列表 +class MsgFeedCursor; // 消息页 回复列表游标 +class FeedReplyResultWrapper; // 消息页 回复列表 +class FeedAtResultWrapper; // 消息页 at 列表 +class FeedLikeResultWrapper; // 消息页 赞列表 +class UserCardResult; +typedef std::vector UserCardListResult; +class InboxChatResultWrapper; +class InboxMessageResultWrapper; +class InboxSendResult; +class DynamicVideoResult; class PGCIndexResultWrapper; class PGCIndexFilterWrapper; class PGCResultWrapper; @@ -124,6 +135,45 @@ class BilibiliClient { const std::function& callback = nullptr, const ErrorCallback& error = nullptr); + /// 批量获取用户昵称头像 + static void get_user_cards(const std::vector& uids, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + /// 私信消息记录 + static void new_inbox_sessions(time_t begin_ts = 0, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + /// 私信消息记录 + static void fetch_inbox_msgs(const std::string& talker_id, size_t size = 20, int session_type = 1, + const std::string& begin_seqno = "", + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + static void update_inbox_ack(const std::string& talker_id, int session_type = 1, const std::string& ack_seqno = "", + const std::string& csrf = "", const ErrorCallback& error = nullptr); + + static void send_inbox_msg(const std::string& sender_id, const std::string& receiver_id, const std::string& message, + const std::string& csrf = "", + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + /// 消息页 回复列表 + static void msg_feed_reply(const MsgFeedCursor& cursor, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + /// 消息页 at 列表 + static void msg_feed_at(const MsgFeedCursor& cursor, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + + /// 消息页 收到的赞列表 + static void msg_feed_like(const MsgFeedCursor& cursor, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + /// get person history videos static void get_my_history(const HistoryVideoListCursor& cursor, const std::function& callback = nullptr, @@ -222,7 +272,7 @@ class BilibiliClient { const ErrorCallback& error = nullptr); /// 获取视频防遮挡数据 - static void get_webmask(const std::string& url, uint64_t rangeStart, uint64_t rangeEnd, + static void get_webmask(const std::string& url, int64_t rangeStart, int64_t rangeEnd, const std::function& callback = nullptr, const ErrorCallback& error = nullptr); @@ -381,8 +431,7 @@ class BilibiliClient { const ErrorCallback& error = nullptr); /// 点赞动态 static void be_agree_dynamic(const std::string& access_key, const std::string& id, bool is_like, - const std::function& callback = nullptr, - const ErrorCallback& error = nullptr); + const std::function& callback = nullptr, const ErrorCallback& error = nullptr); /** * 回复评论 @@ -538,6 +587,11 @@ class BilibiliClient { static void dynamic_up_list(const std::function& callback = nullptr, const ErrorCallback& error = nullptr); + /// 获取单个动态详情 + static void get_dynamic_detail(const std::string& id, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + /// 设置页 获取网络时间 static void get_unix_time(const std::function& callback = nullptr, const ErrorCallback& error = nullptr); diff --git a/wiliwili/include/api/bilibili/api.h b/wiliwili/include/api/bilibili/api.h index c1dcff17..6bdfe749 100644 --- a/wiliwili/include/api/bilibili/api.h +++ b/wiliwili/include/api/bilibili/api.h @@ -169,7 +169,15 @@ const std::string CollectionVideoList = _apiBase + "/x/v3/fav/resource/list" const std::string CollectionVideoListSave = _apiBase + "/x/v3/fav/resource/deal"; const std::string UserUploadedVideo = _apiBase + "/x/space/arc/search"; const std::string UserRelationStat = _apiBase + "/x/relation/stat"; +const std::string MsgFeedLike = _apiBase + "/x/msgfeed/like"; +const std::string MsgFeedAt = _apiBase + "/x/msgfeed/at"; +const std::string MsgFeedReply = _apiBase + "/x/msgfeed/reply"; +const std::string UserCards = _vcBase + "/account/v1/user/cards"; const std::string UserDynamicStat = _vcBase + "/dynamic_svr/v1/dynamic_svr/space_num_ex"; +const std::string ChatSessions = _vcBase + "/session_svr/v1/session_svr/new_sessions"; +const std::string ChatUpdateAct = _vcBase + "/session_svr/v1/session_svr/update_ack"; +const std::string ChatFetchMsgs = _vcBase + "/svr_sync/v1/svr_sync/fetch_session_msgs"; +const std::string ChatSendMsg = _vcBase + "/web_im/v1/web_im/send_msg"; /// 用户追番/追剧 const std::string UserBangumiCollection = _apiBase + "/x/space/bangumi/follow/list"; /// 用户订阅合集列表 @@ -195,6 +203,7 @@ const std::string DynamicUpList = _vcBase + "/dynamic_svr/v1/dynamic_svr/w_dy const std::string DynamicUpListV2 = _apiBase + "/x/polymer/web-dynamic/v1/uplist"; const std::string DynamicLike = _vcBase + "/dynamic_like/v1/dynamic_like/thumb"; const std::string UserDynamicVideo = _apiBase + "/x/space/arc/list"; +const std::string DynamicDetail = _apiBase + "/x/polymer/web-dynamic/desktop/v1/detail"; /// === /// 设置页API diff --git a/wiliwili/include/api/bilibili/result/dynamic_article.h b/wiliwili/include/api/bilibili/result/dynamic_article.h index 872a1b60..a5b9da11 100644 --- a/wiliwili/include/api/bilibili/result/dynamic_article.h +++ b/wiliwili/include/api/bilibili/result/dynamic_article.h @@ -281,4 +281,12 @@ DynamicArticleModuleResult_DELC; DynamicArticleResult_DELC; +class DynamicArticleResultWrapper { +public: + DynamicArticleResult item; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, DynamicArticleResultWrapper& nlohmann_json_t) { + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, item)); +} + }; // namespace bilibili \ No newline at end of file diff --git a/wiliwili/include/api/bilibili/result/inbox_result.h b/wiliwili/include/api/bilibili/result/inbox_result.h new file mode 100644 index 00000000..da054d92 --- /dev/null +++ b/wiliwili/include/api/bilibili/result/inbox_result.h @@ -0,0 +1,223 @@ +#pragma once + +#include + +namespace bilibili { + +class UserCardResult { +public: + uint64_t mid; + std::string name; + std::string face; + int level; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UserCardResult, mid, name, face, level); + +class MsgFeedUser { +public: + uint64_t mid; + std::string nickname; + std::string avatar; + bool follow; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MsgFeedUser, mid, nickname, avatar, follow); + +class MsgFeedItem { +public: + unsigned int subject_id; + unsigned int source_id; + std::string type; + std::string title; + std::string image; + std::string source_content; + std::string uri; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MsgFeedItem, subject_id, source_id, type, title, image, source_content, uri); + +class FeedReplyResult { +public: + uint64_t id; + MsgFeedUser user; + MsgFeedItem item; + time_t reply_time; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(FeedReplyResult, id, user, item, reply_time); + +typedef std::vector FeedReplyListResult; + +class MsgFeedCursor { +public: + bool is_end; + uint64_t id; + time_t time; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MsgFeedCursor, is_end, id, time); + +class FeedReplyResultWrapper { +public: + MsgFeedCursor cursor; + FeedReplyListResult items; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, FeedReplyResultWrapper& nlohmann_json_t) { + if (nlohmann_json_j.contains("items") && nlohmann_json_j.at("items").is_array()) { + nlohmann_json_j.at("items").get_to(nlohmann_json_t.items); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, cursor)); +} + +class FeedAtResult { +public: + uint64_t id; + MsgFeedUser user; + MsgFeedItem item; + time_t at_time; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(FeedAtResult, id, user, item, at_time); + +typedef std::vector FeedAtListResult; + +class FeedAtResultWrapper { +public: + MsgFeedCursor cursor; + FeedAtListResult items; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, FeedAtResultWrapper& nlohmann_json_t) { + if (nlohmann_json_j.contains("items") && nlohmann_json_j.at("items").is_array()) { + nlohmann_json_j.at("items").get_to(nlohmann_json_t.items); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, cursor)); +} + +class FeedLikeResult { +public: + uint64_t id; + std::vector users; + MsgFeedItem item; + time_t like_time; + size_t counts; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(FeedLikeResult, id, users, item, like_time, counts); + +typedef std::vector FeedLikeListResult; + +class FeedLikeResultTotal { +public: + MsgFeedCursor cursor; + FeedLikeListResult items; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(FeedLikeResultTotal, cursor, items); + +class FeedLikeResultWrapper { +public: + FeedLikeResultTotal total; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(FeedLikeResultWrapper, total); + +class InboxMessageResult { +public: + uint64_t sender_uid, receiver_id; + uint64_t msg_seqno, msg_key; + std::vector at_uids; + int msg_type, msg_source; + nlohmann::json content; + time_t timestamp; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, InboxMessageResult& nlohmann_json_t) { + if (nlohmann_json_j.contains("content") && nlohmann_json_j.at("content").is_string()) { + nlohmann::json::parse(nlohmann_json_j.at("content").get()).get_to(nlohmann_json_t.content); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, sender_uid, receiver_id, msg_seqno, msg_key, msg_type, + msg_source, timestamp)); +} + +typedef std::vector InboxMessageListResult; + +class InboxEmote { +public: + std::string text; + std::string url; + int size = 1; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InboxEmote, text, url); + +class InboxMessageResultWrapper { +public: + InboxMessageListResult messages; + std::vector e_infos; + int has_more; + uint64_t max_seqno; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, InboxMessageResultWrapper& nlohmann_json_t) { + if (nlohmann_json_j.contains("messages") && nlohmann_json_j.at("messages").is_array()) { + nlohmann_json_j.at("messages").get_to(nlohmann_json_t.messages); + } + if (nlohmann_json_j.contains("e_infos") && nlohmann_json_j.at("e_infos").is_array()) { + nlohmann_json_j.at("e_infos").get_to(nlohmann_json_t.e_infos); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, has_more, max_seqno)); +} + +class InboxSendResult { +public: + std::vector e_infos; + std::string msg_content; + uint64_t msg_key; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, InboxSendResult& nlohmann_json_t) { + if (nlohmann_json_j.contains("e_infos") && nlohmann_json_j.at("e_infos").is_array()) { + nlohmann_json_j.at("e_infos").get_to(nlohmann_json_t.e_infos); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, msg_content, msg_key)); +} + +class InboxAccountInfo { +public: + std::string name; + std::string pic_url; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InboxAccountInfo, name, pic_url); + +class InboxChatResult { +public: + uint64_t talker_id; + int session_type; + uint64_t ack_seqno; + uint64_t max_seqno; + int unread_count; + int system_msg_type; + int new_push_msg; + InboxAccountInfo account_info; + InboxMessageResult last_msg; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, InboxChatResult& nlohmann_json_t) { + if (nlohmann_json_j.contains("last_msg") && nlohmann_json_j.at("last_msg").is_object()) { + nlohmann_json_j.at("last_msg").get_to(nlohmann_json_t.last_msg); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, talker_id, session_type, ack_seqno, max_seqno, + unread_count, system_msg_type, new_push_msg, account_info)); +} + +typedef std::vector InboxChatListResult; + +class InboxChatResultWrapper { +public: + InboxChatListResult session_list; + int has_more; +}; +inline void from_json(const nlohmann::json& nlohmann_json_j, InboxChatResultWrapper& nlohmann_json_t) { + if (nlohmann_json_j.contains("session_list") && nlohmann_json_j.at("session_list").is_array()) { + nlohmann_json_j.at("session_list").get_to(nlohmann_json_t.session_list); + } + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, has_more)); +} + +class BroadcastServerResult { +public: + std::string domain; + int ws_port; + int wss_port; + int heartbeat; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(BroadcastServerResult, domain, ws_port, heartbeat); + +} // namespace bilibili \ No newline at end of file diff --git a/wiliwili/include/api/bilibili/result/mine_result.h b/wiliwili/include/api/bilibili/result/mine_result.h index d4c55d8a..84bb0669 100644 --- a/wiliwili/include/api/bilibili/result/mine_result.h +++ b/wiliwili/include/api/bilibili/result/mine_result.h @@ -6,6 +6,7 @@ #include "nlohmann/json.hpp" #include "bilibili/result/dynamic_video.h" +#include "bilibili/result/inbox_result.h" namespace bilibili { diff --git a/wiliwili/include/api/bilibili/util/http.hpp b/wiliwili/include/api/bilibili/util/http.hpp index 8300e62d..dab85803 100644 --- a/wiliwili/include/api/bilibili/util/http.hpp +++ b/wiliwili/include/api/bilibili/util/http.hpp @@ -99,7 +99,7 @@ class HTTP { nlohmann::json res = nlohmann::json::parse(r.text); int code = res.at("code").get(); if (code == 0) { - if (res.contains("data") && res.at("data").is_object()) { + if (res.contains("data") && (res.at("data").is_object() || res.at("data").is_array())) { CALLBACK(res.at("data").get()); } else if (res.contains("result") && res.at("result").is_object()) { CALLBACK(res.at("result").get()); diff --git a/wiliwili/include/fragment/inbox_chat.hpp b/wiliwili/include/fragment/inbox_chat.hpp new file mode 100644 index 00000000..10e80bdf --- /dev/null +++ b/wiliwili/include/fragment/inbox_chat.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "presenter/inbox_msg.hpp" + +namespace brls { +class Label; +}; +class RecyclingGrid; +class CustomButton; + +class InboxChat : public brls::Box, public InboxMsgRequest { +public: + InboxChat(const bilibili::InboxChatResult& r, std::function cb); + + ~InboxChat() override; + + void onMsgList(const bilibili::InboxMessageResultWrapper& result, bool refresh) override; + + void onSendMsg(const bilibili::InboxSendResult& result) override; + + void onError(const std::string& error) override; + +private: + BRLS_BIND(RecyclingGrid, recyclingGrid, "inbox/msgList"); + BRLS_BIND(brls::Label, labelTalker, "inbox/talker"); + BRLS_BIND(CustomButton, inputReply, "inbox/reply/hint"); + + bool toggleSend(); +}; \ No newline at end of file diff --git a/wiliwili/include/fragment/inbox_feed.hpp b/wiliwili/include/fragment/inbox_feed.hpp new file mode 100644 index 00000000..289f504f --- /dev/null +++ b/wiliwili/include/fragment/inbox_feed.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "view/auto_tab_frame.hpp" +#include "presenter/inbox_feed.hpp" + +class RecyclingGrid; + +class InboxFeed : public AttachedView, public InboxFeedRequest { +public: + InboxFeed(); + + ~InboxFeed() override; + + void onCreate() override; + + static View* create(); + + void setMode(MsgFeedMode mode); + + void onFeedReplyList(const bilibili::FeedReplyResultWrapper& result, bool refresh) override; + + void onFeedAtList(const bilibili::FeedAtResultWrapper& result, bool refresh) override; + + void onFeedLikeList(const bilibili::FeedLikeResultWrapper& result, bool refresh) override; + + void onError(const std::string& error) override; + +private: + BRLS_BIND(RecyclingGrid, recyclingGrid, "inbox/feedList"); + + MsgFeedMode feedMode; +}; \ No newline at end of file diff --git a/wiliwili/include/fragment/inbox_view.hpp b/wiliwili/include/fragment/inbox_view.hpp new file mode 100644 index 00000000..5c83c803 --- /dev/null +++ b/wiliwili/include/fragment/inbox_view.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "presenter/inbox_chat.hpp" + +class ButtonClose; +class RecyclingGrid; +class AutoTabFrame; + +class InboxView : public brls::Box, public InboxChatRequest { +public: + InboxView(); + ~InboxView(); + + bool isTranslucent() override; + + View* getDefaultFocus() override; + + void onChatList(const bilibili::InboxChatListResult& result, bool refresh) override; + + void onError(const std::string& error) override; + +private: + BRLS_BIND(ButtonClose, closebtn, "inbox/close"); + BRLS_BIND(brls::Box, cancel, "player/cancel"); + BRLS_BIND(AutoTabFrame, tabFrame, "inbox/tab/frame"); + BRLS_BIND(RecyclingGrid, recyclingGrid, "inbox/chatList"); +}; \ No newline at end of file diff --git a/wiliwili/include/presenter/dynamic_video.hpp b/wiliwili/include/presenter/dynamic_video.hpp index cbef9515..e3ae3be0 100644 --- a/wiliwili/include/presenter/dynamic_video.hpp +++ b/wiliwili/include/presenter/dynamic_video.hpp @@ -15,7 +15,7 @@ class DynamicVideoRequest : public Presenter { public: virtual void onDynamicVideoList(const bilibili::DynamicVideoListResult& result, unsigned int index); - virtual void onDynamicArticleList(const bilibili::DynamicArticleListResult &result, unsigned int index); + virtual void onDynamicArticleList(const bilibili::DynamicArticleListResult& result, unsigned int index); virtual void onVideoError(const std::string& error); @@ -32,9 +32,18 @@ class DynamicVideoRequest : public Presenter { void requestUserDynamicVideoList(int64_t mid, int pn = 0, int ps = 30); protected: - int64_t currentUser = 0; + int64_t currentUser = 0; unsigned int currentVideoPage = 1, currentArticlePage = 1; std::string currentVideoOffset, currentArticleOffset; void requestVideoData(unsigned int page, const std::string& offset, int64_t mid); +}; + +class DynamicArticleRequest : public Presenter { +public: + virtual void onDynamicArticle(const bilibili::DynamicArticleResult& result); + + virtual void onError(const std::string& error); + + void requestDynamicArticle(const std::string& id); }; \ No newline at end of file diff --git a/wiliwili/include/presenter/inbox_chat.hpp b/wiliwili/include/presenter/inbox_chat.hpp new file mode 100644 index 00000000..3507ae92 --- /dev/null +++ b/wiliwili/include/presenter/inbox_chat.hpp @@ -0,0 +1,23 @@ +// +// Created by fang on 2022/8/18. +// + +#pragma once + +#include "bilibili/result/inbox_result.h" +#include "presenter.h" + +typedef std::unordered_map InboxUserMap; + +class InboxChatRequest : public Presenter { +public: + virtual void onChatList(const bilibili::InboxChatListResult& result, bool refresh); + + virtual void onError(const std::string& error); + + void requestData(bool refresh = false); + +protected: + time_t last_time = 0; + InboxUserMap user_map; +}; \ No newline at end of file diff --git a/wiliwili/include/presenter/inbox_feed.hpp b/wiliwili/include/presenter/inbox_feed.hpp new file mode 100644 index 00000000..4c40f434 --- /dev/null +++ b/wiliwili/include/presenter/inbox_feed.hpp @@ -0,0 +1,31 @@ +// +// Created by fang on 2022/8/18. +// + +#pragma once + +#include "bilibili/result/inbox_result.h" +#include "presenter.h" + +enum class MsgFeedMode +{ + REPLY, + AT, + LIKE +}; + +class InboxFeedRequest : public Presenter { +public: + virtual void onFeedReplyList(const bilibili::FeedReplyResultWrapper& result, bool refresh); + + virtual void onFeedAtList(const bilibili::FeedAtResultWrapper& result, bool refresh); + + virtual void onFeedLikeList(const bilibili::FeedLikeResultWrapper& result, bool refresh); + + virtual void onError(const std::string& error); + + void requestData(MsgFeedMode mode, bool refresh = false); + +protected: + bilibili::MsgFeedCursor cursor{}; +}; \ No newline at end of file diff --git a/wiliwili/include/presenter/inbox_msg.hpp b/wiliwili/include/presenter/inbox_msg.hpp new file mode 100644 index 00000000..87331923 --- /dev/null +++ b/wiliwili/include/presenter/inbox_msg.hpp @@ -0,0 +1,31 @@ +// +// Created by fang on 2022/8/18. +// + +#pragma once + +#include "bilibili/result/inbox_result.h" +#include "presenter.h" + +class InboxMsgRequest : public Presenter { +public: + virtual void onMsgList(const bilibili::InboxMessageResultWrapper& result, bool refresh); + + virtual void onSendMsg(const bilibili::InboxSendResult& result); + + virtual void onError(const std::string& error); + + void setTalkerId(uint64_t mid); + + void setMsgSeq(uint64_t seq); + + void requestData(bool refresh = false, int session_type = 1, size_t size = 20); + + void updateAck(int session_type = 1); + + void sendMsg(const std::string& text); + +protected: + uint64_t talkerId = 0; + uint64_t msgSeq = 0; +}; \ No newline at end of file diff --git a/wiliwili/include/utils/activity_helper.hpp b/wiliwili/include/utils/activity_helper.hpp index d4d622f4..eef9bca7 100644 --- a/wiliwili/include/utils/activity_helper.hpp +++ b/wiliwili/include/utils/activity_helper.hpp @@ -28,6 +28,9 @@ class Intent { // 开启应用设置 static void openSetting(); + // 开启消息盒子 + static void openInbox(); + // switch 应用开启教程 static void openHint(); @@ -40,6 +43,9 @@ class Intent { // 开启 DLNA static void openDLNA(); + // 开启动态 + static void openActivity(const std::string& id); + static void _registerFullscreen(brls::Activity* activity); }; diff --git a/wiliwili/include/view/dynamic_article.hpp b/wiliwili/include/view/dynamic_article.hpp index e6f783d2..a06efd8f 100644 --- a/wiliwili/include/view/dynamic_article.hpp +++ b/wiliwili/include/view/dynamic_article.hpp @@ -7,7 +7,9 @@ #include #include "view/recycling_grid.hpp" +#include "view/button_close.hpp" #include "bilibili/result/dynamic_article.h" +#include "presenter/dynamic_video.hpp" #include "presenter/comment_related.hpp" class UserInfoView; @@ -15,6 +17,33 @@ class TextBox; class DynamicVideoCardView; class SVGImage; +class DynamicArticleDetail : public brls::Box, public CommentRequest, public DynamicArticleRequest { +public: + DynamicArticleDetail(const bilibili::DynamicArticleResult& data, + const bilibili::DynamicArticleModuleState& state); + + explicit DynamicArticleDetail(const std::string& id); + + void initList(const bilibili::DynamicArticleResult& data, + const bilibili::DynamicArticleModuleState& state); + + void onCommentInfo(const bilibili::VideoCommentResultWrapper& result) override; + + void onDynamicArticle(const bilibili::DynamicArticleResult& result) override; + + void toggleCommentMode(); + + brls::Event likeStateEvent; + brls::Event likeNumEvent; + +private: + BRLS_BIND(RecyclingGrid, recyclingGrid, "dynamic/detail/grid"); + BRLS_BIND(ButtonClose, buttonClose, "button/close"); + bilibili::DynamicArticleResult data; + bilibili::DynamicArticleModuleState state; +}; + + class DynamicArticleView : public RecyclingGridItem { public: DynamicArticleView(); diff --git a/wiliwili/include/view/inbox_msg_card.hpp b/wiliwili/include/view/inbox_msg_card.hpp new file mode 100644 index 00000000..0f354a3a --- /dev/null +++ b/wiliwili/include/view/inbox_msg_card.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "view/recycling_grid.hpp" +#include "bilibili/result/inbox_result.h" + +class TextBox; + +typedef std::shared_ptr InboxEmotePtr; +typedef std::unordered_map IEMap; + +class InboxMsgCard : public RecyclingGridItem { +public: + InboxMsgCard(); + + void setCard(const bilibili::InboxMessageResult& r, const IEMap& m, uint64_t talker); + + void setAvatar(const std::string& face); + + void setTimeVisible(bool visible); + +private: + BRLS_BIND(TextBox, textBox, "msg/content"); + BRLS_BIND(brls::Box, msgBox, "msg/content_box"); + BRLS_BIND(brls::Box, shareBox, "msg/share_box"); + BRLS_BIND(brls::Label, shareMisc, "msg/share/misc"); + BRLS_BIND(brls::Label, shareAuthor, "msg/share/author"); + BRLS_BIND(brls::Image, shareThumb, "msg/share/thumb"); + BRLS_BIND(brls::Image, talker, "avatar/talker"); + BRLS_BIND(brls::Image, mine, "avatar/mine"); + BRLS_BIND(brls::Label, msgTime, "msg/time"); +}; diff --git a/wiliwili/source/activity/main_activity.cpp b/wiliwili/source/activity/main_activity.cpp index ee6c95fb..2e4042a7 100644 --- a/wiliwili/source/activity/main_activity.cpp +++ b/wiliwili/source/activity/main_activity.cpp @@ -18,6 +18,7 @@ #include "activity/main_activity.hpp" #include "utils/activity_helper.hpp" +#include "utils/dialog_helper.hpp" #include "view/custom_button.hpp" #include "view/auto_tab_frame.hpp" #include "view/svg_image.hpp" @@ -56,7 +57,7 @@ void MainActivity::onContentAvailable() { } }); - this->settingBtn->setCustomNavigation([this](brls::FocusDirection direction) { + this->inboxBtn->setCustomNavigation([this](brls::FocusDirection direction) { if (tabFrame->getSideBarPosition() == AutoTabBarPosition::LEFT) { if (direction == brls::FocusDirection::RIGHT) { return (brls::View*)this->tabFrame->getActiveTab(); @@ -72,5 +73,37 @@ void MainActivity::onContentAvailable() { } return (brls::View*)nullptr; }); - this->settingBtn->getParent()->addGestureRecognizer(new brls::TapGestureRecognizer(this->settingBtn)); + this->settingBtn->setCustomNavigation([this](brls::FocusDirection direction) { + if (tabFrame->getSideBarPosition() == AutoTabBarPosition::LEFT) { + if (direction == brls::FocusDirection::RIGHT) { + return (brls::View*)this->tabFrame->getActiveTab(); + } else if (direction == brls::FocusDirection::UP) { + return (brls::View*)this->inboxBtn; + } + } else if (tabFrame->getSideBarPosition() == AutoTabBarPosition::TOP) { + if (direction == brls::FocusDirection::DOWN) { + return (brls::View*)this->tabFrame->getActiveTab(); + } else if (direction == brls::FocusDirection::LEFT) { + return (brls::View*)this->inboxBtn; + } + } + return (brls::View*)nullptr; + }); + this->settingBtn->addGestureRecognizer(new brls::TapGestureRecognizer(this->settingBtn)); + + this->inboxBtn->registerClickAction([](brls::View* view) -> bool { + if (DialogHelper::checkLogin()) Intent::openInbox(); + return true; + }); + + this->inboxBtn->getFocusEvent()->subscribe([this](bool value) { + SVGImage* image = dynamic_cast(this->inboxBtn->getChildren()[0]); + if (!image) return; + if (value) { + image->setImageFromSVGRes("svg/ico-inbox-activate.svg"); + } else { + image->setImageFromSVGRes("svg/ico-inbox.svg"); + } + }); + this->inboxBtn->addGestureRecognizer(new brls::TapGestureRecognizer(this->inboxBtn)); } \ No newline at end of file diff --git a/wiliwili/source/api/dynamic_api.cpp b/wiliwili/source/api/dynamic_api.cpp index cc989707..ceeb7543 100644 --- a/wiliwili/source/api/dynamic_api.cpp +++ b/wiliwili/source/api/dynamic_api.cpp @@ -57,4 +57,12 @@ void BilibiliClient::be_agree_dynamic(const std::string& access_key, const std:: HTTP::postResultAsync(Api::DynamicLike, {}, payload, callback, error); } +void BilibiliClient::get_dynamic_detail(const std::string& id, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync( + Api::DynamicDetail, {{"id", id}}, [callback](const DynamicArticleResultWrapper& wrapper) { callback(wrapper); }, + error); +} + } // namespace bilibili \ No newline at end of file diff --git a/wiliwili/source/api/mine_api.cpp b/wiliwili/source/api/mine_api.cpp index 437396a3..80ebb282 100644 --- a/wiliwili/source/api/mine_api.cpp +++ b/wiliwili/source/api/mine_api.cpp @@ -3,12 +3,14 @@ // #include #include +#include #include "bilibili.h" #include "bilibili/util/md5.hpp" #include "curl/curl.h" #include "bilibili/util/http.hpp" #include "bilibili/result/home_result.h" #include "bilibili/result/setting.h" +#include "bilibili/result/inbox_result.h" #include "bilibili/result/mine_collection_result.h" #include "bilibili/result/mine_bangumi_result.h" #include "bilibili/result/mine_result.h" @@ -124,6 +126,102 @@ void BilibiliClient::get_user_dynamic_count(const std::string& mid, HTTP::getResultAsync(Api::UserDynamicStat, {{"uids", mid}}, callback, error); } +void BilibiliClient::get_user_cards(const std::vector& uids, + const std::function& callback, + const ErrorCallback& error) { + std::string mid = fmt::format("{}", fmt::join(uids, ",")); + HTTP::getResultAsync(Api::UserCards, {{"uids", mid}}, callback, error); +} + +void BilibiliClient::new_inbox_sessions(time_t begin_ts, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::ChatSessions, { + {"begin_ts", std::to_string(begin_ts)}, + {"mobi_app", "web"}, + }, callback, error); +} + +void BilibiliClient::update_inbox_ack(const std::string& talker_id, + int session_type, + const std::string& ack_seqno, + const std::string& csrf, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::ChatUpdateAct, { + {"talker_id", talker_id}, + {"session_type", std::to_string(session_type)}, + {"ack_seqno", ack_seqno}, + {"csrf", csrf}, + {"mobi_app", "web"}, + }, nullptr, error); +} + +void BilibiliClient::fetch_inbox_msgs(const std::string& talker_id, size_t size, + int session_type, + const std::string& begin_seqno, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::ChatFetchMsgs, { + {"sender_device_id", "1"}, + {"talker_id", talker_id}, + {"session_type", std::to_string(session_type)}, + {"begin_seqno", begin_seqno}, + {"size", std::to_string(size)}, + {"mobi_app", "web"}, + }, callback, error); +} + +void BilibiliClient::send_inbox_msg(const std::string& sender_id, + const std::string& receiver_id, + const std::string& message, + const std::string& csrf, + const std::function& callback, + const ErrorCallback& error) { + cpr::Payload payload = { + {"msg[msg_type]", "1"}, + {"msg[content]", message}, + {"msg[sender_uid]", sender_id}, + {"msg[receiver_id]", receiver_id}, + {"msg[receiver_type]", "1"}, + {"msg[msg_status]", "0"}, + {"msg[timestamp]", std::to_string(wiliwili::unix_time())}, + {"msg[dev_id]", "0"}, + {"msg[new_face_version]", "1"}, + {"csrf", csrf}, + }; + HTTP::postResultAsync(Api::ChatSendMsg, { + {"w_sender_uid", sender_id}, + {"w_receiver_id", receiver_id}, + }, payload, callback, error); +} + +void BilibiliClient::msg_feed_reply(const MsgFeedCursor& cursor, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::MsgFeedReply, + {{"id", std::to_string(cursor.id)}, + {"reply_time", std::to_string(cursor.time)}}, + callback, error); +} + +void BilibiliClient::msg_feed_at(const MsgFeedCursor& cursor, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::MsgFeedAt, + {{"id", std::to_string(cursor.id)}, + {"at_time", std::to_string(cursor.time)}}, + callback, error); +} + +void BilibiliClient::msg_feed_like(const MsgFeedCursor& cursor, + const std::function& callback, + const ErrorCallback& error) { + HTTP::getResultAsync(Api::MsgFeedLike, + {{"id", std::to_string(cursor.id)}, + {"like_time", std::to_string(cursor.time)}}, + callback, error); +} + /// get person history videos void BilibiliClient::get_my_history(const HistoryVideoListCursor& cursor, const std::function& callback, diff --git a/wiliwili/source/api/video_detail_api.cpp b/wiliwili/source/api/video_detail_api.cpp index c9e0b932..2c2ab513 100644 --- a/wiliwili/source/api/video_detail_api.cpp +++ b/wiliwili/source/api/video_detail_api.cpp @@ -37,7 +37,7 @@ void BilibiliClient::get_page_detail(const std::string& bvid, int cid, error); } -void BilibiliClient::get_webmask(const std::string& url, uint64_t rangeStart, uint64_t rangeEnd, +void BilibiliClient::get_webmask(const std::string& url, int64_t rangeStart, int64_t rangeEnd, const std::function& callback, const ErrorCallback& error) { std::optional start, end; if (rangeStart != -1) start = rangeStart; diff --git a/wiliwili/source/fragment/inbox_chat.cpp b/wiliwili/source/fragment/inbox_chat.cpp new file mode 100644 index 00000000..64d0c5de --- /dev/null +++ b/wiliwili/source/fragment/inbox_chat.cpp @@ -0,0 +1,176 @@ +#include +#include +#include + +#include "fragment/inbox_chat.hpp" +#include "view/inbox_msg_card.hpp" +#include "view/custom_button.hpp" +#include "utils/number_helper.hpp" + +using namespace brls::literals; + +class InboxNoticeCard : public RecyclingGridItem { +public: + InboxNoticeCard() { + auto theme = brls::Application::getTheme(); + this->setFocusable(false); + this->setJustifyContent(brls::JustifyContent::CENTER); + noticeLabel = new brls::Label(); + this->noticeLabel->setFontSize(16); + this->noticeLabel->setTextColor(theme.getColor("font/grey")); + this->addView(noticeLabel); + } + + void setCard(const bilibili::InboxMessageResult& r) { + std::string msg = r.content.at("content"); + auto result = nlohmann::json::parse(msg); + this->noticeLabel->setText(result[0]["text"]); + } + +private: + brls::Label* noticeLabel; +}; + +class DataSourceMsgList : public RecyclingGridDataSource { +public: + explicit DataSourceMsgList(const bilibili::InboxMessageResultWrapper& result, uint64_t mid) + : list(std::move(result.messages)), talkerId(mid) { + std::sort(this->list.begin(), this->list.end(), + [](const bilibili::InboxMessageResult& x, const bilibili::InboxMessageResult& y) { + return x.msg_seqno < y.msg_seqno; + }); + + for (auto& e : result.e_infos) { + this->emotes.insert({e.text, std::make_shared(e)}); + } + } + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override { + auto& r = this->list[index]; + + if (r.msg_type == 18) { + auto* item = (InboxNoticeCard*)recycler->dequeueReusableCell("Notice"); + item->setCard(r); + return item; + } + + auto* item = (InboxMsgCard*)recycler->dequeueReusableCell("Cell"); + item->setCard(r, this->emotes, this->talkerId); + + item->setTimeVisible(index == 0 || this->list[index - 1].timestamp + 300 < r.timestamp); + return item; + } + + size_t getItemCount() override { return list.size(); } + + void onItemSelected(RecyclingGrid* recycler, size_t index) override {} + + bool appendData(const bilibili::InboxMessageResultWrapper& result) { + bool skip_all = true; + for (const auto& i : result.messages) { + bool skip = false; + for (const auto& j : this->list) { + if (j.msg_seqno == i.msg_seqno) { + skip = true; + break; + } + } + if (!skip) { + this->list.push_back(i); + skip_all = false; + } + } + + for (auto& e : result.e_infos) { + this->emotes[e.text] = std::make_shared(e); + } + return skip_all; + } + + void clearData() override { this->list.clear(); } + +private: + bilibili::InboxMessageListResult list; + IEMap emotes; + uint64_t talkerId; +}; + +InboxChat::InboxChat(const bilibili::InboxChatResult& r, std::function cb) { + this->inflateFromXMLRes("xml/fragment/inbox_chat.xml"); + brls::Logger::debug("Fragment InboxChat: create"); + + this->registerAction("hints/cancel"_i18n, brls::BUTTON_B, [this, cb](...) { + this->dismiss(cb); + return true; + }); + + this->setTalkerId(r.talker_id); + this->setMsgSeq(r.ack_seqno); + + recyclingGrid->registerCell("Cell", [r]() { + auto* card = new InboxMsgCard(); + card->setAvatar(r.account_info.pic_url); + return card; + }); + recyclingGrid->registerCell("Notice", []() { return new InboxNoticeCard(); }); + recyclingGrid->onNextPage([this, r]() { this->requestData(false, r.session_type); }); + recyclingGrid->setRefreshAction([this]() { this->recyclingGrid->forceRequestNextPage(); }); + // 避免聊天框和底部刷新按钮重叠 + recyclingGrid->setPaddingBottom(80); + + this->registerAction("wiliwili/home/common/refresh"_i18n, brls::BUTTON_X, [this](brls::View* view) { + this->recyclingGrid->refresh(); + return true; + }); + + labelTalker->setText(r.account_info.name); + + if (r.system_msg_type > 0) { + inputReply->setVisibility(brls::Visibility::GONE); + } else { + inputReply->registerClickAction([this](...) { return this->toggleSend(); }); + inputReply->addGestureRecognizer(new brls::TapGestureRecognizer(this->inputReply)); + + this->registerAction("", brls::BUTTON_Y, [this](brls::View* view) { return this->toggleSend(); }, true); + } + + this->requestData(true, r.session_type); +} + +InboxChat::~InboxChat() { brls::Logger::debug("Fragment InboxChat: delete"); } + +bool InboxChat::toggleSend() { + return brls::Application::getImeManager()->openForText( + [this](const std::string& text) { + if (text.empty()) return; + this->sendMsg(text); + }, + "wiliwili/inbox/chat/hint"_i18n, "", 500, "", 0); +} + +void InboxChat::onError(const std::string& error) { + brls::Threading::sync([this, error]() { this->recyclingGrid->setError(error); }); +} + +void InboxChat::onMsgList(const bilibili::InboxMessageResultWrapper& result, bool refresh) { + brls::Threading::sync([this, result, refresh]() { + auto* datasource = dynamic_cast(recyclingGrid->getDataSource()); + if (datasource && !refresh) { + if (!result.messages.empty()) { + if(!datasource->appendData(result)){ + // 只在有内容更新时才调整位置 + recyclingGrid->notifyDataChanged(); + recyclingGrid->selectRowAt(datasource->getItemCount() - 1, true); + } + } + } else { + datasource = new DataSourceMsgList(result, this->talkerId); + recyclingGrid->setDefaultCellFocus(datasource->getItemCount() - 1); + recyclingGrid->setDataSource(datasource); + brls::sync([this]() { recyclingGrid->selectRowAt(recyclingGrid->getDefaultCellFocus(), true); }); + } + }); +} + +void InboxChat::onSendMsg(const bilibili::InboxSendResult& result) { + brls::Threading::sync([this]() { this->recyclingGrid->forceRequestNextPage(); }); +} \ No newline at end of file diff --git a/wiliwili/source/fragment/inbox_feed.cpp b/wiliwili/source/fragment/inbox_feed.cpp new file mode 100644 index 00000000..53c113dd --- /dev/null +++ b/wiliwili/source/fragment/inbox_feed.cpp @@ -0,0 +1,201 @@ +#include +#include + +#include "fragment/inbox_feed.hpp" +#include "view/recycling_grid.hpp" +#include "view/text_box.hpp" +#include "utils/image_helper.hpp" +#include "utils/activity_helper.hpp" +#include "utils/string_helper.hpp" + +using namespace brls::literals; + +class FeedCard : public RecyclingGridItem { +public: + FeedCard() { this->inflateFromXMLRes("xml/views/feed_card.xml"); } + + void setItem(const bilibili::MsgFeedItem& item) { + this->textBox->setText(item.source_content); + if (item.image.empty()) { + this->picture->setVisibility(brls::Visibility::GONE); + this->title->setVisibility(brls::Visibility::VISIBLE); + this->title->setText(item.title); + } else { + this->picture->setVisibility(brls::Visibility::VISIBLE); + this->title->setVisibility(brls::Visibility::GONE); + ImageHelper::with(this->picture)->load(item.image + ImageHelper::v_ext); + } + } + + void setAuther(const bilibili::FeedLikeResult& r) { + std::vector users; + std::string t = brls::getStr("wiliwili/inbox/type/" + r.item.type); + std::string suffix; + if (r.users.size() > 2) { + suffix = wiliwili::format("wiliwili/inbox/more"_i18n, r.counts, t); + } else { + suffix = wiliwili::format("wiliwili/inbox/like"_i18n, t); + } + for (size_t i = 0; i < r.users.size() && i < 2; i++) { + users.emplace_back(r.users[i].nickname); + } + ImageHelper::with(this->avatar)->load(r.users.front().avatar + ImageHelper::face_ext); + this->labelAuthor->setText(pystring::join("、", users) + " " + suffix); + this->labelTime->setText(wiliwili::sec2date(r.like_time)); + } + + void setAuther(const bilibili::FeedAtResult& r) { + std::string t = brls::getStr("wiliwili/inbox/type/" + r.item.type); + ImageHelper::with(this->avatar)->load(r.user.avatar + ImageHelper::face_ext); + this->labelAuthor->setText(r.user.nickname); + this->labelTime->setText(wiliwili::sec2date(r.at_time)); + this->labelMisc->setText(wiliwili::format("wiliwili/inbox/at"_i18n, t)); + } + + void setAuther(const bilibili::FeedReplyResult& r) { + std::string t = brls::getStr("wiliwili/inbox/type/" + r.item.type); + ImageHelper::with(this->avatar)->load(r.user.avatar + ImageHelper::face_ext); + this->labelAuthor->setText(r.user.nickname); + this->labelTime->setText(wiliwili::sec2date(r.reply_time)); + this->labelMisc->setText(wiliwili::format("wiliwili/inbox/reply"_i18n, t)); + } + + void prepareForReuse() override { + this->avatar->setImageFromRes("pictures/default_avatar.png"); + this->picture->setImageFromRes("pictures/video-card-bg.png"); + } + + void cacheForReuse() override { + ImageHelper::clear(this->avatar); + ImageHelper::clear(this->picture); + } + +private: + BRLS_BIND(brls::Image, avatar, "feed/avatar"); + BRLS_BIND(brls::Label, labelAuthor, "feed/label/author"); + BRLS_BIND(brls::Label, labelMisc, "feed/label/misc"); + BRLS_BIND(brls::Label, labelTime, "feed/time"); + BRLS_BIND(TextBox, textBox, "feed/content"); + BRLS_BIND(TextBox, title, "feed/card/title"); + BRLS_BIND(brls::Image, picture, "feed/card/picture"); +}; + +template +class DataSourceFeedList : public RecyclingGridDataSource { +public: + explicit DataSourceFeedList(const std::vector& result) : list(std::move(result)) {} + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override { + auto* item = (FeedCard*)recycler->dequeueReusableCell("Cell"); + auto& r = this->list[index]; + item->setItem(r.item); + item->setAuther(r); + return item; + } + + size_t getItemCount() override { return list.size(); } + + void onItemSelected(RecyclingGrid* recycler, size_t index) override { + auto& r = this->list[index]; + size_t pos = r.item.uri.find_last_of('/'); + if (pos <= 0) return; + if (r.item.uri.compare(0, 18, "https://t.bilibili") == 0) { + // 解析动态id + std::string t = r.item.uri.substr(pos + 1); + Intent::openActivity(t); + return; + } else if (r.item.type == "video" || r.item.type == "reply") { + // 解析BV号 + std::string bv = r.item.uri.substr(pos + 1); + if (bv.compare(0, 2, "BV") == 0) + Intent::openBV(bv); + } + } + + void appendData(const std::vector& result) { + for (const auto& i : result) { + this->list.push_back(i); + } + } + + void clearData() override { this->list.clear(); } + +private: + std::vector list; +}; + +InboxFeed::InboxFeed() { + this->inflateFromXMLRes("xml/fragment/inbox_feed.xml"); + brls::Logger::debug("Fragment InboxFeed: create"); + + BRLS_REGISTER_ENUM_XML_ATTRIBUTE("mode", MsgFeedMode, this->setMode, + { + {"reply", MsgFeedMode::REPLY}, + {"at", MsgFeedMode::AT}, + {"like", MsgFeedMode::LIKE}, + }); + + // 消息列表 + recyclingGrid->registerCell("Cell", []() { return new FeedCard(); }); + + recyclingGrid->onNextPage([this](){ + this->requestData(feedMode, false); + }); + + recyclingGrid->setRefreshAction([this](){ + this->recyclingGrid->estimatedRowHeight = 100; + this->recyclingGrid->showSkeleton(); + this->requestData(feedMode, true); + brls::Application::giveFocus(this->getTabBar()); + }); +} + +InboxFeed::~InboxFeed() { brls::Logger::debug("Fragment InboxFeed: delete"); } + +void InboxFeed::onCreate() { + this->requestData(feedMode, true); + this->registerTabAction("", brls::ControllerButton::BUTTON_X ,[this](brls::View* view) { + this->recyclingGrid->refresh(); + return true; + }, true); +} + +void InboxFeed::setMode(MsgFeedMode mode) { this->feedMode = mode; } + +void InboxFeed::onFeedReplyList(const bilibili::FeedReplyResultWrapper& result, bool refresh) { + this->recyclingGrid->estimatedRowHeight = 500; + auto dataSource = dynamic_cast*>(recyclingGrid->getDataSource()); + if (dataSource && !refresh) { + dataSource->appendData(result.items); + recyclingGrid->notifyDataChanged(); + } else { + recyclingGrid->setDataSource(new DataSourceFeedList(result.items)); + } +} + +void InboxFeed::onFeedAtList(const bilibili::FeedAtResultWrapper& result, bool refresh) { + this->recyclingGrid->estimatedRowHeight = 500; + auto dataSource = dynamic_cast*>(recyclingGrid->getDataSource()); + if (dataSource && !refresh) { + dataSource->appendData(result.items); + recyclingGrid->notifyDataChanged(); + } else { + recyclingGrid->setDataSource(new DataSourceFeedList(result.items)); + } +} + +void InboxFeed::onFeedLikeList(const bilibili::FeedLikeResultWrapper& result, bool refresh) { + this->recyclingGrid->estimatedRowHeight = 500; + auto dataSource = dynamic_cast*>(recyclingGrid->getDataSource()); + if (dataSource && !refresh) { + dataSource->appendData(result.total.items); + recyclingGrid->notifyDataChanged(); + } else { + recyclingGrid->setDataSource(new DataSourceFeedList(result.total.items)); + } +} + +void InboxFeed::onError(const std::string& error) { + this->recyclingGrid->setError(error); +} + +brls::View* InboxFeed::create() { return new InboxFeed(); } \ No newline at end of file diff --git a/wiliwili/source/fragment/inbox_view.cpp b/wiliwili/source/fragment/inbox_view.cpp new file mode 100644 index 00000000..3012097a --- /dev/null +++ b/wiliwili/source/fragment/inbox_view.cpp @@ -0,0 +1,167 @@ +#include +#include + +#include "fragment/inbox_view.hpp" +#include "fragment/inbox_chat.hpp" +#include "view/button_close.hpp" +#include "view/recycling_grid.hpp" +#include "view/auto_tab_frame.hpp" +#include "view/user_info.hpp" +#include "utils/image_helper.hpp" +#include "utils/number_helper.hpp" + +using namespace brls::literals; + +class ChatUserCard : public RecyclingGridItem { +public: + ChatUserCard() { this->inflateFromXMLRes("xml/views/inbox_user.xml"); } + + void setCard(const bilibili::InboxChatResult& r) { + std::string misc; + if (r.last_msg.msg_type == 1) { + misc = r.last_msg.content.at("content"); + } else if (r.last_msg.msg_type == 2) { + misc = "[图片]"; + } else if (r.last_msg.msg_type == 7) { + misc = "[分享] "; + misc.append(r.last_msg.content.at("title")); + } else if (r.last_msg.msg_type == 14) { + misc = "[分享] "; + misc.append(r.last_msg.content.at("desc")); + } else if (r.last_msg.content.contains("title")) { + misc = r.last_msg.content.at("title"); + } + this->talker->setUserInfo(r.account_info.pic_url + ImageHelper::face_ext, r.account_info.name, misc); + if (r.last_msg.timestamp > 0) { + this->time->setText(wiliwili::sec2date(r.last_msg.timestamp)); + } + if (r.unread_count > 0) { + this->badge->setVisibility(brls::Visibility::VISIBLE); + } else { + this->badge->setVisibility(brls::Visibility::INVISIBLE); + } + } + + void prepareForReuse() override { this->talker->getAvatar()->setImageFromRes("pictures/default_avatar.png"); } + + void cacheForReuse() override { ImageHelper::clear(this->talker->getAvatar()); } + +private: + BRLS_BIND(UserInfoView, talker, "chat/talker"); + BRLS_BIND(brls::Label, time, "inbox/lastTime"); + BRLS_BIND(brls::Rectangle, badge, "badge"); +}; + +class DataSourceChatList : public RecyclingGridDataSource { +public: + explicit DataSourceChatList(bilibili::InboxChatListResult result) : list(std::move(result)) {} + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override { + //从缓存列表中取出 或者 新生成一个表单项 + auto* item = (ChatUserCard*)recycler->dequeueReusableCell("Cell"); + auto& r = this->list[index]; + item->setCard(r); + return item; + } + + size_t getItemCount() override { return list.size(); } + + void onItemSelected(RecyclingGrid* recycler, size_t index) override { + auto& r = this->list[index]; + auto* view = new InboxChat(r, [recycler]() { + recycler->refresh(); + }); + recycler->present(view); + } + + void appendData(const bilibili::InboxChatListResult& data) {} + + void clearData() override { this->list.clear(); } + +private: + bilibili::InboxChatListResult list; +}; + +InboxView::InboxView() { + this->inflateFromXMLRes("xml/fragment/inbox_view.xml"); + brls::Logger::debug("Fragment InboxView: create"); + + this->registerAction("hints/cancel"_i18n, brls::BUTTON_B, [](...) { + brls::Application::popActivity(); + return true; + }); + + this->cancel->registerClickAction([](...) { + brls::Application::popActivity(); + return true; + }); + this->cancel->addGestureRecognizer(new brls::TapGestureRecognizer(this->cancel)); + + closebtn->registerClickAction([](...) { + brls::Application::popActivity(); + return true; + }); + + this->registerAction( + "上一项", brls::ControllerButton::BUTTON_LT, + [this](brls::View* view) -> bool { + tabFrame->focus2LastTab(); + return true; + }, + true); + this->registerAction( + "上一项", brls::ControllerButton::BUTTON_LB, + [this](brls::View* view) -> bool { + tabFrame->focus2LastTab(); + return true; + }, + true); + + this->registerAction( + "下一项", brls::ControllerButton::BUTTON_RT, + [this](brls::View* view) -> bool { + tabFrame->focus2NextTab(); + return true; + }, + true); + this->registerAction( + "下一项", brls::ControllerButton::BUTTON_RB, + [this](brls::View* view) -> bool { + tabFrame->focus2NextTab(); + return true; + }, + true); + + recyclingGrid->registerCell("Cell", []() { return new ChatUserCard(); }); + recyclingGrid->setRefreshAction([this]() { + brls::Application::giveFocus(tabFrame->getSidebar()); + this->recyclingGrid->showSkeleton(); + this->requestData(true); + }); + recyclingGrid->registerAction("wiliwili/home/common/refresh"_i18n, brls::BUTTON_X, [this](brls::View* view) { + this->recyclingGrid->refresh(); + return true; + }); + + this->requestData(true); +} + +InboxView::~InboxView() { brls::Logger::debug("Fragment InboxView: delete"); } + +bool InboxView::isTranslucent() { return true; } + +brls::View* InboxView::getDefaultFocus() { return this->tabFrame->getDefaultFocus(); } + +void InboxView::onChatList(const bilibili::InboxChatListResult& result, bool refresh) { + auto* datasource = dynamic_cast(recyclingGrid->getDataSource()); + if (datasource && !refresh) { + if (!result.empty()) { + datasource->appendData(result); + recyclingGrid->notifyDataChanged(); + } + } else { + auto dataSource = new DataSourceChatList(result); + recyclingGrid->setDataSource(dataSource); + } +} + +void InboxView::onError(const std::string& error) { this->recyclingGrid->setError(error); } \ No newline at end of file diff --git a/wiliwili/source/presenter/dynamic_video.cpp b/wiliwili/source/presenter/dynamic_video.cpp index e49c1889..a08a3673 100644 --- a/wiliwili/source/presenter/dynamic_video.cpp +++ b/wiliwili/source/presenter/dynamic_video.cpp @@ -3,6 +3,7 @@ // #include +#include #include "bilibili.h" #include "bilibili/result/mine_result.h" @@ -105,4 +106,26 @@ void DynamicVideoRequest::requestUserDynamicVideoList(int64_t mid, int pn, int p [this](BILI_ERR) { this->onVideoError(error); }); +} + +void DynamicArticleRequest::onDynamicArticle(const bilibili::DynamicArticleResult& result) {} + +void DynamicArticleRequest::onError(const std::string& error) {} + +void DynamicArticleRequest::requestDynamicArticle(const std::string& id) { + ASYNC_RETAIN + BILI::get_dynamic_detail( + id, + [ASYNC_TOKEN](const bilibili::DynamicArticleResultWrapper& result) { + brls::sync([ASYNC_TOKEN, result]() { + ASYNC_RELEASE + this->onDynamicArticle(result.item); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); } \ No newline at end of file diff --git a/wiliwili/source/presenter/inbox_chat.cpp b/wiliwili/source/presenter/inbox_chat.cpp new file mode 100644 index 00000000..aebec680 --- /dev/null +++ b/wiliwili/source/presenter/inbox_chat.cpp @@ -0,0 +1,77 @@ +#include + +#include "bilibili.h" +#include "presenter/inbox_chat.hpp" +#include "utils/number_helper.hpp" +#include "utils/config_helper.hpp" + +void InboxChatRequest::onChatList(const bilibili::InboxChatListResult& result, bool refresh) {} + +void InboxChatRequest::onError(const std::string& error) {} + +void InboxChatRequest::requestData(bool refresh) { + ASYNC_RETAIN + BILI::new_inbox_sessions( + refresh ? 0 : this->last_time, + [ASYNC_TOKEN, refresh](const bilibili::InboxChatResultWrapper& result) { + + std::vector uids; + for (auto& s : result.session_list) { + if (s.account_info.name.empty()) { + auto it = user_map.find(s.talker_id); + if (it == user_map.end()) { + uids.push_back(std::to_string(s.talker_id)); + } + } + } + this->last_time = wiliwili::unix_time() * 1000000; + + // 不需要请求头像 + if (uids.empty()) { + auto list = std::move(result.session_list); + for (auto& s : list) { + auto it = user_map.find(s.talker_id); + if (it != user_map.end()) { + s.account_info.name = it->second.name; + s.account_info.pic_url = it->second.face; + } + } + brls::sync([ASYNC_TOKEN, list, refresh](){ + ASYNC_RELEASE + this->onChatList(list, refresh); + }); + return; + } + + BILI::get_user_cards( + uids, + [ASYNC_TOKEN, result, refresh](const bilibili::UserCardListResult& users) { + for (auto& u : users) user_map[u.mid] = u; + + auto list = std::move(result.session_list); + for (auto& s : list) { + auto it = user_map.find(s.talker_id); + if (it != user_map.end()) { + s.account_info.name = it->second.name; + s.account_info.pic_url = it->second.face; + } + } + brls::sync([ASYNC_TOKEN, list, refresh](){ + ASYNC_RELEASE + this->onChatList(list, refresh); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); +} \ No newline at end of file diff --git a/wiliwili/source/presenter/inbox_feed.cpp b/wiliwili/source/presenter/inbox_feed.cpp new file mode 100644 index 00000000..387fa275 --- /dev/null +++ b/wiliwili/source/presenter/inbox_feed.cpp @@ -0,0 +1,77 @@ +#include + +#include "bilibili.h" +#include "presenter/inbox_feed.hpp" + +void InboxFeedRequest::onFeedReplyList(const bilibili::FeedReplyResultWrapper& result, bool refresh) {} + +void InboxFeedRequest::onFeedAtList(const bilibili::FeedAtResultWrapper& result, bool refresh) {} + +void InboxFeedRequest::onFeedLikeList(const bilibili::FeedLikeResultWrapper& result, bool refresh) {} + +void InboxFeedRequest::onError(const std::string& error) {} + +void InboxFeedRequest::requestData(MsgFeedMode mode, bool refresh) { + if (this->cursor.is_end && !refresh) return; + if (refresh) this->cursor = bilibili::MsgFeedCursor{}; + switch (mode) { + case MsgFeedMode::REPLY: { + ASYNC_RETAIN + BILI::msg_feed_reply( + this->cursor, + [ASYNC_TOKEN, refresh](const bilibili::FeedReplyResultWrapper& result) { + brls::sync([ASYNC_TOKEN, result, refresh]() { + ASYNC_RELEASE + this->cursor = result.cursor; + this->onFeedReplyList(result, refresh); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); + break; + } + case MsgFeedMode::AT: { + ASYNC_RETAIN + BILI::msg_feed_at( + this->cursor, + [ASYNC_TOKEN, refresh](const bilibili::FeedAtResultWrapper& result) { + brls::sync([ASYNC_TOKEN, result, refresh]() { + ASYNC_RELEASE + this->cursor = result.cursor; + this->onFeedAtList(result, refresh); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); + break; + } + case MsgFeedMode::LIKE: { + ASYNC_RETAIN + BILI::msg_feed_like( + this->cursor, + [ASYNC_TOKEN, refresh](const bilibili::FeedLikeResultWrapper& result) { + brls::sync([ASYNC_TOKEN, result, refresh]() { + ASYNC_RELEASE + this->cursor = result.total.cursor; + this->onFeedLikeList(result, refresh); + }); + }, + [ASYNC_TOKEN](BILI_ERR) { + brls::sync([ASYNC_TOKEN, error]() { + ASYNC_RELEASE + this->onError(error); + }); + }); + break; + } + default:; + } +} \ No newline at end of file diff --git a/wiliwili/source/presenter/inbox_msg.cpp b/wiliwili/source/presenter/inbox_msg.cpp new file mode 100644 index 00000000..18ce6634 --- /dev/null +++ b/wiliwili/source/presenter/inbox_msg.cpp @@ -0,0 +1,58 @@ +#include "bilibili.h" +#include "presenter/inbox_msg.hpp" +#include "utils/config_helper.hpp" + +void InboxMsgRequest::onMsgList(const bilibili::InboxMessageResultWrapper& result, bool refresh) {} + +void InboxMsgRequest::onSendMsg(const bilibili::InboxSendResult& result) {} + +void InboxMsgRequest::onError(const std::string& error) {} + +void InboxMsgRequest::requestData(bool refresh, int session_type, size_t size) { + CHECK_AND_SET_REQUEST + ASYNC_RETAIN + BILI::fetch_inbox_msgs( + std::to_string(this->talkerId), size, session_type, refresh ? "" : std::to_string(this->msgSeq), + [ASYNC_TOKEN, refresh, session_type](const bilibili::InboxMessageResultWrapper& result) { + ASYNC_RELEASE + this->onMsgList(result, refresh); + if (this->msgSeq != result.max_seqno) { + this->msgSeq = result.max_seqno; + this->updateAck(session_type); + } + UNSET_REQUEST + }, + [ASYNC_TOKEN](BILI_ERR) { + ASYNC_RELEASE + this->onError(error); + UNSET_REQUEST + }); +} + +void InboxMsgRequest::setTalkerId(uint64_t mid) { this->talkerId = mid; } + +void InboxMsgRequest::setMsgSeq(uint64_t seq) { this->msgSeq = seq; } + +void InboxMsgRequest::sendMsg(const std::string& text) { + CHECK_AND_SET_REQUEST + ASYNC_RETAIN + std::string csrf = ProgramConfig::instance().getCSRF(); + nlohmann::json content = {{"content", text}}; + BILI::send_inbox_msg( + ProgramConfig::instance().getUserID(), std::to_string(this->talkerId), content.dump(), csrf, + [ASYNC_TOKEN](const bilibili::InboxSendResult& result) { + ASYNC_RELEASE + this->onSendMsg(result); + UNSET_REQUEST + }, + [ASYNC_TOKEN](BILI_ERR) { + ASYNC_RELEASE + this->onError(error); + UNSET_REQUEST + }); +} + +void InboxMsgRequest::updateAck(int session_type) { + BILI::update_inbox_ack( + std::to_string(this->talkerId), session_type, std::to_string(this->msgSeq), ProgramConfig::instance().getCSRF()); +} \ No newline at end of file diff --git a/wiliwili/source/utils/activity_helper.cpp b/wiliwili/source/utils/activity_helper.cpp index 317ed91e..eb4d90bb 100644 --- a/wiliwili/source/utils/activity_helper.cpp +++ b/wiliwili/source/utils/activity_helper.cpp @@ -13,7 +13,9 @@ #include "activity/main_activity.hpp" #include "activity/gallery_activity.hpp" #include "activity/dlna_activity.hpp" +#include "activity/dynamic_activity.hpp" #include "fragment/mine_collection_video_list.hpp" +#include "fragment/inbox_view.hpp" #include "utils/activity_helper.hpp" #include "utils/config_helper.hpp" @@ -76,6 +78,11 @@ void Intent::openSetting() { registerFullscreen(activity); } +void Intent::openInbox() { + auto inbox = new InboxView(); + brls::Application::pushActivity(new brls::Activity(inbox)); +} + void Intent::openHint() { brls::Application::pushActivity(new HintActivity()); } void Intent::openMain() { @@ -96,6 +103,12 @@ void Intent::openDLNA() { registerFullscreen(activity); } +void Intent::openActivity(const std::string& id) { + auto activity = new DynamicActivity(id); + brls::Application::pushActivity(activity, brls::TransitionAnimation::NONE); + registerFullscreen(activity); +} + void Intent::_registerFullscreen(brls::Activity* activity) { activity->registerAction("", brls::BUTTON_F, [](...) { ProgramConfig::instance().toggleFullscreen(); diff --git a/wiliwili/source/utils/register_helper.cpp b/wiliwili/source/utils/register_helper.cpp index c920e3a6..3af02354 100644 --- a/wiliwili/source/utils/register_helper.cpp +++ b/wiliwili/source/utils/register_helper.cpp @@ -19,6 +19,7 @@ #include "fragment/mine_collection.hpp" #include "fragment/mine_collection_video_list.hpp" #include "fragment/mine_bangumi.hpp" +#include "fragment/inbox_feed.hpp" #include "fragment/search_tab.hpp" #include "fragment/search_order.hpp" #include "fragment/search_video.hpp" @@ -97,6 +98,7 @@ void Register::initCustomView() { brls::Application::registerXMLView("MineCollection", MineCollection::create); brls::Application::registerXMLView("MineCollectionVideoList", MineCollectionVideoList::create); brls::Application::registerXMLView("MineBangumi", MineBangumi::create); + brls::Application::registerXMLView("InboxFeed", InboxFeed::create); brls::Application::registerXMLView("SearchTab", SearchTab::create); brls::Application::registerXMLView("SearchOrder", SearchOrder::create); brls::Application::registerXMLView("SearchVideo", SearchVideo::create); diff --git a/wiliwili/source/view/dynamic_article.cpp b/wiliwili/source/view/dynamic_article.cpp index ddd03536..6e98dedb 100644 --- a/wiliwili/source/view/dynamic_article.cpp +++ b/wiliwili/source/view/dynamic_article.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "view/dynamic_article.hpp" @@ -100,8 +101,8 @@ class DynamicCommentAction : public brls::Box { this->article->setLiked(state.like.like_state); this->backgroundBox->setWidth(width + 48); this->live.room_id = 0; - this->video.epid = 0; - this->video.bvid = ""; + this->video.epid = 0; + this->video.bvid = ""; for (auto& module : data.modules) { auto moduleType = (bilibili::DynamicArticleModuleType)module.data.index(); @@ -388,80 +389,94 @@ class DataSourceDynamicDetailList : public RecyclingGridDataSource, public Comme std::function switchModeCallback = nullptr; }; -class DynamicArticleDetail : public brls::Box, public CommentRequest { -public: - explicit DynamicArticleDetail(const bilibili::DynamicArticleResult& data, - const bilibili::DynamicArticleModuleState& state) - : data(data), state(state) { - this->inflateFromXMLRes("xml/fragment/dynamic_detail.xml"); - - this->recyclingGrid->registerCell("Cell", []() { return DynamicArticleView::create(); }); - - this->recyclingGrid->registerCell("Reply", []() { return VideoCommentReply::create(); }); - - this->recyclingGrid->registerCell("Sort", []() { return VideoCommentSort::create(); }); - - this->recyclingGrid->registerCell("Hint", []() { return GridHintView::create(); }); - - this->recyclingGrid->registerCell("Comment", []() { return VideoComment::create(); }); +void DynamicArticleDetail::initList(const bilibili::DynamicArticleResult& result, + const bilibili::DynamicArticleModuleState& moduleState) { + data = result; + state = moduleState; + this->recyclingGrid->registerCell("Cell", []() { return DynamicArticleView::create(); }); + this->recyclingGrid->registerCell("Reply", []() { return VideoCommentReply::create(); }); + this->recyclingGrid->registerCell("Sort", []() { return VideoCommentSort::create(); }); + this->recyclingGrid->registerCell("Hint", []() { return GridHintView::create(); }); + this->recyclingGrid->registerCell("Comment", []() { return VideoComment::create(); }); + + this->recyclingGrid->registerAction("wiliwili/home/common/switch"_i18n, brls::ControllerButton::BUTTON_X, + [this](brls::View* view) -> bool { + this->toggleCommentMode(); + return true; + }); + this->recyclingGrid->setDataSource(new DataSourceDynamicDetailList(data, state, &likeStateEvent, &likeNumEvent, + this->getVideoCommentMode(), + [this]() { this->toggleCommentMode(); })); + this->recyclingGrid->onNextPage([this]() { + this->requestVideoComment(this->state.comment.comment_id, -1, -1, this->state.comment.comment_type); + }); +} - this->recyclingGrid->setDataSource(new DataSourceDynamicDetailList(data, state, &likeStateEvent, &likeNumEvent, - this->getVideoCommentMode(), - [this]() { this->toggleCommentMode(); })); +DynamicArticleDetail::DynamicArticleDetail(const std::string& id) { + this->inflateFromXMLRes("xml/fragment/dynamic_detail.xml"); + requestDynamicArticle(id); + this->buttonClose->setFocusable(true); + this->buttonClose->registerClickAction([](...){ + brls::Application::popActivity(brls::TransitionAnimation::NONE); + return true; + }); + brls::Application::giveFocus(this->buttonClose); +} - this->recyclingGrid->registerAction("wiliwili/home/common/switch"_i18n, brls::ControllerButton::BUTTON_X, - [this](brls::View* view) -> bool { - this->toggleCommentMode(); - return true; - }); +DynamicArticleDetail::DynamicArticleDetail(const bilibili::DynamicArticleResult& data, + const bilibili::DynamicArticleModuleState& state) { + this->inflateFromXMLRes("xml/fragment/dynamic_detail.xml"); + initList(data, state); +} - this->recyclingGrid->onNextPage([this]() { - this->requestVideoComment(this->state.comment.comment_id, -1, -1, this->state.comment.comment_type); - }); +void DynamicArticleDetail::onDynamicArticle(const bilibili::DynamicArticleResult& result) { + for (auto& j : result.modules) { + if ((bilibili::DynamicArticleModuleType)j.data.index() != bilibili::DynamicArticleModuleType::MODULE_TYPE_STAT) + continue; + auto* dataState = std::get_if(&j.data); + if (!dataState) break; + state = *dataState; + break; } + initList(result, state); + this->buttonClose->setFocusable(false); + if (brls::Application::getCurrentFocus() == this->buttonClose) + brls::Application::giveFocus(this->recyclingGrid); +} - void onCommentInfo(const bilibili::VideoCommentResultWrapper& result) override { - auto* datasource = dynamic_cast(recyclingGrid->getDataSource()); - if (!datasource) return; - if (result.requestIndex == 0) { - // 第一页评论 - //整合置顶评论 - std::vector comments(result.top_replies); - comments.insert(comments.end(), result.replies.begin(), result.replies.end()); - datasource->appendData(comments); - } else { - // 第N页评论 - if (!result.replies.empty()) { - datasource->appendData(result.replies); - } - } - if (result.cursor.is_end) { - bilibili::VideoCommentResult bottom; - bottom.rpid = 1; - datasource->appendData({bottom}); +void DynamicArticleDetail::onCommentInfo(const bilibili::VideoCommentResultWrapper& result) { + auto* datasource = dynamic_cast(recyclingGrid->getDataSource()); + if (!datasource) return; + if (result.requestIndex == 0) { + // 第一页评论 + //整合置顶评论 + std::vector comments(result.top_replies); + comments.insert(comments.end(), result.replies.begin(), result.replies.end()); + datasource->appendData(comments); + } else { + // 第N页评论 + if (!result.replies.empty()) { + datasource->appendData(result.replies); } - recyclingGrid->notifyDataChanged(); } - - void toggleCommentMode() { - // 更新请求模式 - int newMode = this->getVideoCommentMode() == 3 ? 2 : 3; - this->setVideoCommentMode(newMode); - // 清空评论列表 - this->recyclingGrid->setDataSource(new DataSourceDynamicDetailList( - data, state, &likeStateEvent, &likeNumEvent, newMode, [this]() { this->toggleCommentMode(); })); - // 焦点切换到动态 - brls::Application::giveFocus(this->recyclingGrid); + if (result.cursor.is_end) { + bilibili::VideoCommentResult bottom; + bottom.rpid = 1; + datasource->appendData({bottom}); } + recyclingGrid->notifyDataChanged(); +} - brls::Event likeStateEvent; - brls::Event likeNumEvent; - -private: - BRLS_BIND(RecyclingGrid, recyclingGrid, "dynamic/detail/grid"); - bilibili::DynamicArticleResult data; - bilibili::DynamicArticleModuleState state; -}; +void DynamicArticleDetail::toggleCommentMode() { + // 更新请求模式 + int newMode = this->getVideoCommentMode() == 3 ? 2 : 3; + this->setVideoCommentMode(newMode); + // 清空评论列表 + this->recyclingGrid->setDataSource(new DataSourceDynamicDetailList( + data, state, &likeStateEvent, &likeNumEvent, newMode, [this]() { this->toggleCommentMode(); })); + // 焦点切换到动态 + brls::Application::giveFocus(this->recyclingGrid); +} DynamicArticleView::DynamicArticleView() { this->inflateFromXMLRes("xml/views/dynamic_card.xml"); diff --git a/wiliwili/source/view/inbox_msg_card.cpp b/wiliwili/source/view/inbox_msg_card.cpp new file mode 100644 index 00000000..2960c130 --- /dev/null +++ b/wiliwili/source/view/inbox_msg_card.cpp @@ -0,0 +1,146 @@ +#include "view/inbox_msg_card.hpp" +#include "view/text_box.hpp" +#include "utils/string_helper.hpp" + +using namespace brls::literals; + +InboxMsgCard::InboxMsgCard() { this->inflateFromXMLRes("xml/views/inbox_msg.xml"); } + +void InboxMsgCard::setCard(const bilibili::InboxMessageResult& r, const IEMap& m, uint64_t talker) { + RichTextData d; + auto theme = brls::Application::getTheme(); + auto textColor = theme.getColor("brls/text"); + + // 设置用户头像 + if (r.msg_type == 10) { + this->talker->setVisibility(brls::Visibility::GONE); + this->mine->setVisibility(brls::Visibility::GONE); + } else if (talker == r.sender_uid) { + this->talker->setVisibility(brls::Visibility::VISIBLE); + this->mine->setVisibility(brls::Visibility::INVISIBLE); + } else { + this->talker->setVisibility(brls::Visibility::INVISIBLE); + this->mine->setVisibility(brls::Visibility::VISIBLE); + } + + this->msgTime->setText(wiliwili::sec2FullDate(r.timestamp)); + + // 分享消息 + if (r.msg_type == 7) { + if (r.content.contains("title")) { + std::string title = r.content.at("title"); + this->shareMisc->setText(title); + } + if (r.content.contains("thumb")) { + std::string thumb = r.content.at("thumb"); + ImageHelper::with(this->shareThumb)->load(thumb); + } + if (r.content.contains("author")) { + std::string author = r.content.at("author"); + this->shareAuthor->setText(author); + } else if (r.content.contains("source_desc")) { + std::string desc = r.content.at("source_desc"); + this->shareAuthor->setText(desc); + } + this->shareBox->setVisibility(brls::Visibility::VISIBLE); + this->msgBox->setVisibility(brls::Visibility::GONE); + return; + } + + this->shareBox->setVisibility(brls::Visibility::GONE); + this->msgBox->setVisibility(brls::Visibility::VISIBLE); + + switch (r.msg_type) { + case 1: { // 文本消息 + if (!r.content.contains("content")) break; + std::string msg = r.content.at("content"); + size_t start = 0; + for (size_t i = 0; i < msg.length(); i++) { + InboxEmotePtr matched = nullptr; + size_t nextMatch = -1; + for (auto& key : m) { + size_t position = msg.find(key.first, i); + if (position < nextMatch) { + nextMatch = position; + matched = key.second; + break; + } + } + if (matched == nullptr) nextMatch = msg.length(); + if (start < nextMatch) { + // 纯文本 + std::string text = msg.substr(start, nextMatch - start); + d.push_back(std::make_shared(text, textColor)); + } + if (matched == nullptr) break; + + // 处理表情 + std::shared_ptr item; + if (matched->size == 2) { + item = std::make_shared(matched->url, 50, 50); + item->t_margin = 4; + } else { + item = std::make_shared(matched->url, 30, 30); + } + item->v_align = 4; + item->l_margin = 2; + item->r_margin = 2; + d.push_back(item); + + i = nextMatch + matched->text.length() - 1; + start = i + 1; + } + break; + } + case 2: { // 图片消息 + if (!r.content.contains("url")) break; + std::string pic = r.content.at("url"); + float width = r.content.at("width"); + float height = r.content.at("height"); + + if (width > 400.f) { + height = height * 400.f / width; + width = 400.f; + } + if (height > 400.f) { + width = width * 400.f / height; + height = 400.f; + } +#ifdef __PSV__ + std::string custom = + wiliwili::format(ImageHelper::note_custom_ext, (int)(width * 0.5f), (int)(height * 0.5f)); +#else + std::string custom = wiliwili::format(ImageHelper::note_custom_ext, (int)width, (int)height); +#endif + textBox->setLineHeight(1.0f); + d.push_back(std::make_shared(pic + custom, width, height)); + break; + } + case 10: { // 系统消息 + if (!r.content.contains("title")) break; + auto titleColor = theme.getColor("color/bilibili"); + std::string title = r.content.at("title"); + std::string text = r.content.at("text"); + d.push_back(std::make_shared(title, titleColor)); + // todo: 仔细研究一下开头的 \n 到底在什么情况下有效果 + if (!text.empty() && text[0] != '\n') + d.push_back(std::make_shared()); + d.push_back(std::make_shared(text, textColor)); + break; + } + default: { + auto fontGrey = theme.getColor("font/grey"); + d.push_back(std::make_shared("wiliwili/inbox/chat/unknown"_i18n, fontGrey)); + } + } + + this->textBox->setRichText(d); +} + +void InboxMsgCard::setAvatar(const std::string& face) { + ImageHelper::with(this->talker)->load(face + ImageHelper::face_ext); +} + +void InboxMsgCard::setTimeVisible(bool visible) { + this->msgTime->setVisibility(visible ? brls::Visibility::VISIBLE : brls::Visibility::GONE); +} \ No newline at end of file diff --git a/wiliwili/source/view/recycling_grid.cpp b/wiliwili/source/view/recycling_grid.cpp index 95c2702a..18b449c9 100644 --- a/wiliwili/source/view/recycling_grid.cpp +++ b/wiliwili/source/view/recycling_grid.cpp @@ -271,6 +271,11 @@ void RecyclingGrid::reloadData() { renderedFrame = brls::Rect(); renderedFrame.size.width = getWidth(); + if (renderedFrame.size.width != renderedFrame.size.width) { + // 当列表在展示骨架屏后被隐藏,这时获取到 width 的值为 NAN + // 使用历史宽度值避免后续计算错误 + renderedFrame.size.width = oldWidth; + } setContentOffsetY(0, false); if (dataSource == nullptr) return; @@ -422,6 +427,9 @@ void RecyclingGrid::itemsRecyclingLoop() { getHeightByCellIndex(visibleMax, visibleMax - preFetchLine * spanCount) <= visibleFrame.getMaxY())) break; + if (visibleMax == 0) { + break; + } float cellHeight = estimatedRowHeight; if (isFlowMode) cellHeight = cellHeightCache[visibleMax];