aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO3
-rw-r--r--include/FileAnalyzer.hpp2
-rw-r--r--plugins/Matrix.hpp35
-rw-r--r--plugins/Page.hpp2
-rw-r--r--src/FileAnalyzer.cpp5
-rw-r--r--src/QuickMedia.cpp35
-rw-r--r--src/plugins/Matrix.cpp184
7 files changed, 225 insertions, 41 deletions
diff --git a/TODO b/TODO
index 0d19b85..cb64535 100644
--- a/TODO
+++ b/TODO
@@ -159,4 +159,5 @@ Check what happens with xsrf_token if comments are not fetched for a long time.
Add support for comments in live youtube videos, api is at: https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8.
Make video visible when reading comments (youtube).
Convert nyaa.si/spotify/soundcloud date from ISO date string to local time.
-When ui is scaled then the predicated thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested. \ No newline at end of file
+When ui is scaled then the predicated thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested.
+Check if get_page handlers in pages need to check if next batch is valid. If the server returns empty next batch we shouldn't fetch the first page... \ No newline at end of file
diff --git a/include/FileAnalyzer.hpp b/include/FileAnalyzer.hpp
index be0cc25..92bd042 100644
--- a/include/FileAnalyzer.hpp
+++ b/include/FileAnalyzer.hpp
@@ -34,7 +34,7 @@ namespace QuickMedia {
bool is_content_type_image(ContentType content_type);
const char* content_type_to_string(ContentType content_type);
- bool video_get_first_frame(const char *filepath, const char *destination_path);
+ bool video_get_first_frame(const char *filepath, const char *destination_path, int width, int height);
class FileAnalyzer {
public:
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index b240cb8..eea7f9b 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -427,7 +427,7 @@ namespace QuickMedia {
class MatrixChatPage : public Page {
public:
MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page);
- ~MatrixChatPage() override;
+ ~MatrixChatPage();
const char* get_title() const override { return ""; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override {
@@ -445,6 +445,33 @@ namespace QuickMedia {
bool should_clear_data = false;
};
+ class MatrixRoomDirectoryPage : public Page {
+ public:
+ MatrixRoomDirectoryPage(Program *program, Matrix *matrix) : Page(program), matrix(matrix) {}
+ const char* get_title() const override { return "Room directory"; }
+ bool allow_submit_no_selection() const override { return true; }
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ private:
+ Matrix *matrix;
+ };
+
+ class MatrixServerRoomListPage : public LazyFetchPage {
+ public:
+ MatrixServerRoomListPage(Program *program, Matrix *matrix, const std::string &server_name) : LazyFetchPage(program), matrix(matrix), server_name(server_name), current_page(0) {}
+ const char* get_title() const override { return "Select a room to join"; }
+ bool search_is_filter() override { return false; }
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
+ SearchResult search(const std::string &str, BodyItems &result_items) override;
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ private:
+ Matrix *matrix;
+ const std::string server_name;
+ std::string next_batch;
+ std::string search_term;
+ int current_page;
+ };
+
class Matrix {
public:
void start_sync(MatrixDelegate *delegate, bool &cached);
@@ -486,6 +513,9 @@ namespace QuickMedia {
PluginResult join_room(const std::string &room_id);
PluginResult leave_room(const std::string &room_id);
+ // If |since| is empty, then the first page is fetched
+ PluginResult get_public_rooms(const std::string &server, const std::string &search_term, const std::string &since, BodyItems &rooms, std::string &next_batch);
+
// |message| is from |BodyItem.userdata| and is of type |Message*|
bool was_message_posted_by_me(void *message);
@@ -496,6 +526,8 @@ namespace QuickMedia {
std::shared_ptr<UserInfo> get_me(RoomData *room);
+ const std::string& get_homeserver_domain() const;
+
// Returns nullptr if message cant be found. Note: cached
std::shared_ptr<Message> get_message_by_id(RoomData *room, const std::string &event_id);
@@ -541,6 +573,7 @@ namespace QuickMedia {
std::string my_user_id;
std::string access_token;
std::string homeserver;
+ std::string homeserver_domain;
std::optional<int> upload_limit;
std::string next_batch;
std::mutex next_batch_mutex;
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 8ef2b59..b5af7a9 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -47,6 +47,8 @@ namespace QuickMedia {
virtual bool is_single_page() const { return false; }
virtual bool is_trackable() const { return false; }
virtual bool is_lazy_fetch_page() const { return false; }
+ // Note: If submit is done without any selection, then the search term is sent as the |title|, not |url|
+ virtual bool allow_submit_no_selection() const { return false; }
// This is called both when first navigating to page and when going back to page
virtual void on_navigate_to_page(Body *body) { (void)body; }
diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp
index b397def..adfb7cc 100644
--- a/src/FileAnalyzer.cpp
+++ b/src/FileAnalyzer.cpp
@@ -87,8 +87,9 @@ namespace QuickMedia {
return 0;
}
- bool video_get_first_frame(const char *filepath, const char *destination_path) {
- const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", destination_path, nullptr };
+ bool video_get_first_frame(const char *filepath, const char *destination_path, int width, int height) {
+ std::string thumbnail_size = std::to_string(width) + "x" + std::to_string(height);
+ const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", "-s", thumbnail_size.c_str(), destination_path, nullptr };
std::string ffmpeg_result;
if(exec_program(program_args, nullptr, nullptr) != 0) {
fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n");
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 95eb47a..92f9309 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -1096,16 +1096,16 @@ namespace QuickMedia {
window_size.x = window_size_u.x;
window_size.y = window_size_u.y;
- std::function<void()> submit_handler;
+ std::function<void(const std::string&)> submit_handler;
- submit_handler = [this, &submit_handler, &after_submit_handler, &json_chapters, &tabs, &tab_associated_data, &selected_tab, &loop_running, &redraw]() {
+ submit_handler = [this, &submit_handler, &after_submit_handler, &json_chapters, &tabs, &tab_associated_data, &selected_tab, &loop_running, &redraw](const std::string &search_text) {
auto selected_item = tabs[selected_tab].body->get_selected_shared();
- if(!selected_item)
+ if(!selected_item && !tabs[selected_tab].page->allow_submit_no_selection())
return;
std::vector<Tab> new_tabs;
tabs[selected_tab].page->submit_body_item = selected_item;
- PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs);
+ PluginResult submit_result = tabs[selected_tab].page->submit(selected_item ? selected_item->get_title() : search_text, selected_item ? selected_item->url : "", new_tabs);
if(submit_result != PluginResult::OK) {
// TODO: Show the exact cause of error (get error message from curl).
show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL);
@@ -1147,7 +1147,9 @@ namespace QuickMedia {
hide_virtual_keyboard();
- if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) {
+ if(tabs[selected_tab].page->allow_submit_no_selection()) {
+ page_loop(new_tabs, 0, after_submit_handler);
+ } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) {
select_episode(selected_item.get(), false);
Body *chapters_body = tabs[selected_tab].body.get();
chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter
@@ -1205,7 +1207,7 @@ namespace QuickMedia {
}
tabs[selected_tab].body->body_item_select_callback = [&submit_handler](BodyItem *body_item) {
- submit_handler();
+ submit_handler(body_item->get_title());
};
//select_body_item_by_room(tabs[selected_tab].body.get(), current_chat_room);
current_chat_room = nullptr;
@@ -1231,7 +1233,7 @@ namespace QuickMedia {
for(size_t i = 0; i < tabs.size(); ++i) {
Tab &tab = tabs[i];
tab.body->body_item_select_callback = [&submit_handler](BodyItem *body_item) {
- submit_handler();
+ submit_handler(body_item->get_title());
};
TabAssociatedData &associated_data = tab_associated_data[i];
@@ -1255,10 +1257,10 @@ namespace QuickMedia {
associated_data.typing = false;
};
- tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string&) {
+ tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string &search_text) {
if(associated_data.typing)
return;
- submit_handler();
+ submit_handler(search_text);
};
}
@@ -1355,7 +1357,10 @@ namespace QuickMedia {
} else if(event.key.code == sf::Keyboard::Tab) {
if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete();
} else if(event.key.code == sf::Keyboard::Enter) {
- if(!tabs[selected_tab].search_bar) submit_handler();
+ if(!tabs[selected_tab].search_bar) {
+ BodyItem *selected_item = tabs[selected_tab].body->get_selected();
+ submit_handler(selected_item ? selected_item->get_title() : "");
+ }
} else if(event.key.code == sf::Keyboard::T && event.key.control) {
BodyItem *selected_item = tabs[selected_tab].body->get_selected();
if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) {
@@ -5132,11 +5137,10 @@ namespace QuickMedia {
auto matrix_invites_page_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
auto matrix_invites_page = std::make_unique<MatrixInvitesPage>(this, matrix, invites_body.get(), matrix_invites_page_search_bar.get());
- //Tab options_tab = create_menu_selection_tab("Options",
- //{
- // { "Notifications", nullptr, 0, [](Program *program) { return std::make_unique<MatrixNotificationsPage>(program); } },
- // { "Room directory", "Search for search on...", SEARCH_DELAY_FILTER, [](Program *program) { return std::make_unique<MatrixRoomDirectoryPage>(program); } }
- //});
+ auto room_directory_body = create_body();
+ room_directory_body->items.push_back(BodyItem::create(matrix->get_homeserver_domain()));
+ room_directory_body->items.push_back(BodyItem::create("matrix.org"));
+ auto matrix_room_directory_page = std::make_unique<MatrixRoomDirectoryPage>(this, matrix);
MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get());
bool sync_cached = false;
@@ -5147,6 +5151,7 @@ namespace QuickMedia {
tabs.push_back(Tab{std::move(rooms_body), std::move(matrix_rooms_page), std::move(matrix_rooms_page_search_bar)});
tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), std::move(matrix_rooms_tage_page_search_bar)});
tabs.push_back(Tab{std::move(invites_body), std::move(matrix_invites_page), std::move(matrix_invites_page_search_bar)});
+ tabs.push_back(Tab{std::move(room_directory_body), std::move(matrix_room_directory_page), create_search_bar("Server to search on...", SEARCH_DELAY_FILTER)});
while(window.isOpen()) {
page_loop(tabs);
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 128e610..5d942ed 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -18,7 +18,7 @@
#include <unistd.h>
#include "../../include/QuickMedia.hpp"
-// TODO: Update avatar/display name when its changed in the room/globally.
+// TODO: Use string assign with string length instead of assigning to c string (which calls strlen)
// Show images/videos inline.
// TODO: Verify if buffer of size 512 is enough for endpoints
// Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one.
@@ -72,6 +72,8 @@ static std::string extract_first_line_elipses(const std::string &str, size_t max
}
namespace QuickMedia {
+ static const sf::Vector2i thumbnail_max_size(600, 337);
+
static void remove_body_item_by_url(BodyItems &body_items, const std::string &url) {
for(auto it = body_items.begin(); it != body_items.end();) {
if((*it)->url == url)
@@ -980,6 +982,46 @@ namespace QuickMedia {
rooms_page->update();
}
+ PluginResult MatrixRoomDirectoryPage::submit(const std::string &title, const std::string&, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{create_body(), std::make_unique<MatrixServerRoomListPage>(program, matrix, title), create_search_bar("Search...", 350)});
+ return PluginResult::OK;
+ }
+
+ PluginResult MatrixServerRoomListPage::lazy_fetch(BodyItems &result_items) {
+ return matrix->get_public_rooms(server_name, search_term, next_batch, result_items, next_batch);
+ }
+
+ PluginResult MatrixServerRoomListPage::get_page(const std::string&, int page, BodyItems &result_items) {
+ while(current_page < page && !next_batch.empty()) {
+ PluginResult plugin_result = lazy_fetch(result_items);
+ if(plugin_result != PluginResult::OK) return plugin_result;
+ ++current_page;
+ }
+ return PluginResult::OK;
+ }
+
+ SearchResult MatrixServerRoomListPage::search(const std::string &str, BodyItems &result_items) {
+ next_batch.clear();
+ current_page = 0;
+ search_term = str;
+ return plugin_result_to_search_result(lazy_fetch(result_items));
+ }
+
+ PluginResult MatrixServerRoomListPage::submit(const std::string &title, const std::string &url, std::vector<Tab>&) {
+ TaskResult task_result = program->run_task_with_loading_screen([this, url]() {
+ return matrix->join_room(url) == PluginResult::OK;
+ });
+
+ if(task_result == TaskResult::TRUE) {
+ show_notification("QuickMedia", "You joined " + title, Urgency::NORMAL);
+ program->set_go_to_previous_page();
+ } else if(task_result == TaskResult::FALSE) {
+ show_notification("QuickMedia", "Failed to join " + title, Urgency::CRITICAL);
+ }
+
+ return PluginResult::OK;
+ }
+
static std::array<const char*, 7> sync_fail_error_codes = {
"M_FORBIDDEN",
"M_UNKNOWN_TOKEN",
@@ -998,18 +1040,6 @@ namespace QuickMedia {
}
}
- static void remove_ephemeral_field_in_sync_rooms_response(rapidjson::Value &rooms_json) {
- auto join_it = rooms_json.FindMember("join");
- if(join_it == rooms_json.MemberEnd() || !join_it->value.IsObject())
- return;
-
- for(auto &it : join_it->value.GetObject()) {
- if(!it.value.IsObject())
- continue;
- it.value.RemoveMember("ephemeral");
- }
- }
-
static void remove_empty_fields_in_sync_account_data_response(rapidjson::Value &account_data_json) {
for(const char *member_name : {"events"}) {
auto join_it = account_data_json.FindMember(member_name);
@@ -2416,6 +2446,7 @@ namespace QuickMedia {
std::string room_id_str(room_id.GetString(), room_id.GetStringLength());
if(set_invite(room_id_str, invite))
delegate->add_invite(room_id_str, std::move(invite));
+
break;
}
}
@@ -3168,7 +3199,7 @@ namespace QuickMedia {
char tmp_filename[] = "/tmp/quickmedia_video_frame_XXXXXX";
int tmp_file = mkstemp(tmp_filename);
if(tmp_file != -1) {
- if(video_get_first_frame(filepath.c_str(), tmp_filename)) {
+ if(video_get_first_frame(filepath.c_str(), tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) {
UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails.
PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, thumbnail_info, upload_info_ignored, err_msg, false);
if(upload_thumbnail_result != PluginResult::OK) {
@@ -3189,7 +3220,7 @@ namespace QuickMedia {
int tmp_file = mkstemp(tmp_filename);
if(tmp_file != -1) {
std::string thumbnail_path;
- if(create_thumbnail(filepath, tmp_filename, sf::Vector2i(600, 337)))
+ if(create_thumbnail(filepath, tmp_filename, thumbnail_max_size))
thumbnail_path = tmp_filename;
else
thumbnail_path = filepath;
@@ -3306,6 +3337,10 @@ namespace QuickMedia {
return PluginResult::ERR;
}
+ const rapidjson::Value &home_server_json = GetMember(json_root, "home_server");
+ if(home_server_json.IsString())
+ this->homeserver_domain = home_server_json.GetString();
+
// Use the user-provided homeserver instead of the one the server tells us about, otherwise this wont work with a proxy
// such as pantalaimon
json_root.AddMember("homeserver", rapidjson::StringRef(homeserver.c_str()), request_data.GetAllocator());
@@ -3348,6 +3383,7 @@ namespace QuickMedia {
my_user_id.clear();
access_token.clear();
homeserver.clear();
+ homeserver_domain.clear();
upload_limit.reset();
set_next_batch("");
invites.clear();
@@ -3440,13 +3476,13 @@ namespace QuickMedia {
return PluginResult::ERR;
}
- std::string user_id = user_id_json.GetString();
- std::string access_token = access_token_json.GetString();
- std::string homeserver = homeserver_json.GetString();
+ const rapidjson::Value &home_server_json = GetMember(json_root, "home_server");
+ if(home_server_json.IsString())
+ this->homeserver_domain = home_server_json.GetString();
- this->my_user_id = std::move(user_id);
- this->access_token = std::move(access_token);
- this->homeserver = std::move(homeserver);
+ this->my_user_id = user_id_json.GetString();
+ this->access_token = access_token_json.GetString();
+ this->homeserver = homeserver_json.GetString();
return PluginResult::OK;
}
@@ -3600,6 +3636,108 @@ namespace QuickMedia {
return download_result_to_plugin_result(download_result);
}
+ PluginResult Matrix::get_public_rooms(const std::string &server, const std::string &search_term, const std::string &since, BodyItems &rooms, std::string &next_batch) {
+ rapidjson::Document filter_data(rapidjson::kObjectType);
+ if(!search_term.empty())
+ filter_data.AddMember("generic_search_term", rapidjson::StringRef(search_term.c_str()), filter_data.GetAllocator());
+
+ rapidjson::Document request_data(rapidjson::kObjectType);
+ request_data.AddMember("limit", 20, request_data.GetAllocator());
+
+ if(!search_term.empty())
+ request_data.AddMember("filter", std::move(filter_data), request_data.GetAllocator());
+
+ if(!since.empty())
+ request_data.AddMember("since", rapidjson::StringRef(since.c_str()), request_data.GetAllocator());
+
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ request_data.Accept(writer);
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "POST" },
+ { "-H", "content-type: application/json" },
+ { "-H", "Authorization: Bearer " + access_token },
+ { "--data-binary", buffer.GetString() }
+ };
+
+ std::string url = homeserver + "/_matrix/client/r0/publicRooms?server=";
+ url += url_param_encode(server);
+
+ rapidjson::Document json_root;
+ DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true);
+ if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result);
+
+ if(!json_root.IsObject())
+ return PluginResult::ERR;
+
+ const rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch");
+ if(next_batch_json.IsString())
+ next_batch = next_batch_json.GetString();
+ else
+ next_batch.clear();
+
+ const rapidjson::Value &chunk_json = GetMember(json_root, "chunk");
+ if(chunk_json.IsArray()) {
+ for(const rapidjson::Value &chunk_item_json : chunk_json.GetArray()) {
+ if(!chunk_item_json.IsObject())
+ continue;
+
+ const rapidjson::Value &room_id_json = GetMember(chunk_item_json, "room_id");
+ if(!room_id_json.IsString())
+ continue;
+
+ std::string room_name;
+ const rapidjson::Value &name_json = GetMember(chunk_item_json, "name");
+ if(name_json.IsString())
+ room_name = name_json.GetString();
+ else
+ room_name = room_id_json.GetString();
+
+ auto room_body_item = BodyItem::create(std::move(room_name));
+ room_body_item->url = room_id_json.GetString();
+ std::string description;
+
+ const rapidjson::Value &topic_json = GetMember(chunk_item_json, "topic");
+ if(topic_json.IsString())
+ description = strip(topic_json.GetString());
+
+ const rapidjson::Value &canonical_alias_json = GetMember(chunk_item_json, "canonical_alias");
+ if(canonical_alias_json.IsString()) {
+ if(!description.empty())
+ description += '\n';
+ description += canonical_alias_json.GetString();
+ }
+
+ const rapidjson::Value &num_joined_members_json = GetMember(chunk_item_json, "num_joined_members");
+ if(num_joined_members_json.IsInt()) {
+ if(!description.empty())
+ description += '\n';
+ description += "👤" + std::to_string(num_joined_members_json.GetInt());
+ }
+
+ room_body_item->set_description(std::move(description));
+
+ const rapidjson::Value &avatar_url_json = GetMember(chunk_item_json, "avatar_url");
+ if(avatar_url_json.IsString()) {
+ std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString());
+ if(!avatar_url.empty())
+ avatar_url = get_thumbnail_url(homeserver, avatar_url);
+
+ if(!avatar_url.empty()) {
+ room_body_item->thumbnail_url = std::move(avatar_url);
+ room_body_item->thumbnail_size = sf::Vector2i(32 * get_ui_scale(), 32 * get_ui_scale());
+ room_body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
+ }
+ }
+
+ rooms.push_back(std::move(room_body_item));
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
bool Matrix::was_message_posted_by_me(void *message) {
Message *message_typed = (Message*)message;
return my_user_id == message_typed->user->user_id;
@@ -3645,6 +3783,10 @@ namespace QuickMedia {
return get_user_by_id(room, my_user_id);
}
+ const std::string& Matrix::get_homeserver_domain() const {
+ return homeserver_domain;
+ }
+
RoomData* Matrix::get_room_by_id(const std::string &id) {
std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
auto room_it = room_data_by_id.find(id);