aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AsyncImageLoader.cpp3
-rw-r--r--src/Body.cpp31
-rw-r--r--src/BodyItem.cpp7
-rw-r--r--src/Config.cpp16
-rw-r--r--src/Entry.cpp17
-rw-r--r--src/ImageViewer.cpp16
-rw-r--r--src/QuickMedia.cpp78
-rw-r--r--src/SearchBar.cpp29
-rw-r--r--src/Tabs.cpp31
-rw-r--r--src/Text.cpp27
-rw-r--r--src/VideoPlayer.cpp14
-rw-r--r--src/gui/Button.cpp13
-rw-r--r--src/plugins/LocalAnime.cpp392
-rw-r--r--src/plugins/LocalManga.cpp46
-rw-r--r--src/plugins/Matrix.cpp1
-rw-r--r--src/plugins/Page.cpp2
16 files changed, 577 insertions, 146 deletions
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index d7fbfbe..e6d0b52 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -16,7 +16,6 @@
#include <signal.h>
#include <malloc.h>
#include <assert.h>
-#include <cmath>
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "../external/stb/stb_image_resize.h"
@@ -258,6 +257,8 @@ namespace QuickMedia {
while(image_thumbnail_create_queue.is_running()) {
thumbnail_load_data_opt = image_thumbnail_create_queue.pop_if_available();
if(thumbnail_load_data_opt) {
+ // TODO: Do this multithreaded because creating thumbnails is pretty slow single-threaded,
+ // especially video thumbnails.
process_thumbnail(thumbnail_load_data_opt.value());
if(thumbnail_load_data_opt.value().thumbnail_data->loading_state == LoadingState::READY_TO_LOAD)
load_processed_thumbnail(thumbnail_load_data_opt.value());
diff --git a/src/Body.cpp b/src/Body.cpp
index 4681410..6fdb285 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -13,8 +13,8 @@
#include <mglpp/window/Window.hpp>
#include <mglpp/graphics/Shader.hpp>
#include <assert.h>
-#include <cmath>
#include <malloc.h>
+#include <cmath>
struct BodySpacing {
float spacing_y = 0.0f;
@@ -954,7 +954,7 @@ namespace QuickMedia {
if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0)
content_size = clamp_to_size(mgl::vec2i(std::floor(item->thumbnail_size.x * get_config().scale), std::floor(item->thumbnail_size.y * get_config().scale)), thumbnail_max_size_scaled);
else
- content_size = mgl::vec2i(250 * get_config().scale, 141 * get_config().scale);
+ content_size = clamp_to_size(mgl::vec2i(250 * get_config().scale, 141 * get_config().scale), thumbnail_max_size_scaled);
return content_size;
}
@@ -1365,6 +1365,7 @@ namespace QuickMedia {
const float padding_y = item_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only;
+ bool thumbnail_drawn = false;
float text_offset_x = body_spacing[body_theme].padding_x;
if(item_thumbnail && !merge_with_previous) {
// TODO: Verify if this is safe. The thumbnail is being modified in another thread
@@ -1390,6 +1391,7 @@ namespace QuickMedia {
text_offset_x += body_spacing[body_theme].image_padding_x + new_image_size.x;
// We want the next image fallback to have the same size as the successful image rendering, because its likely the image fallback will have the same size (for example thumbnails on youtube)
//image_fallback.set_size(mgl::vec2f(width_ratio * image_size.x, height_ratio * image_size.y));
+ thumbnail_drawn = true;
} else if(!item->thumbnail_url.empty()) {
mgl::vec2f content_size = thumbnail_size.to_vec2f();
@@ -1506,6 +1508,17 @@ namespace QuickMedia {
window.draw(*item->timestamp_text);
}
+ if(item->extra) {
+ Widgets widgets;
+ if(thumbnail_drawn) {
+ ThumbnailWidget thumbnail;
+ thumbnail.position = image.get_position();
+ thumbnail.size = image.get_texture()->get_size().to_vec2f() * image.get_scale();
+ widgets.thumbnail = std::move(thumbnail);
+ }
+ item->extra->draw_overlay(window, widgets);
+ }
+
if(!content_progress.isObject())
return;
@@ -1530,6 +1543,7 @@ namespace QuickMedia {
mgl::View new_view = { mgl::vec2i(0, scissor_y), mgl::vec2i(window_size.x, body_size.y) };
window.set_view(new_view);
+ bool thumbnail_drawn = false;
{
float image_height = 0.0f;
if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.is_valid()) {
@@ -1552,6 +1566,8 @@ namespace QuickMedia {
} else {
window.draw(image);
}
+
+ thumbnail_drawn = true;
} else if(!item->thumbnail_url.empty()) {
mgl::vec2f content_size = thumbnail_size.to_vec2f();
mgl::vec2f loading_icon_size(loading_icon.get_texture()->get_size().x, loading_icon.get_texture()->get_size().y);
@@ -1563,6 +1579,17 @@ namespace QuickMedia {
image_height = content_size.y;
}
+ if(item->extra) {
+ Widgets widgets;
+ if(thumbnail_drawn) {
+ ThumbnailWidget thumbnail;
+ thumbnail.position = image.get_position();
+ thumbnail.size = image.get_texture()->get_size().to_vec2f() * image.get_scale();
+ widgets.thumbnail = std::move(thumbnail);
+ }
+ item->extra->draw_overlay(window, widgets);
+ }
+
const float text_padding = item_thumbnail ? card_image_text_padding : 0.0f;
mgl::vec2f text_pos = mgl::vec2f(pos.x, scissor_y + body_spacing[body_theme].body_padding_vertical) + pos_offset + mgl::vec2f(card_padding_x, card_padding_y) + mgl::vec2f(0.0f, image_height + text_padding);
diff --git a/src/BodyItem.cpp b/src/BodyItem.cpp
index ebc3ead..0c6e1ce 100644
--- a/src/BodyItem.cpp
+++ b/src/BodyItem.cpp
@@ -1,9 +1,12 @@
#include "../include/BodyItem.hpp"
#include "../include/Theme.hpp"
#include "../include/Config.hpp"
-#include <cmath>
namespace QuickMedia {
+ static float floor(float v) {
+ return (int)v;
+ }
+
// static
std::shared_ptr<BodyItem> BodyItem::create(std::string title, bool selectable) {
return std::shared_ptr<BodyItem>(new BodyItem(std::move(title), selectable));
@@ -78,7 +81,7 @@ namespace QuickMedia {
void BodyItem::add_reaction(std::string text, void *userdata) {
Reaction reaction;
- reaction.text = std::make_unique<Text>(std::move(text), false, std::floor(get_config().body.reaction_font_size * get_config().scale * get_config().font_scale), 0.0f);
+ reaction.text = std::make_unique<Text>(std::move(text), false, floor(get_config().body.reaction_font_size * get_config().scale * get_config().font_scale), 0.0f);
reaction.userdata = userdata;
reactions.push_back(std::move(reaction));
}
diff --git a/src/Config.cpp b/src/Config.cpp
index e98f4c7..d6f826b 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -134,8 +134,12 @@ namespace QuickMedia {
const Json::Value &local_manga_json = json_root["local_manga"];
if(local_manga_json.isObject()) {
const Json::Value &directory_json = local_manga_json["directory"];
- if(directory_json.isString())
+ if(directory_json.isString()) {
config->local_manga.directory = directory_json.asString();
+ while(config->local_manga.directory.size() > 1 && config->local_manga.directory.back() == '/') {
+ config->local_manga.directory.pop_back();
+ }
+ }
const Json::Value &sort_by_name_json = local_manga_json["sort_by_name"];
if(sort_by_name_json.isBool())
@@ -149,16 +153,16 @@ namespace QuickMedia {
const Json::Value &local_anime_json = json_root["local_anime"];
if(local_anime_json.isObject()) {
const Json::Value &directory_json = local_anime_json["directory"];
- if(directory_json.isString())
+ if(directory_json.isString()) {
config->local_anime.directory = directory_json.asString();
+ while(config->local_anime.directory.size() > 1 && config->local_anime.directory.back() == '/') {
+ config->local_anime.directory.pop_back();
+ }
+ }
const Json::Value &sort_by_name_json = local_anime_json["sort_by_name"];
if(sort_by_name_json.isBool())
config->local_anime.sort_by_name = sort_by_name_json.asBool();
-
- const Json::Value &sort_episodes_by_name_json = local_anime_json["sort_episodes_by_name"];
- if(sort_episodes_by_name_json.isBool())
- config->local_anime.sort_episodes_by_name = sort_episodes_by_name_json.asBool();
}
const Json::Value &use_system_fonts_json = json_root["use_system_fonts"];
diff --git a/src/Entry.cpp b/src/Entry.cpp
index b196009..c3ecb40 100644
--- a/src/Entry.cpp
+++ b/src/Entry.cpp
@@ -7,17 +7,20 @@
#include <mglpp/window/Window.hpp>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Event.hpp>
-#include <math.h>
namespace QuickMedia {
- static const float background_margin_horizontal = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
- static const float padding_vertical = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
- static const float background_margin_vertical = std::floor(0.0f * get_config().scale * get_config().spacing_scale);
+ static float floor(float v) {
+ return (int)v;
+ }
+
+ static const float background_margin_horizontal = floor(5.0f * get_config().scale * get_config().spacing_scale);
+ static const float padding_vertical = floor(5.0f * get_config().scale * get_config().spacing_scale);
+ static const float background_margin_vertical = floor(0.0f * get_config().scale * get_config().spacing_scale);
Entry::Entry(const std::string &placeholder_text, mgl::Shader *rounded_rectangle_shader) :
on_submit_callback(nullptr),
draw_background(true),
- text("", false, std::floor(get_config().input.font_size * get_config().scale * get_config().font_scale), 0.0f),
+ text("", false, floor(get_config().input.font_size * get_config().scale * get_config().font_scale), 0.0f),
width(0.0f),
background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().selected_color, rounded_rectangle_shader),
placeholder(placeholder_text, *FontLoader::get_font(FontLoader::FontType::LATIN, get_config().input.font_size * get_config().scale * get_config().font_scale)),
@@ -107,7 +110,7 @@ namespace QuickMedia {
void Entry::set_position(const mgl::vec2f &pos) {
background.set_position(pos);
text.set_position(pos + mgl::vec2f(background_margin_horizontal, background_margin_vertical));
- placeholder.set_position(pos + mgl::vec2f(background_margin_horizontal, background_margin_vertical + std::floor(-1.0f * get_config().scale)));
+ placeholder.set_position(pos + mgl::vec2f(background_margin_horizontal, background_margin_vertical + floor(-1.0f * get_config().scale)));
}
void Entry::set_max_width(float width) {
@@ -121,7 +124,7 @@ namespace QuickMedia {
float Entry::get_height() {
text.updateGeometry();
- return std::floor(text.getHeight() + background_margin_vertical * 2.0f + padding_vertical * 2.0f);
+ return floor(text.getHeight() + background_margin_vertical * 2.0f + padding_vertical * 2.0f);
}
const std::string& Entry::get_text() const {
diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp
index 2b40c1f..d00c375 100644
--- a/src/ImageViewer.cpp
+++ b/src/ImageViewer.cpp
@@ -5,12 +5,12 @@
#include "../include/Scale.hpp"
#include "../include/Config.hpp"
#include <mglpp/system/FloatRect.hpp>
-#include <cmath>
#include <malloc.h>
#include <mglpp/window/Event.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/graphics/Image.hpp>
+#include <cmath>
namespace QuickMedia {
static const int page_text_character_size = 14 * get_config().scale * get_config().font_scale;
@@ -88,7 +88,7 @@ namespace QuickMedia {
std::shared_ptr<ImageData> &page_image_data = image_data[page];
const mgl::vec2d image_size = get_page_size(page);
- mgl::vec2d render_pos(std::floor(window_size.x * 0.5 - image_size.x * 0.5), scroll + offset_y);
+ mgl::vec2d render_pos(floor(window_size.x * 0.5 - image_size.x * 0.5), scroll + offset_y);
if(render_pos.y + image_size.y <= 0.0 || render_pos.y >= window_size.y) {
if(page_image_data)
page_image_data->visible_on_screen = false;
@@ -97,7 +97,7 @@ namespace QuickMedia {
bool scrolling = (std::abs(scroll_speed) > 0.01f);
if(!scrolling)
- render_pos.y = std::floor(render_pos.y);
+ render_pos.y = floor(render_pos.y);
double top_dist = std::abs(0.0 - render_pos.y);
if(top_dist < min_page_top_dist) {
@@ -141,10 +141,10 @@ namespace QuickMedia {
mgl::Text error_message(std::move(msg), *FontLoader::get_font(FontLoader::FontType::LATIN, 30 * get_config().scale * get_config().font_scale));
auto text_bounds = error_message.get_bounds();
error_message.set_color(mgl::Color(0, 0, 0, 255));
- mgl::vec2d render_pos_text(std::floor(window_size.x * 0.5 - text_bounds.size.x * 0.5), image_size.y * 0.5 - text_bounds.size.y * 0.5 + scroll + offset_y);
+ mgl::vec2d render_pos_text(floor(window_size.x * 0.5 - text_bounds.size.x * 0.5), image_size.y * 0.5 - text_bounds.size.y * 0.5 + scroll + offset_y);
if(!scrolling)
- render_pos_text.y = std::floor(render_pos_text.y);
+ render_pos_text.y = floor(render_pos_text.y);
mgl::Rectangle background(mgl::vec2f(image_size.x, image_size.y));
background.set_color(mgl::Color(255, 255, 255, 255));
@@ -160,10 +160,10 @@ namespace QuickMedia {
mgl::Text error_message("Downloading page " + page_str, *FontLoader::get_font(FontLoader::FontType::LATIN, 30 * get_config().scale * get_config().font_scale));
auto text_bounds = error_message.get_bounds();
error_message.set_color(mgl::Color(0, 0, 0, 255));
- mgl::vec2d render_pos_text(std::floor(window_size.x * 0.5 - text_bounds.size.x * 0.5), image_size.y * 0.5 - text_bounds.size.y * 0.5 + scroll + offset_y);
+ mgl::vec2d render_pos_text(floor(window_size.x * 0.5 - text_bounds.size.x * 0.5), image_size.y * 0.5 - text_bounds.size.y * 0.5 + scroll + offset_y);
if(!scrolling)
- render_pos_text.y = std::floor(render_pos_text.y);
+ render_pos_text.y = floor(render_pos_text.y);
mgl::Rectangle background(mgl::vec2f(image_size.x, image_size.y));
background.set_color(mgl::Color(255, 255, 255, 255));
@@ -367,7 +367,7 @@ namespace QuickMedia {
window->draw(page_text_background);
auto page_text_bounds = page_text.get_bounds();
- page_text.set_position(mgl::vec2f(std::floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), std::floor(window_size.y - background_height * 0.5f - font_height * 0.7f)));
+ page_text.set_position(mgl::vec2f(floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), floor(window_size.y - background_height * 0.5f - font_height * 0.7f)));
window->draw(page_text);
// Free pages that are not visible on the screen
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 99e8f1b..33bab93 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2,6 +2,7 @@
#include "../plugins/Manganelo.hpp"
#include "../plugins/Mangadex.hpp"
#include "../plugins/LocalManga.hpp"
+#include "../plugins/LocalAnime.hpp"
#include "../plugins/MangaGeneric.hpp"
#include "../plugins/MangaCombined.hpp"
#include "../plugins/MediaGeneric.hpp"
@@ -44,7 +45,6 @@
#include "../external/hash-library/sha256.h"
#include <assert.h>
-#include <cmath>
#include <string.h>
#include <signal.h>
#include <malloc.h>
@@ -52,6 +52,7 @@
#include <libgen.h>
#include <limits.h>
#include <sys/stat.h>
+#include <cmath>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/graphics/Sprite.hpp>
@@ -82,6 +83,7 @@ static const std::pair<const char*, const char*> valid_plugins[] = {
std::make_pair("onimanga", nullptr),
std::make_pair("readm", "readm_logo.png"),
std::make_pair("local-manga", nullptr),
+ std::make_pair("local-anime", nullptr),
std::make_pair("manga", nullptr),
std::make_pair("youtube", "yt_logo_rgb_dark_small.png"),
std::make_pair("peertube", "peertube_logo.png"),
@@ -283,7 +285,7 @@ namespace QuickMedia {
static void usage() {
fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir <directory>] [-e <window>] [youtube-url]\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
+ fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n");
fprintf(stderr, " --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n");
@@ -983,6 +985,7 @@ namespace QuickMedia {
create_launcher_body_item("AniList", "anilist", resources_root + "images/anilist_logo.png"),
create_launcher_body_item("Hot Examples", "hotexamples", ""),
create_launcher_body_item("Lbry", "lbry", resources_root + "icons/lbry_launcher.png"),
+ create_launcher_body_item("Local anime", "local-anime", ""),
create_launcher_body_item("Local manga", "local-manga", ""),
create_launcher_body_item("Manga (all)", "manga", ""),
create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"),
@@ -1079,13 +1082,28 @@ namespace QuickMedia {
} else if(strcmp(plugin_name, "local-manga") == 0) {
auto search_page = std::make_unique<LocalMangaSearchPage>(this, true);
- tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get(), true), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
- tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(false, true), std::make_unique<BookmarksPage>(this, search_page.get(), true), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.back().page.get(), HistoryType::MANGA, true);
- tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(false, true), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
start_tab_index = 1;
+ } else if(strcmp(plugin_name, "local-anime") == 0) {
+ if(get_config().local_anime.directory.empty()) {
+ show_notification("QuickMedia", "local_anime.directory config is not set", Urgency::CRITICAL);
+ exit(1);
+ }
+
+ if(get_file_type(get_config().local_anime.directory) != FileType::DIRECTORY) {
+ show_notification("QuickMedia", "local_anime.directory config is not set to a valid directory", Urgency::CRITICAL);
+ exit(1);
+ }
+
+ auto search_page = std::make_unique<LocalAnimeSearchPage>(this, get_config().local_anime.directory, LocalAnimeSearchPageType::DIRECTORY);
+ tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+
+ start_tab_index = 0;
} else if(strcmp(plugin_name, "manga") == 0) {
auto mangadex = std::make_unique<MangadexSearchPage>(this);
@@ -2684,6 +2702,9 @@ namespace QuickMedia {
PageType previous_page = pop_page_stack();
bool video_loaded = false;
+ double video_time_pos = 0.0; // Time in media in seconds. Updates every 5 seconds and when starting to watch the video and when seeking.
+ bool update_time_pos = false;
+ mgl::Clock video_time_pos_clock;
std::string youtube_video_id_dummy;
const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy);
@@ -2700,7 +2721,7 @@ namespace QuickMedia {
}
mgl::WindowHandle video_player_window = None;
- auto on_window_create = [this, &video_player_window, &video_page](mgl::WindowHandle _video_player_window) mutable {
+ auto on_window_create = [&](mgl::WindowHandle _video_player_window) mutable {
video_player_window = _video_player_window;
XSelectInput(disp, video_player_window, KeyPressMask | PointerMotionMask);
XSync(disp, False);
@@ -2709,6 +2730,9 @@ namespace QuickMedia {
video_page->get_subtitles(subtitle_data);
if(!subtitle_data.url.empty())
video_player->add_subtitle(subtitle_data.url, subtitle_data.title, "eng");
+
+ if(video_page->is_local())
+ update_time_pos = true;
};
std::unique_ptr<YoutubeMediaProxy> youtube_video_media_proxy;
@@ -2897,12 +2921,13 @@ namespace QuickMedia {
startup_args.audio_path = a;
startup_args.parent_window = window.get_system_handle();
startup_args.no_video = is_audio_only;
- startup_args.use_system_mpv_config = get_config().use_system_mpv_config;
- startup_args.use_system_input_config = false;
+ startup_args.use_system_mpv_config = get_config().use_system_mpv_config || video_page->is_local();
+ startup_args.use_system_input_config = video_page->is_local();
startup_args.keep_open = is_matrix && !is_youtube;
+ startup_args.resume = false;
startup_args.resource_root = resources_root;
startup_args.monitor_height = video_max_height;
- startup_args.use_youtube_dl = use_youtube_dl;
+ startup_args.use_youtube_dl = use_youtube_dl && !video_page->is_local();
startup_args.title = video_title;
startup_args.start_time = start_time;
startup_args.chapters = std::move(media_chapters);
@@ -2959,7 +2984,7 @@ namespace QuickMedia {
}
};
- video_event_callback = [&video_loaded](const char *event_name) mutable {
+ video_event_callback = [&](const char *event_name) mutable {
if(strcmp(event_name, "pause") == 0) {
//double time_remaining = 9999.0;
//if(video_player->get_time_remaining(&time_remaining) == VideoPlayer::Error::OK && time_remaining <= 1.0)
@@ -2968,10 +2993,15 @@ namespace QuickMedia {
//video_player->set_paused(false);
} else if(strcmp(event_name, "start-file") == 0) {
video_loaded = true;
+ if(video_page->is_local())
+ update_time_pos = true;
} else if(strcmp(event_name, "file-loaded") == 0) {
video_loaded = true;
} else if(strcmp(event_name, "video-reconfig") == 0 || strcmp(event_name, "audio-reconfig") == 0) {
video_loaded = true;
+ } else if(strcmp(event_name, "seek") == 0) {
+ if(video_page->is_local())
+ update_time_pos = true;
}
//fprintf(stderr, "event name: %s\n", event_name);
@@ -3018,9 +3048,9 @@ namespace QuickMedia {
}
} else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::F && event.key.control) {
window_set_fullscreen(disp, window.get_system_handle(), WindowFullscreenState::TOGGLE);
- } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::C && event.key.control) {
+ } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::C && event.key.control && !video_page->is_local()) {
save_video_url_to_clipboard();
- } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::F5) {
+ } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::F5 && !video_page->is_local()) {
load_video_error_check();
}
}
@@ -3044,13 +3074,13 @@ namespace QuickMedia {
}
} else if(pressed_keysym == XK_f && pressing_ctrl) {
window_set_fullscreen(disp, window.get_system_handle(), WindowFullscreenState::TOGGLE);
- } else if(pressed_keysym == XK_s && pressing_ctrl) {
+ } else if(pressed_keysym == XK_s && pressing_ctrl && !video_page->is_local()) {
video_page_download_video(video_page->get_download_url(video_get_max_height()), video_player_window);
- } else if(pressed_keysym == XK_F5) {
+ } else if(pressed_keysym == XK_F5 && !video_page->is_local()) {
double resume_start_time = 0.0;
video_player->get_time_in_file(&resume_start_time);
load_video_error_check(std::to_string((int)resume_start_time));
- } else if(pressed_keysym == XK_r && pressing_ctrl) {
+ } else if(pressed_keysym == XK_r && pressing_ctrl && !video_page->is_local()) {
bool cancelled = false;
if(video_tasks.valid()) {
XUnmapWindow(disp, video_player_window);
@@ -3201,7 +3231,8 @@ namespace QuickMedia {
// If there are no videos to play, then dont play any...
if(new_video_url.empty()) {
- show_notification("QuickMedia", "No more related videos to play");
+ if(!video_page->is_local())
+ show_notification("QuickMedia", "No more related videos to play");
current_page = previous_page;
go_to_previous_page = true;
break;
@@ -3250,6 +3281,18 @@ namespace QuickMedia {
continue;
}
+ if(video_player) {
+ if(video_time_pos_clock.get_elapsed_time_seconds() >= 5.0) {
+ video_time_pos_clock.restart();
+ update_time_pos = true;
+ }
+
+ if(update_time_pos) {
+ update_time_pos = false;
+ video_player->get_time_in_file(&video_time_pos);
+ }
+ }
+
if(video_player_window) {
if(!cursor_visible) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
@@ -3272,6 +3315,9 @@ namespace QuickMedia {
auto window_size_u = window.get_size();
window_size.x = window_size_u.x;
window_size.y = window_size_u.y;
+
+ if(video_page->is_local())
+ video_page->set_watch_progress(video_time_pos);
}
void Program::select_episode(BodyItem *item, bool start_from_beginning) {
diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp
index 6d790ad..71e662e 100644
--- a/src/SearchBar.cpp
+++ b/src/SearchBar.cpp
@@ -8,16 +8,19 @@
#include <mglpp/system/Utf8.hpp>
#include <mglpp/graphics/Texture.hpp>
#include <mglpp/window/Window.hpp>
-#include <cmath>
#include <assert.h>
// TODO: Use a seperate placeholder mgl::Text instead of switching the text to placeholder text....
namespace QuickMedia {
- static const float background_margin_horizontal = std::floor(15.0f * get_config().scale * get_config().spacing_scale);
- static const float padding_top_default = std::floor(10.0f * get_config().scale * get_config().spacing_scale);
- static const float padding_bottom_default = std::floor(15.0f * get_config().scale * get_config().spacing_scale);
- static const float background_margin_vertical = std::floor(4.0f * get_config().scale * get_config().spacing_scale);
+ static float floor(float v) {
+ return (int)v;
+ }
+
+ static const float background_margin_horizontal = floor(15.0f * get_config().scale);
+ static const float padding_top_default = floor(10.0f * get_config().scale * get_config().spacing_scale);
+ static const float padding_bottom_default = floor(15.0f * get_config().scale * get_config().spacing_scale);
+ static const float background_margin_vertical = floor(4.0f * get_config().scale * get_config().spacing_scale);
static const int character_size = get_config().search.font_size * get_config().scale * get_config().font_scale;
SearchBar::SearchBar(mgl::Texture *plugin_logo, mgl::Shader *rounded_rectangle_shader, const std::string &placeholder, bool input_masked) :
@@ -73,7 +76,7 @@ namespace QuickMedia {
} else {
window.draw(text);
if(show_placeholder || text.get_string().empty())
- caret.set_position(text.get_position() - mgl::vec2f(2.0f, 0.0f) + mgl::vec2f(0.0f, character_size * 0.4f));
+ caret.set_position(text.get_position() + mgl::vec2f(0.0f, character_size * 0.4f));
else
caret.set_position(text.find_character_pos(text.get_string().size()) + mgl::vec2f(0.0f, character_size * 0.4f));
}
@@ -156,30 +159,30 @@ namespace QuickMedia {
draw_logo = false;
float font_height = character_size + 7.0f;
- float rect_height = std::floor(font_height + background_margin_vertical * 2.0f);
+ float rect_height = floor(font_height + background_margin_vertical * 2.0f);
float offset_x;
if(draw_logo) {
- float one_line_height = std::floor(character_size + 8.0f + background_margin_vertical * 2.0f);
+ float one_line_height = floor(character_size + 8.0f + background_margin_vertical * 2.0f);
auto texture_size = plugin_logo_sprite.get_texture()->get_size();
mgl::vec2f texture_size_f(texture_size.x, texture_size.y);
mgl::vec2f new_size = wrap_to_size(texture_size_f, mgl::vec2f(200.0f, one_line_height));
plugin_logo_sprite.set_scale(get_ratio(texture_size_f, new_size));
plugin_logo_sprite.set_position(mgl::vec2f(pos.x + padding_x, pos.y + padding_top + rect_height * 0.5f - plugin_logo_sprite.get_texture()->get_size().y * plugin_logo_sprite.get_scale().y * 0.5f));
- offset_x = padding_x + new_size.x + std::floor(10.0f * get_config().spacing_scale);
+ offset_x = padding_x + new_size.x + floor(10.0f * get_config().spacing_scale);
} else {
offset_x = padding_x;
}
- const float width = std::floor(size.x - offset_x - padding_x);
+ const float width = floor(size.x - offset_x - padding_x);
background.set_size(mgl::vec2f(width, rect_height));
shade.set_size(mgl::vec2f(size.x, padding_top + rect_height + padding_bottom));
- caret.set_size(vec2f_floor(2.0f * get_config().scale, character_size + std::floor(2.0f * get_config().scale)));
+ caret.set_size(vec2f_floor(2.0f * get_config().scale, character_size + floor(2.0f * get_config().scale)));
background.set_position(mgl::vec2f(pos.x + offset_x, pos.y + padding_top));
shade.set_position(pos);
- mgl::vec2f font_position(std::floor(pos.x + offset_x + background_margin_horizontal), std::floor(pos.y + padding_top + background_margin_vertical - character_size * 0.3f));
+ mgl::vec2f font_position(floor(pos.x + offset_x + background_margin_horizontal), floor(pos.y + padding_top + background_margin_vertical - character_size * 0.3f));
text.set_position(font_position);
}
@@ -288,7 +291,7 @@ namespace QuickMedia {
float SearchBar::getBottomWithoutShadow() const {
float font_height = character_size + 7.0f;
- return std::floor(font_height + background_margin_vertical * 2.0f + padding_top + padding_bottom);
+ return floor(font_height + background_margin_vertical * 2.0f + padding_top + padding_bottom);
}
std::string SearchBar::get_text() const {
diff --git a/src/Tabs.cpp b/src/Tabs.cpp
index 6b33df1..213e11e 100644
--- a/src/Tabs.cpp
+++ b/src/Tabs.cpp
@@ -8,13 +8,16 @@
#include <mglpp/window/Event.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/graphics/Texture.hpp>
-#include <cmath>
namespace QuickMedia {
- static const float tab_text_size = std::floor(get_config().tab.font_size * get_config().scale * get_config().font_scale);
- static const float tab_height = tab_text_size + std::floor(10.0f * get_config().scale);
+ static float floor(float v) {
+ return (int)v;
+ }
+
+ static const float tab_text_size = floor(get_config().tab.font_size * get_config().scale * get_config().font_scale);
+ static const float tab_height = tab_text_size + floor(10.0f * get_config().scale);
static const float tab_min_width = 250.0f;
- static const float tab_margin_x = std::floor(10.0f * get_config().spacing_scale);
+ static const float tab_margin_x = floor(10.0f * get_config().spacing_scale);
// static
float Tabs::get_height() {
@@ -23,7 +26,7 @@ namespace QuickMedia {
// static
float Tabs::get_shade_height() {
- return tab_height + std::floor(10.0f * get_config().scale * get_config().spacing_scale);
+ return tab_height + floor(10.0f * get_config().scale * get_config().spacing_scale);
}
Tabs::Tabs(mgl::Shader *rounded_rectangle_shader, mgl::Color shade_color) : background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().selected_color, rounded_rectangle_shader), shade_color(shade_color) {
@@ -98,14 +101,14 @@ namespace QuickMedia {
container_width = width;
const int num_visible_tabs = std::min((int)tabs.size(), std::max(1, (int)(width / tab_min_width)));
- width_per_tab = std::floor(width / num_visible_tabs);
- const float tab_text_y = std::floor(pos.y + tab_height*0.5f - tab_text_size);
- tab_background_width = std::floor(width_per_tab - tab_margin_x*2.0f);
+ width_per_tab = floor(width / num_visible_tabs);
+ const float tab_text_y = floor(pos.y + tab_height*0.5f - tab_text_size);
+ tab_background_width = floor(width_per_tab - tab_margin_x*2.0f);
background.set_size(mgl::vec2f(tab_background_width, tab_height));
if(shade_color != mgl::Color(0, 0, 0, 0)) {
shade.set_size(mgl::vec2f(width, get_shade_height()));
- shade.set_position(mgl::vec2f(std::floor(pos.x), std::floor(pos.y)));
+ shade.set_position(mgl::vec2f(floor(pos.x), floor(pos.y)));
window.draw(shade);
}
@@ -131,7 +134,7 @@ namespace QuickMedia {
pos.x += scroll_fixed;
for(size_t i = 0; i < tabs.size(); ++i) {
const int index = i;
- const float background_pos_x = std::floor(pos.x + tab_index_to_x_offset(i));
+ const float background_pos_x = floor(pos.x + tab_index_to_x_offset(i));
if(background_pos_x - start_pos.x >= width - tab_margin_x) {
tabs_cutoff_right = true;
break;
@@ -141,12 +144,12 @@ namespace QuickMedia {
}
if((int)index == selected_tab) {
- background.set_position(mgl::vec2f(background_pos_x, std::floor(pos.y)));
+ background.set_position(mgl::vec2f(background_pos_x, floor(pos.y)));
background.draw(window);
}
mgl::Text &tab_text = tabs[index].text;
- float text_pos_x = std::floor(pos.x + i*width_per_tab + width_per_tab*0.5f - tab_text.get_bounds().size.x*0.5f);
+ float text_pos_x = floor(pos.x + i*width_per_tab + width_per_tab*0.5f - tab_text.get_bounds().size.x*0.5f);
text_pos_x = std::max(text_pos_x, background_pos_x);
window.set_view(create_scissor_view({ text_pos_x, tab_text_y }, { tab_background_width, tab_height }));
@@ -154,7 +157,7 @@ namespace QuickMedia {
window.set_view(prev_view);
}
- const float lw = std::floor(25.0f * get_config().scale);
+ const float lw = floor(25.0f * get_config().scale);
const float lh = background.get_size().y;
if(tabs_cutoff_left) {
@@ -224,6 +227,6 @@ namespace QuickMedia {
}
float Tabs::tab_index_to_x_offset(int index) {
- return std::floor(index*width_per_tab + tab_margin_x);
+ return floor(index*width_per_tab + tab_margin_x);
}
} \ No newline at end of file
diff --git a/src/Text.cpp b/src/Text.cpp
index 53118b8..383256a 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -9,10 +9,17 @@
#include <mglpp/graphics/Font.hpp>
#include <mglpp/graphics/Texture.hpp>
#include <mglpp/system/Utf8.hpp>
-#include <cmath>
namespace QuickMedia
{
+ static float floor(float v) {
+ return (int)v;
+ }
+
+ static float fabs(float v) {
+ return v >= 0.0 ? v : -v;
+ }
+
static const float TAB_WIDTH = 4.0f;
static const float WORD_WRAP_MIN_SIZE = 80.0f;
@@ -381,7 +388,7 @@ namespace QuickMedia
}
float Text::font_get_real_height(mgl::Font *font) {
- return font->get_glyph('|').size.y + std::floor(4.0f * ((float)characterSize / (float)14.0f));
+ return font->get_glyph('|').size.y + floor(4.0f * ((float)characterSize / (float)14.0f));
}
float Text::get_text_quad_left_side(const VertexRef &vertex_ref) const {
@@ -510,11 +517,11 @@ namespace QuickMedia
int vertexStart = vertices[vertices_index].size();
EmojiRectangle emoji_rec = emoji_get_extents(codepoint);
- const float font_height_offset = std::floor(-vspace * 0.2f);
- mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - std::floor(emoji_rec.height * emoji_scale) * 0.5f);
- mgl::vec2f vertexTopRight(glyphPos.x + std::floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset - std::floor(emoji_rec.height * emoji_scale) * 0.5f);
+ const float font_height_offset = floor(-vspace * 0.2f);
+ mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - floor(emoji_rec.height * emoji_scale) * 0.5f);
+ mgl::vec2f vertexTopRight(glyphPos.x + floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset - floor(emoji_rec.height * emoji_scale) * 0.5f);
mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + emoji_rec.height * emoji_scale * 0.5f);
- mgl::vec2f vertexBottomRight(glyphPos.x + std::floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset + std::floor(emoji_rec.height * emoji_scale) * 0.5f);
+ mgl::vec2f vertexBottomRight(glyphPos.x + floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset + floor(emoji_rec.height * emoji_scale) * 0.5f);
vertexTopLeft = vec2f_floor(vertexTopLeft);
vertexTopRight = vec2f_floor(vertexTopRight);
@@ -533,7 +540,7 @@ namespace QuickMedia
vertices[vertices_index].emplace_back(vertexBottomRight, textureBottomRight, emoji_color);
vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, emoji_color);
- glyphPos.x += std::floor(emoji_rec.width * emoji_scale) + characterSpacing;
+ glyphPos.x += floor(emoji_rec.width * emoji_scale) + characterSpacing;
vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint});
i += clen;
@@ -1133,12 +1140,12 @@ namespace QuickMedia
if(!editable) return true;
pos.y -= floor(vspace * 2.0f);
- const float caret_margin = std::floor(2.0f * get_config().scale);
+ const float caret_margin = floor(2.0f * get_config().scale);
- mgl::Rectangle caretRect(mgl::vec2f(0.0f, 0.0f), mgl::vec2f(std::floor(2.0f * get_config().scale), floor(vspace - caret_margin * 2.0f)));
+ mgl::Rectangle caretRect(mgl::vec2f(0.0f, 0.0f), mgl::vec2f(floor(2.0f * get_config().scale), floor(vspace - caret_margin * 2.0f)));
caretRect.set_position(mgl::vec2f(
floor(pos.x + caretPosition.x),
- floor(pos.y + caretPosition.y + caret_margin + std::floor(4.0f * get_config().scale))
+ floor(pos.y + caretPosition.y + caret_margin + floor(4.0f * get_config().scale))
));
target.draw(caretRect);
return true;
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index 4be0671..8fdf9dc 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -175,9 +175,7 @@ namespace QuickMedia {
args.insert(args.end(), {
video_player_filepath.c_str(),
"--cursor-autohide=no",
- "--save-position-on-quit=no",
"--profile=pseudo-gui", // For gui when playing audio, requires a version of mpv that isn't ancient
- "--resume-playback=no",
// TODO: Disable hr seek on low power devices?
"--hr-seek=yes",
"--force-seekable=yes",
@@ -199,6 +197,14 @@ namespace QuickMedia {
ipc_fd.c_str()
});
+ if(startup_args.resume) {
+ args.push_back("--save-position-on-quit=yes");
+ args.push_back("--resume-playback=yes");
+ } else {
+ args.push_back("--save-position-on-quit=no");
+ args.push_back("--resume-playback=no");
+ }
+
if(!startup_args.use_system_input_config)
args.push_back(input_conf.c_str());
@@ -226,7 +232,9 @@ namespace QuickMedia {
if(get_file_type(mpris_path) == FileType::REGULAR)
mpris_arg = "--scripts=" + mpris_path.data;
- if(!startup_args.use_system_mpv_config) {
+ if(startup_args.use_system_mpv_config) {
+ args.push_back("--config=yes");
+ } else {
args.insert(args.end(), {
"--config=no",
"--profile=gpu-hq",
diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp
index 13bc8c6..bf9c174 100644
--- a/src/gui/Button.cpp
+++ b/src/gui/Button.cpp
@@ -4,9 +4,12 @@
#include <mglpp/system/FloatRect.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
-#include <cmath>
namespace QuickMedia {
+ static float floor(float v) {
+ return (int)v;
+ }
+
static const float PADDING_Y = 5.0f;
Button::Button(const std::string &label, mgl::Font *font, float width, mgl::Shader *rounded_rectangle_shader, float scale) :
@@ -14,7 +17,7 @@ namespace QuickMedia {
background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().shade_color, rounded_rectangle_shader),
scale(scale)
{
- background.set_size(mgl::vec2f(std::floor(width * scale), get_height()));
+ background.set_size(mgl::vec2f(floor(width * scale), get_height()));
set_position(mgl::vec2f(0.0f, 0.0f));
}
@@ -60,8 +63,8 @@ namespace QuickMedia {
const auto label_bounds = label.get_bounds();
mgl::vec2f label_pos(pos + background.get_size() * 0.5f - label_bounds.size * 0.5f - mgl::vec2f(0.0f, 5.0f * scale));
- label_pos.x = std::floor(label_pos.x);
- label_pos.y = std::floor(label_pos.y);
+ label_pos.x = floor(label_pos.x);
+ label_pos.y = floor(label_pos.y);
label.set_position(label_pos);
}
@@ -74,6 +77,6 @@ namespace QuickMedia {
}
float Button::get_height() {
- return std::floor((PADDING_Y * 2.0f) * scale + label.get_bounds().size.y);
+ return floor((PADDING_Y * 2.0f) * scale + label.get_bounds().size.y);
}
} \ No newline at end of file
diff --git a/src/plugins/LocalAnime.cpp b/src/plugins/LocalAnime.cpp
index 4bc296a..9b1205a 100644
--- a/src/plugins/LocalAnime.cpp
+++ b/src/plugins/LocalAnime.cpp
@@ -1,42 +1,408 @@
#include "../../plugins/LocalAnime.hpp"
#include "../../include/Config.hpp"
+#include "../../include/Theme.hpp"
#include "../../include/Storage.hpp"
#include "../../include/Notification.hpp"
+#include "../../include/FileAnalyzer.hpp"
+#include "../../include/ResourceLoader.hpp"
+#include "../../include/StringUtils.hpp"
+#include <mglpp/graphics/Rectangle.hpp>
+#include <mglpp/window/Window.hpp>
+#include <json/value.h>
+#include <math.h>
namespace QuickMedia {
- static bool validate_local_anime_dir_config_is_set() {
- if(get_config().local_anime.directory.empty()) {
- show_notification("QuickMedia", "local_anime.directory config is not set", Urgency::CRITICAL);
- return false;
+ static const mgl::Color finished_watching_color = mgl::Color(43, 255, 47);
+
+ static float floor(float v) {
+ return (int)v;
+ }
+
+ class LocalAnimeBodyItemData : public BodyItemExtra {
+ public:
+ void draw_overlay(mgl::Window &render_target, const Widgets &widgets) override {
+ if(!std::holds_alternative<LocalAnimeEpisode>(anime_item) || !widgets.thumbnail)
+ return;
+
+ const int rect_height = 5;
+ const double watch_ratio = watch_progress.get_watch_ratio();
+
+ mgl::Rectangle watch_rect;
+ watch_rect.set_position({ widgets.thumbnail->position.x, widgets.thumbnail->position.y + widgets.thumbnail->size.y - rect_height });
+ watch_rect.set_size({ floor(widgets.thumbnail->size.x * watch_ratio), rect_height });
+ watch_rect.set_color(mgl::Color(255, 0, 0, 255));
+ render_target.draw(watch_rect);
+
+ mgl::Rectangle unwatch_rect;
+ unwatch_rect.set_position({ floor(widgets.thumbnail->position.x + widgets.thumbnail->size.x * watch_ratio), widgets.thumbnail->position.y + widgets.thumbnail->size.y - rect_height });
+ unwatch_rect.set_size({ floor(widgets.thumbnail->size.x * (1.0 - watch_ratio)), rect_height });
+ unwatch_rect.set_color(mgl::Color(255, 255, 255, 255));
+ render_target.draw(unwatch_rect);
+ }
+
+ LocalAnimeItem anime_item;
+ LocalAnimeWatchProgress watch_progress;
+ };
+
+ static std::vector<LocalAnimeItem> get_episodes_in_directory(const Path &directory) {
+ std::vector<LocalAnimeItem> episodes;
+ for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type) -> bool {
+ if(file_type != FileType::REGULAR || !is_video_ext(filepath.ext()))
+ return true;
+
+ time_t modified_time_seconds;
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
+ return true;
+
+ episodes.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ return true;
+ }, FileSortDirection::DESC);
+ return episodes;
+ }
+
+ static std::vector<LocalAnimeItem> get_episodes_or_seasons_in_directory(const Path &directory) {
+ std::vector<LocalAnimeItem> anime_items;
+ for_files_in_dir_sort_name(directory, [&](const Path &filepath, FileType file_type) -> bool {
+ time_t modified_time_seconds;
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
+ return true;
+
+ if(file_type == FileType::REGULAR) {
+ if(is_video_ext(filepath.ext()))
+ anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ return true;
+ }
+
+ LocalAnimeSeason season;
+ season.path = filepath;
+ season.episodes = get_episodes_in_directory(filepath);
+ season.modified_time_seconds = modified_time_seconds;
+ if(season.episodes.empty())
+ return true;
+
+ anime_items.push_back(std::move(season));
+ return true;
+ }, FileSortDirection::DESC);
+ return anime_items;
+ }
+
+ static std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory) {
+ std::vector<LocalAnimeItem> anime_items;
+ auto callback = [&anime_items](const Path &filepath, FileType file_type) -> bool {
+ time_t modified_time_seconds;
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
+ return true;
+
+ if(file_type == FileType::REGULAR) {
+ if(is_video_ext(filepath.ext()))
+ anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ return true;
+ }
+
+ LocalAnime anime;
+ anime.path = filepath;
+ anime.items = get_episodes_or_seasons_in_directory(filepath);
+ anime.modified_time_seconds = modified_time_seconds;
+ if(anime.items.empty())
+ return true;
+
+ anime_items.push_back(std::move(anime));
+ return true;
+ };
+
+ if(get_config().local_anime.sort_by_name)
+ for_files_in_dir_sort_name(directory, std::move(callback), FileSortDirection::ASC);
+ else
+ for_files_in_dir_sort_last_modified(directory, std::move(callback));
+
+ return anime_items;
+ }
+
+ static const LocalAnimeEpisode* get_latest_anime_item(const LocalAnimeItem &item) {
+ if(std::holds_alternative<LocalAnime>(item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(item);
+ return get_latest_anime_item(anime.items.front());
+ } else if(std::holds_alternative<LocalAnimeSeason>(item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item);
+ return get_latest_anime_item(season.episodes.front());
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ return &episode;
+ } else {
+ return nullptr;
+ }
+ }
+
+ static LocalAnimeWatchProgress get_watch_progress(const Json::Value &watched_json, const LocalAnimeItem &item) {
+ LocalAnimeWatchProgress progress;
+ Path latest_anime_path = get_latest_anime_item(item)->path;
+
+ std::string filename_relative_to_anime_dir = latest_anime_path.data.substr(get_config().local_anime.directory.size() + 1);
+ const Json::Value *found_watched_item = watched_json.find(
+ filename_relative_to_anime_dir.data(),
+ filename_relative_to_anime_dir.data() + filename_relative_to_anime_dir.size());
+ if(!found_watched_item || !found_watched_item->isObject())
+ return progress;
+
+ const Json::Value &time_json = (*found_watched_item)["time"];
+ const Json::Value &duration_json = (*found_watched_item)["duration"];
+ if(!time_json.isInt64() || !duration_json.isInt64() || duration_json.asInt64() == 0)
+ return progress;
+
+ // We consider having watched the anime if the user stopped watching 90% in, because they might skip the ending theme/credits
+ progress.time = (double)time_json.asInt64();
+ progress.duration = (double)duration_json.asInt64();
+ return progress;
+ }
+
+ enum class WatchedStatus {
+ WATCHED,
+ NOT_WATCHED
+ };
+
+ static bool toggle_watched_save_to_file(const Path &filepath, WatchedStatus &watched_status) {
+ Path local_anime_progress_path = get_storage_dir().join("watch-progress").join("local-anime");
+
+ Json::Value json_root;
+ if(!read_file_as_json(local_anime_progress_path, json_root) || !json_root.isObject())
+ json_root = Json::Value(Json::objectValue);
+
+ bool watched = false;
+ std::string filename_relative_to_anime_dir = filepath.data.substr(get_config().local_anime.directory.size() + 1);
+ Json::Value &watched_item = json_root[filename_relative_to_anime_dir];
+ if(watched_item.isObject()) {
+ watched = true;
+ } else {
+ watched_item = Json::Value(Json::objectValue);
+ watched = false;
+ }
+
+ if(watched) {
+ json_root.removeMember(filename_relative_to_anime_dir.c_str());
+ } else {
+ FileAnalyzer file_analyzer;
+ if(!file_analyzer.load_file(filepath.data.c_str(), true)) {
+ show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as watched", Urgency::CRITICAL);
+ return false;
+ }
+
+ if(!file_analyzer.get_duration_seconds() || *file_analyzer.get_duration_seconds() == 0) {
+ show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as watched", Urgency::CRITICAL);
+ return false;
+ }
+
+ watched_item["time"] = (int64_t)*file_analyzer.get_duration_seconds();
+ watched_item["duration"] = (int64_t)*file_analyzer.get_duration_seconds();
+ watched_item["thumbnail_url"] = filename_relative_to_anime_dir;
+ watched_item["timestamp"] = (int64_t)time(nullptr);
}
- if(get_file_type(get_config().local_anime.directory) != FileType::DIRECTORY) {
- show_notification("QuickMedia", "local_anime.directory config is not set to a valid directory", Urgency::CRITICAL);
+ if(!save_json_to_file_atomic(local_anime_progress_path, json_root)) {
+ show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as " + (watched ? "not watched" : "watched"), Urgency::CRITICAL);
return false;
}
+ watched_status = watched ? WatchedStatus::NOT_WATCHED : WatchedStatus::WATCHED;
return true;
}
+ double LocalAnimeWatchProgress::get_watch_ratio() const {
+ if(duration == 0.0)
+ return 0.0;
+ return (double)time / (double)duration;
+ }
+
+ // We consider having watched the anime if the user stopped watching 90% in, because they might skip the ending theme/credits
+ bool LocalAnimeWatchProgress::has_finished_watching() const {
+ return get_watch_ratio() >= 0.9;
+ }
+
PluginResult LocalAnimeSearchPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
- if(!validate_local_anime_dir_config_is_set())
+ LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(args.extra.get());
+ if(std::holds_alternative<LocalAnime>(item_data->anime_item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(item_data->anime_item);
+ result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, anime.path.data, LocalAnimeSearchPageType::ANIME), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
return PluginResult::OK;
-
+ } else if(std::holds_alternative<LocalAnimeSeason>(item_data->anime_item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item_data->anime_item);
+ result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, season.path.data, LocalAnimeSearchPageType::SEASON), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
+ return PluginResult::OK;
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item_data->anime_item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item_data->anime_item);
+ result_tabs.push_back(Tab{ nullptr, std::make_unique<LocalAnimeVideoPage>(program, episode.path.data, item_data->watch_progress), nullptr });
+ return PluginResult::OK;
+ }
return PluginResult::ERR;
}
PluginResult LocalAnimeSearchPage::lazy_fetch(BodyItems &result_items) {
- if(!validate_local_anime_dir_config_is_set())
- return PluginResult::OK;
+ Json::Value json_root;
+ if(!read_file_as_json(get_storage_dir().join("watch-progress").join("local-anime"), json_root) || !json_root.isObject())
+ json_root = Json::Value(Json::objectValue);
- return PluginResult::ERR;
+ std::vector<LocalAnimeItem> anime_items;
+ switch(type) {
+ case LocalAnimeSearchPageType::DIRECTORY:
+ anime_items = get_anime_in_directory(directory);
+ break;
+ case LocalAnimeSearchPageType::ANIME:
+ anime_items = get_episodes_or_seasons_in_directory(directory);
+ break;
+ case LocalAnimeSearchPageType::SEASON:
+ anime_items = get_episodes_in_directory(directory);
+ break;
+ }
+
+ const time_t time_now = time(nullptr);
+ for(LocalAnimeItem &anime_item : anime_items) {
+ auto body_item_data = std::make_shared<LocalAnimeBodyItemData>();
+ body_item_data->watch_progress = get_watch_progress(json_root, anime_item);
+ const bool has_finished_watching = body_item_data->watch_progress.has_finished_watching();
+
+ if(std::holds_alternative<LocalAnime>(anime_item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(anime_item);
+
+ std::string title;
+ if(has_finished_watching)
+ title = "[Finished watching] ";
+ title += anime.path.filename();
+
+ auto body_item = BodyItem::create(std::move(title));
+ if(has_finished_watching)
+ body_item->set_title_color(finished_watching_color);
+
+ body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - anime.modified_time_seconds));
+ body_item->set_description_color(get_theme().faded_text_color);
+
+ body_item->url = anime.path.data;
+
+ body_item_data->anime_item = std::move(anime_item);
+ body_item->extra = std::move(body_item_data);
+ result_items.push_back(std::move(body_item));
+ } else if(std::holds_alternative<LocalAnimeSeason>(anime_item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(anime_item);
+
+ std::string title;
+ if(has_finished_watching)
+ title = "[Finished watching] ";
+ title += season.path.filename();
+
+ auto body_item = BodyItem::create(std::move(title));
+ if(has_finished_watching)
+ body_item->set_title_color(finished_watching_color);
+
+ body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - season.modified_time_seconds));
+ body_item->set_description_color(get_theme().faded_text_color);
+
+ body_item->url = season.path.data;
+
+ body_item_data->anime_item = std::move(anime_item);
+ body_item->extra = std::move(body_item_data);
+ result_items.push_back(std::move(body_item));
+ } else if(std::holds_alternative<LocalAnimeEpisode>(anime_item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(anime_item);
+
+ std::string title;
+ if(has_finished_watching)
+ title = "[Finished watching] ";
+ title += episode.path.filename();
+
+ auto body_item = BodyItem::create(std::move(title));
+ if(has_finished_watching)
+ body_item->set_title_color(finished_watching_color);
+
+ body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - episode.modified_time_seconds));
+ body_item->set_description_color(get_theme().faded_text_color);
+
+ body_item->url = episode.path.data;
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_url = episode.path.data;
+
+ body_item_data->anime_item = std::move(anime_item);
+ body_item->extra = std::move(body_item_data);
+ result_items.push_back(std::move(body_item));
+ }
+ }
+
+ return PluginResult::OK;
}
std::shared_ptr<BodyItem> LocalAnimeSearchPage::get_bookmark_body_item(BodyItem *selected_item) {
- return nullptr;
+ if(!selected_item)
+ return nullptr;
+
+ std::string filename_relative_to_anime_dir = selected_item->url.substr(get_config().local_anime.directory.size() + 1);
+ auto body_item = BodyItem::create(filename_relative_to_anime_dir);
+ body_item->url = filename_relative_to_anime_dir;
+ body_item->thumbnail_url = selected_item->thumbnail_url;
+ return body_item;
}
void LocalAnimeSearchPage::toggle_read(BodyItem *selected_item) {
- // TODO:
+ if(!selected_item)
+ return;
+
+ LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(selected_item->extra.get());
+ WatchedStatus watch_status;
+ if(!toggle_watched_save_to_file(get_latest_anime_item(item_data->anime_item)->path, watch_status))
+ return;
+
+ mgl::Color color = get_theme().text_color;
+ std::string title;
+ if(watch_status == WatchedStatus::WATCHED) {
+ title = "[Finished watching] ";
+ color = finished_watching_color;
+ }
+ title += Path(selected_item->url).filename();
+
+ selected_item->set_title(std::move(title));
+ selected_item->set_title_color(color);
+ }
+
+ std::string LocalAnimeVideoPage::get_video_url(int, bool &has_embedded_audio, std::string &ext) {
+ ext = Path(url).ext();
+ has_embedded_audio = true;
+ return url;
+ }
+
+ std::string LocalAnimeVideoPage::get_url_timestamp() {
+ // If we are very close to the end then start from the beginning.
+ // This is the same behavior as mpv.
+ // This is better because we dont want the video player to stop immediately after we start playing and we dont get any chance to seek.
+ if(watch_progress.time + 10.0 >= watch_progress.duration)
+ return "0";
+ else
+ return std::to_string(watch_progress.time);
+ }
+
+ void LocalAnimeVideoPage::set_watch_progress(double time_pos_sec) {
+ std::string filename_relative_to_anime_dir = url.substr(get_config().local_anime.directory.size() + 1);
+
+ Path watch_progress_dir = get_storage_dir().join("watch-progress");
+ if(create_directory_recursive(watch_progress_dir) != 0) {
+ show_notification("QuickMedia", "Failed to create " + watch_progress_dir.data + " to set watch progress for " + filename_relative_to_anime_dir, Urgency::CRITICAL);
+ return;
+ }
+
+ Path local_anime_progress_path = watch_progress_dir;
+ local_anime_progress_path.join("local-anime");
+
+ Json::Value json_root;
+ if(!read_file_as_json(local_anime_progress_path, json_root) || !json_root.isObject())
+ json_root = Json::Value(Json::objectValue);
+
+ Json::Value watch_progress_json(Json::objectValue);
+ watch_progress_json["time"] = (int64_t)time_pos_sec;
+ watch_progress_json["duration"] = (int64_t)watch_progress.duration;
+ watch_progress_json["thumbnail_url"] = filename_relative_to_anime_dir;
+ watch_progress_json["timestamp"] = (int64_t)time(nullptr);
+ json_root[filename_relative_to_anime_dir] = std::move(watch_progress_json);
+
+ if(!save_json_to_file_atomic(local_anime_progress_path, json_root)) {
+ show_notification("QuickMedia", "Failed to set watch progress for " + filename_relative_to_anime_dir, Urgency::CRITICAL);
+ return;
+ }
+
+ fprintf(stderr, "Set watch progress for \"%s\" to %d/%d\n", filename_relative_to_anime_dir.c_str(), (int)time_pos_sec, (int)watch_progress.duration);
}
} \ No newline at end of file
diff --git a/src/plugins/LocalManga.cpp b/src/plugins/LocalManga.cpp
index 4367401..3fc7269 100644
--- a/src/plugins/LocalManga.cpp
+++ b/src/plugins/LocalManga.cpp
@@ -9,6 +9,8 @@
#include <json/value.h>
#include <dirent.h>
+// TODO: Make thumbnail paths in history and thumbnail-link relative to local_manga.directory
+
namespace QuickMedia {
// This is needed because the manga may be stored on NFS.
// TODO: Remove once body items can async load when visible on screen
@@ -418,47 +420,6 @@ namespace QuickMedia {
selected_item->set_title_color(color);
}
- static std::unordered_set<std::string> get_lines_in_file(const Path &filepath) {
- std::unordered_set<std::string> lines;
-
- std::string file_content;
- if(file_get_content(filepath, file_content) != 0)
- return lines;
-
- string_split(file_content, '\n', [&lines](const char *str_part, size_t size) {
- lines.insert(std::string(str_part, size));
- return true;
- });
-
- return lines;
- }
-
- static bool append_seen_manga_to_automedia_seen(const std::string &manga_chapter_name) {
- Path automedia_config_dir = get_home_dir().join(".config").join("automedia");
- if(create_directory_recursive(automedia_config_dir) != 0) {
- fprintf(stderr, "Warning: failed to create directory: %s\n", automedia_config_dir.data.c_str());
- return false;
- }
-
- Path automedia_seen_filepath = automedia_config_dir;
- automedia_seen_filepath.join("seen");
-
- std::unordered_set<std::string> lines = get_lines_in_file(automedia_seen_filepath);
- if(lines.find(manga_chapter_name) != lines.end())
- return true; // Already seen
-
- FILE *file = fopen(automedia_seen_filepath.data.c_str(), "ab");
- if(!file) {
- fprintf(stderr, "Warning: failed to open automedia seen file %s\n", automedia_seen_filepath.data.c_str());
- return false;
- }
-
- std::string new_line_data = manga_chapter_name + "\n";
- fwrite(new_line_data.data(), 1, new_line_data.size(), file);
- fclose(file);
- return true;
- }
-
PluginResult LocalMangaChaptersPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
if(!validate_local_manga_dir_config_is_set())
return PluginResult::OK;
@@ -484,9 +445,6 @@ namespace QuickMedia {
chapter_image_urls.push_back(local_manga_page.path.data);
}
- if(is_program_executable_by_name("automedia"))
- append_seen_manga_to_automedia_seen(manga_name + "/" + url);
-
num_images = chapter_image_urls.size();
return ImageResult::OK;
}
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 367c777..144bbd8 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -14,7 +14,6 @@
#include <rapidjson/stringbuffer.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/filewritestream.h>
-#include <cmath>
#include <fcntl.h>
#include <unistd.h>
#include <malloc.h>
diff --git a/src/plugins/Page.cpp b/src/plugins/Page.cpp
index c2e8060..654c983 100644
--- a/src/plugins/Page.cpp
+++ b/src/plugins/Page.cpp
@@ -123,7 +123,7 @@ namespace QuickMedia {
if(thumbnail_url_json.isString()) {
body_item->thumbnail_url = thumbnail_url_json.asString();
- body_item->thumbnail_size = {101, 141};
+ body_item->thumbnail_size = thumbnail_size;
body_item->thumbnail_is_local = local_thumbnail;
}