aboutsummaryrefslogtreecommitdiff
path: root/src/QuickMedia.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/QuickMedia.cpp')
-rw-r--r--src/QuickMedia.cpp442
1 files changed, 388 insertions, 54 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 956370f..1d0518f 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -7,6 +7,7 @@
#include "../plugins/Fourchan.hpp"
#include "../plugins/Dmenu.hpp"
#include "../plugins/NyaaSi.hpp"
+#include "../plugins/Matrix.hpp"
#include "../include/Scale.hpp"
#include "../include/Program.h"
#include "../include/VideoPlayer.hpp"
@@ -41,15 +42,15 @@ static const sf::Color tab_selected_color(0, 85, 119);
static const sf::Color tab_unselected_color(43, 45, 47);
// Prevent writing to broken pipe from exiting the program
-static void sigpipe_handler(int unused) {
+static void sigpipe_handler(int) {
}
-static int x_error_handler(Display *display, XErrorEvent *event) {
+static int x_error_handler(Display*, XErrorEvent*) {
return 0;
}
-static int x_io_error_handler(Display *display) {
+static int x_io_error_handler(Display*) {
return 0;
}
@@ -275,7 +276,7 @@ namespace QuickMedia {
current_plugin = nullptr;
std::string plugin_logo_path;
- std::string search_placeholder = "Search...";
+ std::string search_placeholder;
for(int i = 1; i < argc; ++i) {
if(!current_plugin) {
@@ -302,6 +303,9 @@ namespace QuickMedia {
plugin_logo_path = resources_root + "images/nyaa_si_logo.png";
} else if(strcmp(argv[i], "dmenu") == 0) {
current_plugin = new Dmenu();
+ } else if(strcmp(argv[i], "matrix") == 0) {
+ current_plugin = new Matrix();
+ plugin_logo_path = resources_root + "images/matrix_logo.png";
} else {
fprintf(stderr, "Invalid plugin %s\n", argv[i]);
usage();
@@ -327,6 +331,12 @@ namespace QuickMedia {
}
}
+ if(!search_placeholder.empty() && current_plugin->name == "dmenu") {
+ fprintf(stderr, "Option -p is only valid with dmenu\n");
+ usage();
+ return -1;
+ }
+
if(use_tor && !is_program_executable_by_name("torsocks")) {
fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n");
return -2;
@@ -389,6 +399,20 @@ namespace QuickMedia {
plugin_logo.setSmooth(true);
}
+ if(current_plugin->name == "matrix") {
+ Matrix *matrix = static_cast<Matrix*>(current_plugin);
+ if(matrix->load_and_verify_cached_session() == PluginResult::OK) {
+ current_page = Page::CHAT;
+ } else {
+ fprintf(stderr, "Failed to load session cache, redirecting to login page\n");
+ current_page = Page::CHAT_LOGIN;
+ }
+ search_placeholder = "Send a message...";
+ }
+
+ if(search_placeholder.empty())
+ search_placeholder = "Search...";
+
search_bar = std::make_unique<SearchBar>(font, &plugin_logo, search_placeholder);
search_bar->text_autosearch_delay = current_plugin->get_search_delay();
@@ -459,10 +483,15 @@ namespace QuickMedia {
body->clear_thumbnails();
break;
}
- default:
- fprintf(stderr, "Page not implemented: %d\n", current_page);
- window.close();
+ case Page::CHAT_LOGIN: {
+ chat_login_page();
+ break;
+ }
+ case Page::CHAT: {
+ body->draw_thumbnails = true;
+ chat_page();
break;
+ }
}
}
@@ -490,9 +519,9 @@ namespace QuickMedia {
search_bar->clear();
}
}
- } else if(handle_searchbar && event.type == sf::Event::TextEntered) {
- search_bar->onTextEntered(event.text.unicode);
} else if(handle_searchbar) {
+ if(event.type == sf::Event::TextEntered)
+ search_bar->onTextEntered(event.text.unicode);
search_bar->on_event(event);
}
}
@@ -505,41 +534,6 @@ namespace QuickMedia {
return base64_url::decode<std::string>(data);
}
- static bool read_file_as_json(const Path &filepath, Json::Value &result) {
- std::string file_content;
- if(file_get_content(filepath, file_content) != 0) {
- fprintf(stderr, "Failed to get content of file: %s\n", filepath.data.c_str());
- return false;
- }
-
- Json::CharReaderBuilder json_builder;
- std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
- std::string json_errors;
- if(!json_reader->parse(file_content.data(), file_content.data() + file_content.size(), &result, &json_errors)) {
- fprintf(stderr, "Failed to read file %s as json, error: %s\n", filepath.data.c_str(), json_errors.c_str());
- return false;
- }
-
- return true;
- }
-
- static bool save_json_to_file_atomic(const Path &path, const Json::Value &json) {
- Path tmp_path = path;
- tmp_path.append(".tmp");
-
- Json::StreamWriterBuilder json_builder;
- if(file_overwrite(tmp_path, Json::writeString(json_builder, json)) != 0)
- return false;
-
- // Rename is atomic under posix!
- if(rename(tmp_path.data.c_str(), path.data.c_str()) != 0) {
- perror("save_json_to_file_atomic rename");
- return false;
- }
-
- return true;
- }
-
enum class SearchSuggestionTab {
ALL,
HISTORY,
@@ -861,7 +855,8 @@ namespace QuickMedia {
} else if(next_page == Page::VIDEO_CONTENT) {
watched_videos.clear();
if(content_url.empty())
- next_page = Page::SEARCH_RESULT;
+ //next_page = Page::SEARCH_RESULT;
+ next_page = Page::SEARCH_SUGGESTION;
else {
page_stack.push(Page::SEARCH_SUGGESTION);
}
@@ -902,7 +897,7 @@ namespace QuickMedia {
std::vector<Tab> tabs;
int selected_tab = 0;
- auto login_submit_callback = [this, &tabs, &selected_tab](const std::string &text) -> bool {
+ auto login_submit_callback = [this, &tabs, &selected_tab](const std::string&) -> bool {
if(!tabs[selected_tab].body) {
std::string username = tabs[selected_tab].login_tab->username->get_text();
std::string password = tabs[selected_tab].login_tab->password->get_text();
@@ -970,7 +965,7 @@ namespace QuickMedia {
typing = false;
};
- search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string &text) -> bool {
+ search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string&) -> bool {
if(current_plugin->name != "dmenu") {
if(typing || tabs[selected_tab].body->no_items_visible())
return false;
@@ -988,7 +983,11 @@ namespace QuickMedia {
});
} else {
*/
- PluginResult front_page_result = current_plugin->get_front_page(body->items);
+ if(current_plugin->get_front_page(body->items) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to get front page", Urgency::CRITICAL);
+ current_page = Page::EXIT;
+ return;
+ }
body->clamp_selection();
/*}*/
@@ -1556,7 +1555,10 @@ namespace QuickMedia {
}
if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, KeyPress, &xev)/* && xev.xkey.subwindow == video_player_window*/) {
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
KeySym pressed_keysym = XKeycodeToKeysym(disp, xev.xkey.keycode, 0);
+ #pragma GCC diagnostic pop
bool pressing_ctrl = (CLEANMASK(xev.xkey.state) == ControlMask);
if(pressed_keysym == XK_Escape) {
current_page = previous_page;
@@ -1722,7 +1724,7 @@ namespace QuickMedia {
tabs[selected_tab].body->clamp_selection();
};
- search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string &text) -> bool {
+ search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string&) -> bool {
if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) {
BodyItem *selected_item = body->get_selected();
if(!selected_item)
@@ -1741,7 +1743,7 @@ namespace QuickMedia {
}
};
- auto download_create_page = [manga](std::string url) {
+ auto download_creator_page = [manga](std::string url) {
BodyItems body_items;
if(manga->get_creators_manga_list(url, body_items) != PluginResult::OK)
show_notification("Manga", "Failed to download authors page", Urgency::CRITICAL);
@@ -1762,7 +1764,7 @@ namespace QuickMedia {
tab.body = new Body(this, &font, &bold_font);
tab.body->draw_thumbnails = true;
tab.creator = &creator;
- tab.creator_page_download_future = std::async(std::launch::async, download_create_page, creator.url);
+ tab.creator_page_download_future = std::async(std::launch::async, download_creator_page, creator.url);
tab.text = sf::Text(creator.name, font, tab_text_size);
tabs.push_back(std::move(tab));
}
@@ -2323,7 +2325,7 @@ namespace QuickMedia {
}
};
- search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool {
+ search_bar->onTextSubmitCallback = [this](const std::string&) -> bool {
BodyItem *selected_item = body->get_selected();
if(!selected_item)
return false;
@@ -2400,7 +2402,7 @@ namespace QuickMedia {
// TODO: Have an option for the search bar to be multi-line.
search_bar->onTextUpdateCallback = nullptr;
- search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool {
+ search_bar->onTextSubmitCallback = [this](const std::string&) -> bool {
if(current_plugin->name == "nyaa.si") {
BodyItem *selected_item = body->get_selected();
if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) {
@@ -2456,7 +2458,7 @@ namespace QuickMedia {
body->select_first_item();
};
- search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool {
+ search_bar->onTextSubmitCallback = [this](const std::string&) -> bool {
BodyItem *selected_item = body->get_selected();
if(!selected_item)
return false;
@@ -2922,4 +2924,336 @@ namespace QuickMedia {
// so you dont have to retype a post that was in the middle of being posted when returning.
search_bar->clear();
}
+
+ // TODO: Provide a way to logout
+ void Program::chat_login_page() {
+ assert(current_plugin->name == "matrix");
+
+ SearchBar login_input(font, nullptr, "Username");
+ SearchBar password_input(font, nullptr, "Password", true);
+ SearchBar homeserver_input(font, nullptr, "Homeserver");
+
+ sf::Text status_text("", font, 18);
+
+ const int num_inputs = 3;
+ SearchBar *inputs[num_inputs] = { &login_input, &password_input, &homeserver_input };
+ int focused_input = 0;
+
+ auto text_submit_callback = [this, inputs, &status_text](const sf::String&) -> bool {
+ Matrix *matrix = static_cast<Matrix*>(current_plugin);
+ for(int i = 0; i < num_inputs; ++i) {
+ if(inputs[i]->get_text().empty()) {
+ status_text.setString("All fields need to be filled in");
+ return false;
+ }
+ }
+
+ std::string err_msg;
+ // TODO: Make asynchronous
+ if(matrix->login(inputs[0]->get_text(), inputs[1]->get_text(), inputs[2]->get_text(), err_msg) == PluginResult::OK) {
+ current_page = Page::CHAT;
+ } else {
+ status_text.setString("Failed to login, error: " + err_msg);
+ }
+ return false;
+ };
+
+ for(int i = 0; i < num_inputs; ++i) {
+ inputs[i]->caret_visible = false;
+ inputs[i]->onTextSubmitCallback = text_submit_callback;
+ }
+ inputs[focused_input]->caret_visible = true;
+
+ sf::Vector2f body_pos;
+ sf::Vector2f body_size;
+ bool redraw = true;
+ sf::Event event;
+
+ while (current_page == Page::CHAT_LOGIN) {
+ while (window.pollEvent(event)) {
+ base_event_handler(event, Page::EXIT, false, false, false);
+ if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) {
+ redraw = true;
+ } else if(event.type == sf::Event::TextEntered) {
+ inputs[focused_input]->onTextEntered(event.text.unicode);
+ } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) {
+ for(int i = 0; i < num_inputs; ++i) {
+ inputs[i]->caret_visible = false;
+ }
+ focused_input = (focused_input + 1) % num_inputs;
+ inputs[focused_input]->caret_visible = true;
+ }
+ inputs[focused_input]->on_event(event);
+ }
+
+ if(redraw) {
+ redraw = false;
+ search_bar->onWindowResize(window_size);
+ get_body_dimensions(window_size, search_bar.get(), body_pos, body_size);
+ }
+
+ window.clear(back_color);
+ body->draw(window, body_pos, body_size);
+ float y = 0.0f;
+ for(int i = 0; i < num_inputs; ++i) {
+ inputs[i]->set_vertical_position(y);
+ inputs[i]->update();
+ inputs[i]->draw(window);
+ y += inputs[i]->getBottomWithoutShadow();
+ }
+ status_text.setPosition(0.0f, y + 10.0f);
+ window.draw(status_text);
+ window.display();
+ }
+ }
+
+ enum class ChatTabType {
+ MESSAGES,
+ ROOMS
+ };
+
+ struct ChatTab {
+ ChatTabType type;
+ std::unique_ptr<Body> body;
+ std::future<BodyItems> future;
+ sf::Text text;
+ };
+
+ void Program::chat_page() {
+ assert(current_plugin->name == "matrix");
+ Matrix *matrix = static_cast<Matrix*>(current_plugin);
+
+ std::vector<ChatTab> tabs;
+ int selected_tab = 0;
+ size_t room_message_index = 0;
+
+ ChatTab messages_tab;
+ messages_tab.type = ChatTabType::MESSAGES;
+ messages_tab.body = std::make_unique<Body>(this, &font, &bold_font);
+ messages_tab.body->draw_thumbnails = true;
+ messages_tab.text = sf::Text("Messages", font, tab_text_size);
+ tabs.push_back(std::move(messages_tab));
+
+ ChatTab rooms_tab;
+ rooms_tab.type = ChatTabType::ROOMS;
+ rooms_tab.body = std::make_unique<Body>(this, &font, &bold_font);
+ rooms_tab.body->draw_thumbnails = true;
+ rooms_tab.text = sf::Text("Rooms", font, tab_text_size);
+ tabs.push_back(std::move(rooms_tab));
+
+ const int MESSAGES_TAB_INDEX = 0;
+ const int ROOMS_TAB_INDEX = 1;
+
+ tabs[MESSAGES_TAB_INDEX].body->clear_items();
+ /*
+ if(matrix->get_cached_sync(tabs[MESSAGES_TAB_INDEX].body->items) != PluginResult::OK) {
+ fprintf(stderr, "Failed to get matrix cached sync\n");
+ } else {
+ fprintf(stderr, "Loaded matrix sync from cache, num items: %zu\n", tabs[MESSAGES_TAB_INDEX].body->items.size());
+ }
+ */
+ if(matrix->sync() != PluginResult::OK) {
+ show_notification("QuickMedia", "Intial matrix sync failed", Urgency::CRITICAL);
+ current_page = Page::EXIT;
+ return;
+ }
+
+ if(matrix->get_joined_rooms(tabs[ROOMS_TAB_INDEX].body->items) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL);
+ current_page = Page::EXIT;
+ return;
+ }
+
+ // TODO: the initial room to view should be the last viewed room when closing QuickMedia.
+ // The room id should be saved in a file when changing viewed room.
+ std::string current_room_id;
+ if(!tabs[ROOMS_TAB_INDEX].body->items.empty())
+ current_room_id = tabs[ROOMS_TAB_INDEX].body->items[0]->get_title();
+
+ // TODO: Allow empty initial room (if the user hasn't joined any room yet)
+ assert(!current_room_id.empty());
+
+ // TODO: Filer for rooms and settings
+ search_bar->onTextUpdateCallback = nullptr;
+
+ search_bar->onTextSubmitCallback = [matrix, &tabs, &selected_tab, &room_message_index, &current_room_id](const std::string &text) -> bool {
+ if(tabs[selected_tab].type == ChatTabType::MESSAGES) {
+ if(text.empty())
+ return false;
+
+ // TODO: Make asynchronous
+ if(matrix->post_message(current_room_id, text) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to post matrix message", Urgency::CRITICAL);
+ return false;
+ }
+ return true;
+ } else if(tabs[selected_tab].type == ChatTabType::ROOMS) {
+ BodyItem *selected_item = tabs[selected_tab].body->get_selected();
+ if(selected_item) {
+ // TODO: Change to selected_item->url once rooms have a display name
+ current_room_id = selected_item->get_title();
+ selected_tab = MESSAGES_TAB_INDEX;
+ room_message_index = 0;
+ tabs[MESSAGES_TAB_INDEX].body->clear_items();
+
+ size_t num_new_messages = 0;
+ BodyItems new_items;
+ // TODO: Make asynchronous
+ if(matrix->get_room_messages(current_room_id, 0, new_items, num_new_messages) == PluginResult::OK) {
+ room_message_index += num_new_messages;
+ tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(new_items));
+ if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && num_new_messages > 0)
+ tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1);
+ } else {
+ std::string err_msg = "Failed to get messages in room: " + current_room_id;
+ show_notification("QuickMedia", err_msg, Urgency::CRITICAL);
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+
+ struct SyncFutureResult {
+ BodyItems body_items;
+ size_t num_new_messages;
+ };
+
+ std::future<SyncFutureResult> sync_future;
+ bool sync_running = false;
+ std::string sync_future_room_id;
+ sf::Clock sync_timer;
+ sf::Int32 sync_min_time_ms = 0; // Sync immediately the first time
+
+ const float tab_spacer_height = 0.0f;
+ sf::Vector2f body_pos;
+ sf::Vector2f body_size;
+ bool redraw = true;
+ sf::Event event;
+
+ sf::RectangleShape tab_drop_shadow;
+ tab_drop_shadow.setFillColor(sf::Color(23, 25, 27));
+
+ while (current_page == Page::CHAT) {
+ while (window.pollEvent(event)) {
+ base_event_handler(event, Page::EXIT, false, false);
+ if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) {
+ redraw = true;
+ } else if(event.type == sf::Event::KeyPressed) {
+ if(event.key.code == sf::Keyboard::Up) {
+ tabs[selected_tab].body->select_previous_item();
+ } else if(event.key.code == sf::Keyboard::Down) {
+ tabs[selected_tab].body->select_next_item();
+ } else if(event.key.code == sf::Keyboard::Escape) {
+ current_page = Page::EXIT;
+ body->clear_items();
+ body->reset_selected();
+ search_bar->clear();
+ } else if(event.key.code == sf::Keyboard::Left) {
+ tabs[selected_tab].body->filter_search_fuzzy("");
+ tabs[selected_tab].body->clamp_selection();
+ selected_tab = std::max(0, selected_tab - 1);
+ search_bar->clear();
+ } else if(event.key.code == sf::Keyboard::Right) {
+ tabs[selected_tab].body->filter_search_fuzzy("");
+ tabs[selected_tab].body->clamp_selection();
+ selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1);
+ search_bar->clear();
+ }
+ }
+ }
+
+ if(redraw) {
+ redraw = false;
+ search_bar->onWindowResize(window_size);
+ search_bar->set_vertical_position(window_size.y - search_bar->getBottomWithoutShadow());
+
+ float body_padding_horizontal = 25.0f;
+ float body_padding_vertical = 25.0f;
+ float body_width = window_size.x - body_padding_horizontal * 2.0f;
+ if(body_width <= 480.0f) {
+ body_width = window_size.x;
+ body_padding_horizontal = 0.0f;
+ body_padding_vertical = 10.0f;
+ }
+
+ float search_bottom = search_bar->getBottomWithoutShadow();
+ body_pos = sf::Vector2f(body_padding_horizontal, body_padding_vertical + tab_height);
+ body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_height);
+ //get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true);
+ }
+
+ if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) {
+ fprintf(stderr, "Time since last sync: %d ms\n", sync_timer.getElapsedTime().asMilliseconds());
+ // TODO: Ignore matrix->sync() call the first time, its already called above for the first time
+ sync_min_time_ms = 3000;
+ sync_running = true;
+ sync_timer.restart();
+ sync_future_room_id = current_room_id;
+ sync_future = std::async(std::launch::async, [this, &sync_future_room_id, room_message_index]() {
+ Matrix *matrix = static_cast<Matrix*>(current_plugin);
+
+ SyncFutureResult result;
+ result.num_new_messages = 0;
+ if(matrix->sync() == PluginResult::OK) {
+ fprintf(stderr, "Synced matrix\n");
+ if(matrix->get_room_messages(sync_future_room_id, room_message_index, result.body_items, result.num_new_messages) != PluginResult::OK) {
+ fprintf(stderr, "Failed to get new matrix messages in room: %s\n", sync_future_room_id.c_str());
+ }
+ } else {
+ fprintf(stderr, "Failed to sync matrix\n");
+ }
+
+ return result;
+ });
+ }
+
+ if(sync_future.valid() && sync_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
+ SyncFutureResult sync_future_result = sync_future.get();
+ // Ignore finished sync if it happened in another room. When we navigate back to the room we will get the messages again
+ if(sync_future_room_id == current_room_id) {
+ room_message_index += sync_future_result.num_new_messages;
+ tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(sync_future_result.body_items));
+ if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && sync_future_result.num_new_messages > 0)
+ tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1);
+ }
+ sync_running = false;
+ }
+
+ search_bar->update();
+
+ window.clear(back_color);
+
+ const float width_per_tab = window_size.x / tabs.size();
+ sf::RectangleShape tab_background(sf::Vector2f(std::floor(width_per_tab), tab_height));
+
+ float tab_vertical_offset = 0.0f;
+ tabs[selected_tab].body->draw(window, body_pos, body_size);
+ const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f);
+
+ int i = 0;
+ for(ChatTab &tab : tabs) {
+ if(i == selected_tab)
+ tab_background.setFillColor(tab_selected_color);
+ else
+ tab_background.setFillColor(tab_unselected_color);
+
+ tab_background.setPosition(std::floor(i * width_per_tab), tab_spacer_height + std::floor(tab_vertical_offset));
+ window.draw(tab_background);
+ const float center = (i * width_per_tab) + (width_per_tab * 0.5f);
+ tab.text.setPosition(std::floor(center - tab.text.getLocalBounds().width * 0.5f), tab_y);
+ window.draw(tab.text);
+ ++i;
+ }
+
+ tab_drop_shadow.setSize(sf::Vector2f(window_size.x, 5.0f));
+ tab_drop_shadow.setPosition(0.0f, std::floor(tab_vertical_offset + tab_height));
+ window.draw(tab_drop_shadow);
+
+ search_bar->draw(window, false);
+ window.display();
+ }
+
+ exit(0); // Ignore futures and quit immediately
+ }
}