aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/Page.hpp10
-rw-r--r--include/QuickMedia.hpp36
-rw-r--r--include/SearchBar.hpp33
-rw-r--r--include/VideoPlayer.hpp46
-rw-r--r--plugins/Plugin.hpp6
-rw-r--r--project.conf3
-rw-r--r--src/Manganelo.cpp12
-rw-r--r--src/QuickMedia.cpp433
-rw-r--r--src/SearchBar.cpp93
-rw-r--r--src/VideoPlayer.cpp195
-rw-r--r--src/Youtube.cpp17
-rw-r--r--src/main.cpp224
12 files changed, 871 insertions, 237 deletions
diff --git a/include/Page.hpp b/include/Page.hpp
new file mode 100644
index 0000000..ce20971
--- /dev/null
+++ b/include/Page.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+namespace QuickMedia {
+ enum class Page {
+ EXIT,
+ SEARCH_SUGGESTION,
+ SEARCH_RESULT,
+ VIDEO_CONTENT
+ };
+} \ No newline at end of file
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
new file mode 100644
index 0000000..291afff
--- /dev/null
+++ b/include/QuickMedia.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "SearchBar.hpp"
+#include "Page.hpp"
+#include <vector>
+#include <stack>
+#include <memory>
+#include <SFML/Graphics/Font.hpp>
+#include <SFML/Graphics/RenderWindow.hpp>
+
+namespace QuickMedia {
+ class Body;
+ class Plugin;
+
+ class Program {
+ public:
+ Program();
+ ~Program();
+ void run();
+ private:
+ void base_event_handler(sf::Event &event);
+ void search_suggestion_page();
+ void search_result_page();
+ void video_content_page();
+ private:
+ sf::RenderWindow window;
+ sf::Vector2f window_size;
+ sf::Font font;
+ Body *body;
+ Plugin *current_plugin;
+ std::unique_ptr<SearchBar> search_bar;
+ Page current_page;
+ std::string video_url;
+ std::stack<Page> page_view_stack;
+ };
+} \ No newline at end of file
diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp
new file mode 100644
index 0000000..c9f75f0
--- /dev/null
+++ b/include/SearchBar.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <SFML/Graphics/RenderWindow.hpp>
+#include <SFML/Graphics/Font.hpp>
+#include <SFML/Graphics/Text.hpp>
+#include <SFML/Graphics/RectangleShape.hpp>
+#include <functional>
+
+namespace QuickMedia {
+ using TextUpdateCallback = std::function<void(const sf::String &text)>;
+ using TextSubmitCallback = std::function<void(const sf::String &text)>;
+
+ class SearchBar {
+ public:
+ SearchBar(sf::Font &font);
+ void draw(sf::RenderWindow &window);
+ void update();
+ void onWindowResize(const sf::Vector2f &window_size);
+ void onTextEntered(sf::Uint32 codepoint);
+ void clear();
+
+ float getBottom() const;
+
+ TextUpdateCallback onTextUpdateCallback;
+ TextSubmitCallback onTextSubmitCallback;
+ private:
+ sf::Text text;
+ sf::RectangleShape background;
+ bool show_placeholder;
+ bool updated_search;
+ sf::Clock time_since_search_update;
+ };
+} \ No newline at end of file
diff --git a/include/VideoPlayer.hpp b/include/VideoPlayer.hpp
new file mode 100644
index 0000000..e98221e
--- /dev/null
+++ b/include/VideoPlayer.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <SFML/Graphics/RenderWindow.hpp>
+#include <SFML/Graphics/Texture.hpp>
+#include <SFML/Graphics/Sprite.hpp>
+#include <SFML/Window/Context.hpp>
+#include <thread>
+#include <mutex>
+#include <atomic>
+#include <stdexcept>
+
+class mpv_handle;
+class mpv_opengl_cb_context;
+
+namespace QuickMedia {
+ class VideoInitializationException : public std::runtime_error {
+ public:
+ VideoInitializationException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ class VideoPlayer {
+ public:
+ // Throws VideoInitializationException on error
+ VideoPlayer(unsigned int width, unsigned int height, const char *file, bool loop = false);
+ ~VideoPlayer();
+
+ void setPosition(float x, float y);
+ bool resize(const sf::Vector2i &size);
+ void draw(sf::RenderWindow &window);
+
+ // This counter is incremented when mpv wants to redraw content
+ std::atomic_int redrawCounter;
+ private:
+ sf::Context context;
+ mpv_handle *mpv;
+ mpv_opengl_cb_context *mpvGl;
+ std::thread renderThread;
+ std::mutex renderMutex;
+ sf::Sprite sprite;
+ sf::Texture texture;
+ sf::Uint8 *textureBuffer;
+ bool alive;
+ sf::Vector2i video_size;
+ sf::Vector2i desired_size;
+ };
+}
diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp
index 9d62356..dd50998 100644
--- a/plugins/Plugin.hpp
+++ b/plugins/Plugin.hpp
@@ -7,12 +7,14 @@
namespace QuickMedia {
class BodyItem {
public:
- BodyItem(const std::string &_title): title(_title) {
+ BodyItem(const std::string &_title): title(_title), visible(true) {
}
std::string title;
- std::string cover_url;
+ std::string url;
+ std::string thumbnail_url;
+ bool visible;
};
enum class SearchResult {
diff --git a/project.conf b/project.conf
index c24ca64..d54e94b 100644
--- a/project.conf
+++ b/project.conf
@@ -6,4 +6,7 @@ platforms = ["posix"]
[dependencies]
sfml-graphics = "2"
+mpv = "1.25.0"
+gl = ">=17.3"
+x11 = "1.6.5"
jsoncpp = "1.5" \ No newline at end of file
diff --git a/src/Manganelo.cpp b/src/Manganelo.cpp
index de39c77..8bbaa9f 100644
--- a/src/Manganelo.cpp
+++ b/src/Manganelo.cpp
@@ -29,6 +29,7 @@ namespace QuickMedia {
const char *href = quickmedia_html_node_get_attribute_value(node, "href");
const char *text = quickmedia_html_node_get_text(node);
auto item = std::make_unique<BodyItem>(text);
+ item->url = href;
item_data->result_items.push_back(std::move(item));
}, &item_data);
if (result != 0)
@@ -39,7 +40,7 @@ namespace QuickMedia {
ItemData *item_data = (ItemData*)userdata;
const char *src = quickmedia_html_node_get_attribute_value(node, "src");
if(item_data->item_index < item_data->result_items.size()) {
- item_data->result_items[item_data->item_index]->cover_url = src;
+ item_data->result_items[item_data->item_index]->thumbnail_url = src;
++item_data->item_index;
}
}, &item_data);
@@ -70,9 +71,6 @@ namespace QuickMedia {
}
SuggestionResult Manganelo::update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) {
- if(text.empty())
- return SuggestionResult::OK;
-
std::string url = "https://manganelo.com/home_json_search";
std::string search_term = "searchword=";
search_term += url_param_encode(text);
@@ -101,8 +99,10 @@ namespace QuickMedia {
if(name.isString() && name.asCString()[0] != '\0') {
std::string name_str = name.asString();
while(remove_html_span(name_str)) {}
- auto item = std::make_unique<BodyItem>(name_str);
- result_items.push_back(std::move(item));
+ if(name_str != text) {
+ auto item = std::make_unique<BodyItem>(name_str);
+ result_items.push_back(std::move(item));
+ }
}
}
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
new file mode 100644
index 0000000..a63f430
--- /dev/null
+++ b/src/QuickMedia.cpp
@@ -0,0 +1,433 @@
+#include "../include/QuickMedia.hpp"
+#include "../plugins/Manganelo.hpp"
+#include "../plugins/Youtube.hpp"
+#include "../include/VideoPlayer.hpp"
+
+#include <SFML/Graphics/RectangleShape.hpp>
+#include <SFML/Graphics/Text.hpp>
+#include <SFML/Window/Event.hpp>
+#include <assert.h>
+
+const sf::Color front_color(43, 45, 47);
+const sf::Color back_color(33, 35, 37);
+
+namespace QuickMedia {
+ class Body {
+ public:
+ Body(sf::Font &font) : title_text("", font, 14), selected_item(0) {
+ title_text.setFillColor(sf::Color::White);
+ }
+
+ void add_item(std::unique_ptr<BodyItem> item) {
+ items.push_back(std::move(item));
+ }
+
+ // Select previous item, ignoring invisible items
+ void select_previous_item() {
+ if(items.empty())
+ return;
+
+ int num_items = (int)items.size();
+ for(int i = 0; i < num_items; ++i) {
+ --selected_item;
+ if(selected_item < 0)
+ selected_item = num_items - 1;
+ if(items[selected_item]->visible)
+ return;
+ }
+ }
+
+ // Select next item, ignoring invisible items
+ void select_next_item() {
+ if(items.empty())
+ return;
+
+ int num_items = (int)items.size();
+ for(int i = 0; i < num_items; ++i) {
+ ++selected_item;
+ if(selected_item == num_items)
+ selected_item = 0;
+ if(items[selected_item]->visible)
+ return;
+ }
+ }
+
+ void reset_selected() {
+ selected_item = 0;
+ }
+
+ void clear_items() {
+ items.clear();
+ }
+
+ BodyItem* get_selected() const {
+ if(items.empty() || !items[selected_item]->visible)
+ return nullptr;
+ return items[selected_item].get();
+ }
+
+ void clamp_selection() {
+ int num_items = (int)items.size();
+ if(items.empty())
+ return;
+
+ if(selected_item < 0)
+ selected_item = 0;
+ else if(selected_item >= num_items)
+ selected_item = num_items - 1;
+
+ for(int i = selected_item; i >= 0; --i) {
+ if(items[i]->visible) {
+ selected_item = i;
+ return;
+ }
+ }
+
+ for(int i = selected_item; i < num_items; ++i) {
+ if(items[i]->visible) {
+ selected_item = i;
+ return;
+ }
+ }
+ }
+
+ void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) {
+ const float font_height = title_text.getCharacterSize() + 8.0f;
+ const float image_height = 50.0f;
+
+ sf::RectangleShape image(sf::Vector2f(50, image_height));
+ image.setFillColor(sf::Color::White);
+
+ sf::RectangleShape item_background;
+ item_background.setFillColor(front_color);
+ item_background.setOutlineThickness(1.0f);
+ item_background.setOutlineColor(sf::Color(63, 65, 67));
+
+ sf::RectangleShape selected_border(sf::Vector2f(5.0f, 50));
+ selected_border.setFillColor(sf::Color::Red);
+
+ int i = 0;
+ for(const auto &item : items) {
+ if(!item->visible) {
+ ++i;
+ continue;
+ }
+
+ sf::Vector2f item_pos = pos;
+ if(i == selected_item) {
+ selected_border.setPosition(pos);
+ window.draw(selected_border);
+ item_pos.x += selected_border.getSize().x;
+ item_background.setFillColor(front_color);
+ } else {
+ item_background.setFillColor(sf::Color(38, 40, 42));
+ }
+
+ item_background.setPosition(item_pos);
+ item_background.setSize(sf::Vector2f(size.x, 50));
+ window.draw(item_background);
+
+ image.setPosition(item_pos);
+ window.draw(image);
+
+ title_text.setString(item->title);
+ title_text.setPosition(item_pos.x + 50 + 10, item_pos.y);
+ window.draw(title_text);
+
+
+ pos.y += 50 + 10;
+ ++i;
+ }
+ }
+
+ static bool string_find_case_insensitive(const std::string &str, const std::string &substr) {
+ auto it = std::search(str.begin(), str.end(), substr.begin(), substr.end(),
+ [](char c1, char c2) {
+ return std::toupper(c1) == std::toupper(c2);
+ });
+ return it != str.end();
+ }
+
+ // TODO: Make this actually fuzzy... Right now it's just a case insensitive string find.
+ // TODO: Highlight the part of the text that matches the search
+ void filter_search_fuzzy(const std::string &text) {
+ if(text.empty()) {
+ for(auto &item : items) {
+ item->visible = true;
+ }
+ return;
+ }
+
+ for(auto &item : items) {
+ item->visible = string_find_case_insensitive(item->title, text);
+ }
+ }
+
+ sf::Text title_text;
+ int selected_item;
+ std::vector<std::unique_ptr<BodyItem>> items;
+ };
+
+ Program::Program() :
+ window(sf::VideoMode(800, 600), "QuickMedia"),
+ window_size(800, 600),
+ body(nullptr),
+ current_plugin(nullptr),
+ current_page(Page::SEARCH_SUGGESTION)
+ {
+ window.setVerticalSyncEnabled(true);
+ if(!font.loadFromFile("fonts/Lato-Regular.ttf")) {
+ fprintf(stderr, "Failed to load font!\n");
+ abort();
+ }
+ body = new Body(font);
+ //current_plugin = new Manganelo();
+ current_plugin = new Youtube();
+ search_bar = std::make_unique<SearchBar>(font);
+ page_view_stack.push(current_page);
+ }
+
+ Program::~Program() {
+ delete body;
+ delete current_plugin;
+ }
+
+ static SearchResult search_selected_suggestion(Body *body, Plugin *plugin) {
+ BodyItem *selected_item = body->get_selected();
+ if(!selected_item)
+ return SearchResult::ERR;
+
+ std::string selected_item_title = selected_item->title;
+ body->clear_items();
+ SearchResult search_result = plugin->search(selected_item_title, body->items);
+ body->reset_selected();
+ return search_result;
+ }
+
+ static void update_search_suggestions(const sf::String &text, Body *body, Plugin *plugin) {
+ body->clear_items();
+ if(text.isEmpty())
+ return;
+
+ body->items.push_back(std::make_unique<BodyItem>(text));
+ SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items);
+ body->clamp_selection();
+ }
+
+ void Program::run() {
+ while(window.isOpen()) {
+ switch(current_page) {
+ case Page::EXIT:
+ return;
+ case Page::SEARCH_SUGGESTION:
+ search_suggestion_page();
+ break;
+ case Page::SEARCH_RESULT:
+ search_result_page();
+ break;
+ case Page::VIDEO_CONTENT:
+ video_content_page();
+ break;
+ default:
+ return;
+ }
+ }
+ }
+
+ void Program::base_event_handler(sf::Event &event) {
+
+ }
+
+ void Program::search_suggestion_page() {
+ search_bar->onTextUpdateCallback = [this](const std::string &text) {
+ update_search_suggestions(text, body, current_plugin);
+ };
+
+ search_bar->onTextSubmitCallback = [this](const std::string &text) {
+ if(search_selected_suggestion(body, current_plugin) == SearchResult::OK) {
+ current_page = Page::SEARCH_RESULT;
+ page_view_stack.push(current_page);
+ }
+ };
+
+ sf::Vector2f body_pos;
+ sf::Vector2f body_size;
+ bool resized = true;
+
+ while (window.isOpen() && current_page == Page::SEARCH_SUGGESTION) {
+ sf::Event event;
+ while (window.pollEvent(event)) {
+ if (event.type == sf::Event::Closed) {
+ window.close();
+ current_page = Page::EXIT;
+ } else if(event.type == sf::Event::Resized) {
+ window_size.x = event.size.width;
+ window_size.y = event.size.height;
+ sf::FloatRect visible_area(0, 0, window_size.x, window_size.y);
+ window.setView(sf::View(visible_area));
+ resized = true;
+ } else if(event.type == sf::Event::KeyPressed) {
+ if(event.key.code == sf::Keyboard::Up) {
+ body->select_previous_item();
+ } else if(event.key.code == sf::Keyboard::Down) {
+ body->select_next_item();
+ } else if(event.key.code == sf::Keyboard::Escape) {
+ current_page = Page::EXIT;
+ window.close();
+ }
+ } else if(event.type == sf::Event::TextEntered) {
+ search_bar->onTextEntered(event.text.unicode);
+ }
+ }
+
+ if(resized) {
+ search_bar->onWindowResize(window_size);
+
+ float body_padding_horizontal = 50.0f;
+ float body_padding_vertical = 50.0f;
+ float body_width = window_size.x - body_padding_horizontal * 2.0f;
+ if(body_width < 400) {
+ body_width = window_size.x;
+ body_padding_horizontal = 0.0f;
+ }
+
+ float search_bottom = search_bar->getBottom();
+ body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical);
+ body_size = sf::Vector2f(body_width, window_size.y);
+ }
+
+ search_bar->update();
+
+ window.clear(back_color);
+ body->draw(window, body_pos, body_size);
+ search_bar->draw(window);
+ window.display();
+ }
+ }
+
+ void Program::search_result_page() {
+ search_bar->onTextUpdateCallback = [this](const std::string &text) {
+ body->filter_search_fuzzy(text);
+ body->clamp_selection();
+ };
+
+ search_bar->onTextSubmitCallback = [this](const std::string &text) {
+ BodyItem *selected_item = body->get_selected();
+ printf("Selected item: %s\n", selected_item->title.c_str());
+ if(!selected_item)
+ return;
+ video_url = selected_item->url;
+ current_page = Page::VIDEO_CONTENT;
+ page_view_stack.push(current_page);
+ };
+
+ sf::Vector2f body_pos;
+ sf::Vector2f body_size;
+ bool resized = true;
+
+ while (window.isOpen() && current_page == Page::SEARCH_RESULT) {
+ sf::Event event;
+ while (window.pollEvent(event)) {
+ if (event.type == sf::Event::Closed) {
+ window.close();
+ current_page = Page::EXIT;
+ } else if(event.type == sf::Event::Resized) {
+ window_size.x = event.size.width;
+ window_size.y = event.size.height;
+ sf::FloatRect visible_area(0, 0, window_size.x, window_size.y);
+ window.setView(sf::View(visible_area));
+ resized = true;
+ } else if(event.type == sf::Event::KeyPressed) {
+ if(event.key.code == sf::Keyboard::Up) {
+ body->select_previous_item();
+ } else if(event.key.code == sf::Keyboard::Down) {
+ body->select_next_item();
+ } else if(event.key.code == sf::Keyboard::Escape) {
+ current_page = Page::SEARCH_SUGGESTION;
+ body->clear_items();
+ body->reset_selected();
+ search_bar->clear();
+ }
+ } else if(event.type == sf::Event::TextEntered) {
+ search_bar->onTextEntered(event.text.unicode);
+ }
+ }
+
+ if(resized) {
+ search_bar->onWindowResize(window_size);
+
+ float body_padding_horizontal = 50.0f;
+ float body_padding_vertical = 50.0f;
+ float body_width = window_size.x - body_padding_horizontal * 2.0f;
+ if(body_width < 400) {
+ body_width = window_size.x;
+ body_padding_horizontal = 0.0f;
+ }
+
+ float search_bottom = search_bar->getBottom();
+ body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical);
+ body_size = sf::Vector2f(body_width, window_size.y);
+ }
+
+ search_bar->update();
+
+ window.clear(back_color);
+ body->draw(window, body_pos, body_size);
+ search_bar->draw(window);
+ window.display();
+ }
+ }
+
+ void Program::video_content_page() {
+ search_bar->onTextUpdateCallback = nullptr;
+ search_bar->onTextSubmitCallback = nullptr;
+
+ std::unique_ptr<VideoPlayer> video_player;
+ try {
+ printf("Play video: %s\n", video_url.c_str());
+ video_player = std::make_unique<VideoPlayer>(window_size.x, window_size.y, video_url.c_str());
+ } catch(VideoInitializationException &e) {
+ fprintf(stderr, "Failed to create video player!. TODO: Show this to the user");
+ }
+
+ bool resized = false;
+ sf::Clock resize_timer;
+
+ while (window.isOpen() && current_page == Page::VIDEO_CONTENT) {
+ sf::Event event;
+ while (window.pollEvent(event)) {
+ if (event.type == sf::Event::Closed) {
+ window.close();
+ current_page = Page::EXIT;
+ } else if(event.type == sf::Event::Resized) {
+ window_size.x = event.size.width;
+ window_size.y = event.size.height;
+ sf::FloatRect visible_area(0, 0, window_size.x, window_size.y);
+ window.setView(sf::View(visible_area));
+ resized = true;
+ resize_timer.restart();
+ } else if(event.type == sf::Event::KeyPressed) {
+ if(event.key.code == sf::Keyboard::Escape) {
+ current_page = Page::SEARCH_SUGGESTION;
+ body->clear_items();
+ body->reset_selected();
+ search_bar->clear();
+ }
+ }
+ }
+
+ if(resized && resize_timer.getElapsedTime().asMilliseconds() >= 300) {
+ resized = false;
+ if(video_player) {
+ if(!video_player->resize(sf::Vector2i(window_size.x, window_size.y)))
+ video_player.release();
+ }
+ }
+
+ window.clear();
+ if(video_player)
+ video_player->draw(window);
+ window.display();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp
new file mode 100644
index 0000000..dad9e7c
--- /dev/null
+++ b/src/SearchBar.cpp
@@ -0,0 +1,93 @@
+#include "../include/SearchBar.hpp"
+
+const sf::Color text_placeholder_color(255, 255, 255, 100);
+const sf::Color front_color(43, 45, 47);
+const sf::Color back_color(33, 35, 37);
+const float background_margin_horizontal = 8.0f;
+const float background_margin_vertical = 4.0f;
+const float padding_horizontal = 10.0f;
+const float padding_vertical = 10.0f;
+
+namespace QuickMedia {
+ SearchBar::SearchBar(sf::Font &font) :
+ onTextUpdateCallback(nullptr),
+ onTextSubmitCallback(nullptr),
+ text("Search...", font, 18),
+ show_placeholder(true),
+ updated_search(false)
+ {
+ text.setFillColor(text_placeholder_color);
+ background.setFillColor(front_color);
+ background.setPosition(padding_horizontal, padding_vertical);
+ }
+
+ void SearchBar::draw(sf::RenderWindow &window) {
+ window.draw(background);
+ window.draw(text);
+ }
+
+ void SearchBar::update() {
+ if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= 150) {
+ updated_search = false;
+ sf::String str = text.getString();
+ if(show_placeholder)
+ str.clear();
+ if(onTextUpdateCallback)
+ onTextUpdateCallback(str);
+ }
+ }
+
+ void SearchBar::onWindowResize(const sf::Vector2f &window_size) {
+ float font_height = text.getCharacterSize() + 8.0f;
+ float rect_height = font_height + background_margin_vertical * 2.0f;
+ background.setSize(sf::Vector2f(window_size.x - padding_horizontal * 2.0f, rect_height));
+ text.setPosition(padding_horizontal + background_margin_horizontal, padding_vertical + background_margin_vertical);
+ }
+
+ void SearchBar::onTextEntered(sf::Uint32 codepoint) {
+ if(codepoint == 8 && !show_placeholder) { // Backspace
+ sf::String str = text.getString();
+ if(str.getSize() > 0) {
+ str.erase(str.getSize() - 1, 1);
+ text.setString(str);
+ if(str.getSize() == 0) {
+ show_placeholder = true;
+ text.setString("Search...");
+ text.setFillColor(text_placeholder_color);
+ }
+ updated_search = true;
+ time_since_search_update.restart();
+ }
+ } else if(codepoint == 13) { // Return
+ if(onTextSubmitCallback)
+ onTextSubmitCallback(text.getString());
+
+ if(!show_placeholder) {
+ show_placeholder = true;
+ text.setString("Search...");
+ text.setFillColor(text_placeholder_color);
+ }
+ } else if(codepoint > 31) { // Non-control character
+ if(show_placeholder) {
+ show_placeholder = false;
+ text.setString("");
+ text.setFillColor(sf::Color::White);
+ }
+ sf::String str = text.getString();
+ str += codepoint;
+ text.setString(str);
+ updated_search = true;
+ time_since_search_update.restart();
+ }
+ }
+
+ void SearchBar::clear() {
+ show_placeholder = true;
+ text.setString("Search...");
+ text.setFillColor(text_placeholder_color);
+ }
+
+ float SearchBar::getBottom() const {
+ return background.getPosition().y + background.getSize().y;
+ }
+} \ No newline at end of file
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
new file mode 100644
index 0000000..304ef2d
--- /dev/null
+++ b/src/VideoPlayer.cpp
@@ -0,0 +1,195 @@
+#include "../include/VideoPlayer.hpp"
+#include <mpv/client.h>
+#include <mpv/opengl_cb.h>
+#include <clocale>
+
+#include <SFML/Config.hpp>
+
+#if defined(SFML_SYSTEM_WINDOWS)
+ #ifdef _MSC_VER
+ #include <windows.h>
+ #endif
+ #include <GL/gl.h>
+ #include <GL/glx.h>
+#elif defined(SFML_SYSTEM_LINUX) || defined(SFML_SYSTEM_FREEBSD)
+ #if defined(SFML_OPENGL_ES)
+ #include <GLES/gl.h>
+ #include <GLES/glext.h>
+ #else
+ #include <GL/gl.h>
+ #endif
+ #include <GL/glx.h>
+ #define glGetProcAddress glXGetProcAddress
+#elif defined(SFML_SYSTEM_MACOS)
+ #include <OpenGL/gl.h>
+#elif defined (SFML_SYSTEM_IOS)
+ #include <OpenGLES/ES1/gl.h>
+ #include <OpenGLES/ES1/glext.h>
+#elif defined (SFML_SYSTEM_ANDROID)
+ #include <GLES/gl.h>
+ #include <GLES/glext.h>
+ // We're not using OpenGL ES 2+ yet, but we can use the sRGB extension
+ #include <GLES2/gl2ext.h>
+#endif
+
+using namespace std;
+
+namespace QuickMedia {
+ void* getProcAddressMpv(void *funcContext, const char *name) {
+ return (void*)glGetProcAddress((const GLubyte*)name);
+ }
+
+ void onMpvRedraw(void *rawVideo) {
+ VideoPlayer *video = (VideoPlayer*)rawVideo;
+ ++video->redrawCounter;
+ }
+
+ VideoPlayer::VideoPlayer(unsigned int width, unsigned int height, const char *file, bool loop) :
+ redrawCounter(0),
+ context(sf::ContextSettings(), width, height),
+ mpv(nullptr),
+ mpvGl(nullptr),
+ textureBuffer((sf::Uint8*)malloc(width * height * 4)), // 4 = red, green, blue and alpha
+ alive(true),
+ video_size(width, height),
+ desired_size(width, height)
+ {
+ if(!textureBuffer)
+ throw VideoInitializationException("Failed to allocate memory for video");
+
+ context.setActive(true);
+
+ if(!texture.create(width, height))
+ throw VideoInitializationException("Failed to create texture for video");
+ texture.setSmooth(true);
+
+ // mpv_create requires LC_NUMERIC to be set to "C" for some reason, see mpv_create documentation
+ std::setlocale(LC_NUMERIC, "C");
+ mpv = mpv_create();
+ if(!mpv)
+ throw VideoInitializationException("Failed to create mpv handle");
+
+ if(mpv_initialize(mpv) < 0)
+ throw VideoInitializationException("Failed to initialize mpv");
+
+ mpv_set_option_string(mpv, "input-default-bindings", "yes");
+ // Enable keyboard input on the X11 window
+ mpv_set_option_string(mpv, "input-vo-keyboard", "yes");
+
+ mpv_set_option_string(mpv, "vo", "opengl-cb");
+ mpv_set_option_string(mpv, "hwdec", "auto");
+ if(loop)
+ mpv_set_option_string(mpv, "loop", "inf");
+ mpvGl = (mpv_opengl_cb_context*)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB);
+ if(!mpvGl)
+ throw VideoInitializationException("Failed to initialize mpv opengl render context");
+
+ mpv_opengl_cb_set_update_callback(mpvGl, onMpvRedraw, this);
+ if(mpv_opengl_cb_init_gl(mpvGl, nullptr, getProcAddressMpv, nullptr) < 0)
+ throw VideoInitializationException("Failed to initialize mpv gl callback func");
+
+ renderThread = thread([this]() {
+ context.setActive(true);
+ while(alive) {
+ while(true) {
+ mpv_event *mpvEvent = mpv_wait_event(mpv, 0.010);
+ if(mpvEvent->event_id == MPV_EVENT_NONE)
+ break;
+ else if(mpvEvent->event_id == MPV_EVENT_SHUTDOWN)
+ return;
+ else if(mpvEvent->event_id == MPV_EVENT_VIDEO_RECONFIG) {
+ int64_t w, h;
+ if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 &&
+ mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 &&
+ w > 0 && h > 0 && (w != video_size.x || h != video_size.y))
+ {
+ {
+ lock_guard<mutex> lock(renderMutex);
+ video_size.x = w;
+ video_size.y = h;
+ context.setActive(true);
+ if(texture.create(w, h)) {
+ void *newTextureBuf = realloc(textureBuffer, w * h * 4);
+ if(newTextureBuf)
+ textureBuffer = (sf::Uint8*)newTextureBuf;
+ }
+ }
+ resize(desired_size);
+ }
+ }
+ }
+
+ if(redrawCounter > 0) {
+ --redrawCounter;
+ context.setActive(true);
+ lock_guard<mutex> lock(renderMutex);
+ auto textureSize = texture.getSize();
+ //mpv_render_context_render(mpvGl, params);
+ mpv_opengl_cb_draw(mpvGl, 0, textureSize.x, textureSize.y);
+ // TODO: Instead of copying video to cpu buffer and then to texture, copy directly from video buffer to texture buffer
+ glReadPixels(0, 0, textureSize.x, textureSize.y, GL_RGBA, GL_UNSIGNED_BYTE, textureBuffer);
+ texture.update(textureBuffer);
+ sprite.setTexture(texture, true);
+ mpv_opengl_cb_report_flip(mpvGl, 0);
+ }
+ }
+ });
+
+ const char *cmd[] = { "loadfile", file, nullptr };
+ mpv_command(mpv, cmd);
+ context.setActive(false);
+ }
+
+ VideoPlayer::~VideoPlayer() {
+ alive = false;
+ renderThread.join();
+
+ lock_guard<mutex> lock(renderMutex);
+ context.setActive(true);
+ if(mpvGl)
+ mpv_opengl_cb_set_update_callback(mpvGl, nullptr, nullptr);
+
+ free(textureBuffer);
+ mpv_opengl_cb_uninit_gl(mpvGl);
+ mpv_detach_destroy(mpv);
+ }
+
+ void VideoPlayer::setPosition(float x, float y) {
+ sprite.setPosition(x, y);
+ }
+
+ bool VideoPlayer::resize(const sf::Vector2i &size) {
+ lock_guard<mutex> lock(renderMutex);
+ float video_ratio = (double)video_size.x / (double)video_size.y;
+ float scale_x = 1.0f;
+ float scale_y = 1.0f;
+ if(video_ratio >= 0.0f) {
+ double ratio_x = (double)size.x / (double)video_size.x;
+ scale_x = ratio_x;
+ scale_y = ratio_x;
+ sprite.setPosition(0.0f, size.y * 0.5f - video_size.y * scale_y * 0.5f);
+ } else {
+ double ratio_y = (double)size.y / (double)video_size.y;
+ scale_x = ratio_y;
+ scale_y = ratio_y;
+ sprite.setPosition(size.x * 0.5f - video_size.x * scale_x * 0.5f, 0.0f);
+ }
+ sprite.setScale(scale_x, scale_y);
+ desired_size = size;
+ #if 0
+ void *newTextureBuf = realloc(textureBuffer, size.x * size.y * 4);
+ if(!newTextureBuf)
+ return false;
+ textureBuffer = (sf::Uint8*)newTextureBuf;
+ if(!texture.create(size.x, size.y))
+ return false;
+ return true;
+ #endif
+ return true;
+ }
+
+ void VideoPlayer::draw(sf::RenderWindow &window) {
+ lock_guard<mutex> lock(renderMutex);
+ window.draw(sprite);
+ }
+}
diff --git a/src/Youtube.cpp b/src/Youtube.cpp
index 9f9c4c2..85969b9 100644
--- a/src/Youtube.cpp
+++ b/src/Youtube.cpp
@@ -22,6 +22,7 @@ namespace QuickMedia {
const char *href = quickmedia_html_node_get_attribute_value(node, "href");
const char *title = quickmedia_html_node_get_attribute_value(node, "title");
auto item = std::make_unique<BodyItem>(title);
+ item->url = std::string("https://www.youtube.com") + href;
result_items->push_back(std::move(item));
}, &result_items);
@@ -30,24 +31,21 @@ namespace QuickMedia {
return result == 0 ? SearchResult::OK : SearchResult::ERR;
}
- static void iterate_suggestion_result(const Json::Value &value, std::vector<std::unique_ptr<BodyItem>> &result_items, int &ignore_count) {
+ static void iterate_suggestion_result(const Json::Value &value, const std::string &search_text, std::vector<std::unique_ptr<BodyItem>> &result_items) {
if(value.isArray()) {
for(const Json::Value &child : value) {
- iterate_suggestion_result(child, result_items, ignore_count);
+ iterate_suggestion_result(child, search_text, result_items);
}
} else if(value.isString()) {
- if(ignore_count > 1) {
- auto item = std::make_unique<BodyItem>(value.asString());
+ std::string title = value.asString();
+ if(title != search_text) {
+ auto item = std::make_unique<BodyItem>(title);
result_items.push_back(std::move(item));
}
- ++ignore_count;
}
}
SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) {
- if(text.empty())
- return SuggestionResult::OK;
-
std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&q=";
url += url_param_encode(text);
@@ -77,8 +75,7 @@ namespace QuickMedia {
return SuggestionResult::ERR;
}
- int ignore_count = 0;
- iterate_suggestion_result(json_root, result_items, ignore_count);
+ iterate_suggestion_result(json_root, text, result_items);
return SuggestionResult::OK;
}
} \ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index c3c91b7..2dc50ee 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,223 +1,9 @@
-#include "../include/Program.h"
-#include "../plugins/Manganelo.hpp"
-#include "../plugins/Youtube.hpp"
-#include <SFML/Graphics.hpp>
-#include <string>
-#include <vector>
-
-const sf::Color front_color(43, 45, 47);
-const sf::Color back_color(33, 35, 37);
-
-namespace QuickMedia {
- class Body {
- public:
- Body(sf::Font &font) : title_text("", font, 14), selected_item(0) {
- title_text.setFillColor(sf::Color::White);
- }
-
- void add_item(std::unique_ptr<BodyItem> item) {
- items.push_back(std::move(item));
- }
-
- void select_previous_item() {
- selected_item = std::max(0, selected_item - 1);
- }
-
- void select_next_item() {
- const int last_item = std::max(0, (int)items.size() - 1);
- selected_item = std::min(last_item, selected_item + 1);
- }
-
- void reset_selected() {
- selected_item = 0;
- }
-
- void clear_items() {
- items.clear();
- }
-
- void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) {
- const float font_height = title_text.getCharacterSize() + 8.0f;
-
- sf::RectangleShape image(sf::Vector2f(50, 50));
- image.setFillColor(sf::Color::White);
-
- sf::RectangleShape item_background;
- item_background.setFillColor(front_color);
- item_background.setOutlineThickness(1.0f);
- item_background.setOutlineColor(sf::Color(63, 65, 67));
-
- sf::RectangleShape selected_border(sf::Vector2f(5.0f, 50));
- selected_border.setFillColor(sf::Color::Red);
-
- int i = 0;
- for(const auto &item : items) {
- sf::Vector2f item_pos = pos;
- if(i == selected_item) {
- selected_border.setPosition(pos);
- window.draw(selected_border);
- item_pos.x += selected_border.getSize().x;
- }
-
- item_background.setPosition(item_pos);
- item_background.setSize(sf::Vector2f(size.x, 50));
- window.draw(item_background);
-
- image.setPosition(item_pos);
- window.draw(image);
-
- title_text.setString(item->title);
- title_text.setPosition(item_pos.x + 50 + 10, item_pos.y);
- window.draw(title_text);
-
- pos.y += 50 + 10;
- ++i;
- }
- }
-
- sf::Text title_text;
- int selected_item;
- std::vector<std::unique_ptr<BodyItem>> items;
- };
-}
-
-static void search(const sf::String &text, QuickMedia::Body *body, QuickMedia::Plugin *plugin) {
- body->clear_items();
- QuickMedia::SearchResult search_result = plugin->search(text, body->items);
- fprintf(stderr, "Search result: %d\n", search_result);
-}
-
-static void update_search_suggestions(const sf::String &text, QuickMedia::Body *body, QuickMedia::Plugin *plugin) {
- body->clear_items();
- QuickMedia::SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items);
- fprintf(stderr, "Suggestion result: %d\n", suggestion_result);
-}
+#include "../include/QuickMedia.hpp"
+#include <X11/Xlib.h>
int main() {
- const float padding_horizontal = 10.0f;
- const float padding_vertical = 10.0f;
-
- sf::RenderWindow window(sf::VideoMode(800, 800), "SFML works!");
- window.setVerticalSyncEnabled(true);
-
- sf::Font font;
- if(!font.loadFromFile("fonts/Lato-Regular.ttf")) {
- fprintf(stderr, "Failed to load font!\n");
- abort();
- }
-
- bool show_placeholder = true;
- sf::Color text_placeholder_color(255, 255, 255, 100);
- sf::Text search_text("Search...", font, 18);
- search_text.setFillColor(text_placeholder_color);
-
- bool resized = true;
- sf::Vector2f window_size(window.getSize().x, window.getSize().y);
-
- sf::RectangleShape search_background;
- search_background.setFillColor(front_color);
- search_background.setPosition(padding_horizontal, padding_vertical);
- const float search_background_margin_horizontal = 8.0f;
- const float search_background_margin_vertical = 4.0f;
-
- sf::RectangleShape body_background;
- body_background.setFillColor(front_color);
-
- QuickMedia::Body body(font);
- QuickMedia::Manganelo manganelo_plugin;
- QuickMedia::Youtube youtube_plugin;
- QuickMedia::Plugin *plugin = &manganelo_plugin;
-
- sf::Clock time_since_search_update;
- bool updated_search = false;
-
- while (window.isOpen()) {
- sf::Event event;
-
- while (window.pollEvent(event)) {
- if (event.type == sf::Event::Closed)
- window.close();
- else if(event.type == sf::Event::Resized) {
- window_size.x = event.size.width;
- window_size.y = event.size.height;
- sf::FloatRect visible_area(0, 0, window_size.x, window_size.y);
- window.setView(sf::View(visible_area));
- resized = true;
- } else if(event.type == sf::Event::KeyPressed) {
- if(event.key.code == sf::Keyboard::Up) {
- body.select_previous_item();
- } else if(event.key.code == sf::Keyboard::Down) {
- body.select_next_item();
- }
- } else if(event.type == sf::Event::TextEntered) {
- if(event.text.unicode == 8 && !show_placeholder) { // Backspace
- sf::String str = search_text.getString();
- if(str.getSize() > 0) {
- str.erase(str.getSize() - 1, 1);
- search_text.setString(str);
- if(str.getSize() == 0) {
- show_placeholder = true;
- search_text.setString("Search...");
- search_text.setFillColor(text_placeholder_color);
- }
- updated_search = true;
- time_since_search_update.restart();
- }
- } else if(event.text.unicode == 13 && !show_placeholder) { // Return
- body.reset_selected();
- search(search_text.getString(), &body, plugin);
- show_placeholder = true;
- search_text.setString("Search...");
- search_text.setFillColor(text_placeholder_color);
- } else if(event.text.unicode > 31) { // Non-control character
- if(show_placeholder) {
- show_placeholder = false;
- search_text.setString("");
- search_text.setFillColor(sf::Color::White);
- }
- sf::String str = search_text.getString();
- str += event.text.unicode;
- search_text.setString(str);
- updated_search = true;
- time_since_search_update.restart();
- }
- }
- }
-
- if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= 90) {
- updated_search = false;
- sf::String str = search_text.getString();
- if(show_placeholder)
- str.clear();
- update_search_suggestions(str, &body, plugin);
- }
-
- if(resized) {
- resized = false;
-
- float font_height = search_text.getCharacterSize() + 8.0f;
- float rect_height = font_height + search_background_margin_vertical * 2.0f;
- search_background.setSize(sf::Vector2f(window_size.x - padding_horizontal * 2.0f, rect_height));
- search_text.setPosition(padding_horizontal + search_background_margin_horizontal, padding_vertical + search_background_margin_vertical);
-
- float body_padding_horizontal = 50.0f;
- float body_padding_vertical = 50.0f;
- float body_width = window_size.x - body_padding_horizontal * 2.0f;
- if(body_width < 400) {
- body_width = window_size.x;
- body_padding_horizontal = 0.0f;
- }
- body_background.setPosition(body_padding_horizontal, search_background.getPosition().y + search_background.getSize().y + body_padding_vertical);
- body_background.setSize(sf::Vector2f(body_width, window_size.y));
- }
-
- window.clear(back_color);
- body.draw(window, body_background.getPosition(), body_background.getSize());
- window.draw(search_background);
- window.draw(search_text);
- window.display();
- }
-
+ XInitThreads();
+ QuickMedia::Program program;
+ program.run();
return 0;
}
-