diff --git a/application.cpp b/application.cpp index 410f3c2..7c8131c 100644 --- a/application.cpp +++ b/application.cpp @@ -26,6 +26,7 @@ static std::string user_home; +std::vector userFlags; std::vector channels; std::unordered_map> videos; @@ -515,6 +516,97 @@ void action_show_video_detail() { //message_box("Description", selected.description); } +void action_add_new_user_flag() { + std::string name = get_string("Flag name"); + if(name.empty()) + return; + userFlags.push_back(UserFlag::create(db, name)); +} + +void action_manage_user_flags() { + bool done = false; + + constexpr const char userflag_keys[] = "1234567890" + "abcdefghij" + "klmnopqrst" + "uv"; + const std::string userflag_keys_str(userflag_keys); + // +1 to accomodate for the 0 byte at the end + static_assert(sizeof(userflag_keys) == UserFlag::max_flag_count + 1, "There must be a key for each UserFlag"); + + size_t channel_name_width = 0; + for(const Channel &c: channels) { + channel_name_width = std::max(channel_name_width, c.tui_name_width); + } + + const char *box_chars[] = {"┬", "│", "┴"}; + size_t selected_channel = 0; + char key_buf[] = " "; + + std::vector actions = { + {TERMPAINT_EV_KEY, "F2", 0, action_add_new_user_flag, "Add new user flag"}, + {TERMPAINT_EV_KEY, "Escape", 0, [&]{ done = true; }, "Stop user flag management"}, + {TERMPAINT_EV_KEY, "ArrowUp", 0, [&]{ if(selected_channel > 0) selected_channel--; }, "Previous channel"}, + {TERMPAINT_EV_KEY, "ArrowDown", 0, [&]{ if(selected_channel < channels.size()) selected_channel++; }, "Next channel"}, + {EV_IGNORE, "1..0,a..v", 0, nullptr, "Toggle user flags for selected channel"}, + }; + + do { + size_t flag_name_width = 0; + for(const UserFlag &flag: userFlags) { + flag_name_width = std::max(flag_name_width, string_width(flag.name)); + } + + const size_t content_rows_needed = std::max(userFlags.size(), channels.size()); + const size_t box_cols = 1 + channel_name_width + 3 + 2 + flag_name_width + 1; + const size_t box_rows = 1 + content_rows_needed + 1; + + const size_t channel_name_pos = 1; + const size_t divider_pos = channel_name_pos + channel_name_width + 1; + const size_t flag_char_pos = divider_pos + 1; + const size_t flag_name_pos = flag_char_pos + 2; + + Channel ¤t_channel = channels[selected_channel]; + + draw_box_with_caption(0, 0, box_cols, box_rows); + for(size_t row=0; row 0) + (row+1 == box_rows)]; + termpaint_surface_write_with_attr(surface, divider_pos, row, box_char, get_attr(ASNormal, false)); + if(!current_channel.is_virtual && row < userFlags.size()) { + const UserFlag &flag = userFlags.at(row); + termpaint_attr *attr = get_attr(current_channel.user_flags & flag.id ? ASUnwatched : ASWatched, false); + key_buf[0] = userflag_keys[int(log2(flag.id))]; + termpaint_surface_write_with_attr(surface, flag_char_pos, 1 + row, key_buf, attr); + termpaint_surface_write_with_attr(surface, flag_name_pos, 1 + row, flag.name.c_str(), attr); + } + } + tp_flush(false); + + auto event = tp_wait_for_event(); + if(!event) + abort(); + + if(!tui_handle_action(*event, actions)) { + if(event->type == TERMPAINT_EV_CHAR && event->string.length() == 1) { + if(current_channel.is_virtual) + continue; + const size_t index = userflag_keys_str.find(event->string[0]); + if(index == std::string::npos) + continue; + if(std::find_if(userFlags.cbegin(), userFlags.cend(), + [&](const UserFlag &f) { return f.id == (1< load_json(const std::string &filename) { std::ifstream ifs(filename); @@ -622,6 +714,8 @@ static void run() std::chrono::system_clock::time_point last_user_action; db_init(database_filename); + + userFlags = UserFlag::get_all(db); make_virtual_unwatched_channel(); for(Channel &channel: Channel::get_all(db)) { add_channel_to_list(channel); @@ -657,6 +751,7 @@ static void run() {TERMPAINT_EV_KEY, "ArrowLeft", 0, action_scroll_title_left, "Scroll title left"}, {TERMPAINT_EV_KEY, "ArrowRight", 0, action_scroll_title_right, "Scroll title right"}, {TERMPAINT_EV_CHAR, "l", TERMPAINT_MOD_CTRL, [&](){ force_repaint = true; }, "Force redraw"}, + {TERMPAINT_EV_KEY, "F2", 0, action_manage_user_flags, "Manage user flags"}, }; bool draw = true; diff --git a/db.cpp b/db.cpp index 5772d24..9daf788 100644 --- a/db.cpp +++ b/db.cpp @@ -18,6 +18,11 @@ std::string get_string(sqlite3_stmt *row, int col) return std::string((char*)sqlite3_column_text(row, col)); } +int get_int(sqlite3_stmt *row, int col) +{ + return sqlite3_column_int(row, col); +} + void db_check_schema(); void db_init(const std::string &filename) diff --git a/db.h b/db.h index 6dfdc63..aac4db2 100644 --- a/db.h +++ b/db.h @@ -16,6 +16,7 @@ public: ~db_transaction(); }; std::string get_string(sqlite3_stmt *row, int col); +int get_int(sqlite3_stmt *row, int col); void db_init(const std::string &filename); void db_shutdown(); diff --git a/tui.cpp b/tui.cpp index 18217f6..92188bc 100644 --- a/tui.cpp +++ b/tui.cpp @@ -207,7 +207,7 @@ public: }; -static void draw_box_with_caption(int x, int y, int w, int h, const std::string &caption=std::string()) +void draw_box_with_caption(int x, int y, int w, int h, const std::string &caption) { termpaint_surface_clear_rect(surface, x, y, w, h, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR); const int fill = w - 2; diff --git a/tui.h b/tui.h index d11de9f..f60852e 100644 --- a/tui.h +++ b/tui.h @@ -79,6 +79,7 @@ void write_multiline_string(const int x, const int y, const std::string &str, te std::string text_wrap(const std::string &text, const size_t desired_width); Button message_box(const std::string &caption, const std::string &text, const Button buttons=Button::Ok, const Button default_button=Button::Ok, const Align align=Align::Center); +void draw_box_with_caption(int x, int y, int w, int h, const std::string &caption=std::string()); int get_selection(const std::string &caption, const std::vector &choices, size_t selected=0, const Align align=Align::Center); std::string get_string(const std::string &caption, const std::string &text=std::string(), const Align align=Align::Center); diff --git a/yt.cpp b/yt.cpp index adeba92..2d11ade 100644 --- a/yt.cpp +++ b/yt.cpp @@ -5,12 +5,68 @@ #include #include +#include + #include "tui.h" #include "db.h" using json = nlohmann::json; struct yt_config yt_config; +UserFlag::UserFlag(sqlite3_stmt *row): id(get_int(row, 0)), name(get_string(row, 1)) {} + +UserFlag UserFlag::create(sqlite3 *db, const std::string &name) +{ + int next_flag = next_free(db); + if(next_flag == -1) { + tui_abort("Out of UserFlags..."); + } + + sqlite3_stmt *query; + SC(sqlite3_prepare_v2(db, "INSERT INTO user_flags(flagId, name) values(?1, ?2);", -1, &query, nullptr)); + SC(sqlite3_bind_int(query, 1, next_flag)); + SC(sqlite3_bind_text(query, 2, name.c_str(), -1, nullptr)); + SC(sqlite3_step(query)); + SC(sqlite3_finalize(query)); + + return UserFlag(next_flag, name); +} + +int UserFlag::next_free(sqlite3 *db) +{ + int64_t flag = 1; + sqlite3_stmt *query; + SC(sqlite3_prepare_v2(db, "SELECT flagId FROM user_flags ORDER BY flagId;", -1, &query, nullptr)); + while(sqlite3_step(query) == SQLITE_ROW) { + const int fid = get_int(query, 0); + if(flag != fid) { + tui_abort("Invalid UserFlag " PRId64 ". Expected " PRId64, fid, flag); + } + flag <<= 1; + } + SC(sqlite3_finalize(query)); + + if(flag > (2L<<32)) + return -1; + return flag; +} + +std::vector UserFlag::get_all(sqlite3 *db) +{ + std::vector out; + + sqlite3_stmt *query; + SC(sqlite3_prepare_v2(db, "SELECT * FROM user_flags ORDER BY flagId;", -1, &query, nullptr)); + while(sqlite3_step(query) == SQLITE_ROW) { + out.emplace_back(query); + } + SC(sqlite3_finalize(query)); + + return out; +} + +UserFlag::UserFlag(int id, const std::string &name): id(id), name(name) {} + static size_t curl_writecallback(void *data, size_t size, size_t nmemb, void *userp) { size_t to_add = size * nmemb; @@ -68,11 +124,13 @@ static json api_request(const std::string &url, std::map extra_headers; } yt_config; +class UserFlag +{ +public: + int id; + std::string name; + + UserFlag(sqlite3_stmt *row); + + static UserFlag create(sqlite3 *db, const std::string &name); + static int next_free(sqlite3 *db); + static std::vector get_all(sqlite3 *db); + + static constexpr int max_flag_count = (sizeof(uint32_t)*8); + +private: + UserFlag(int id, const std::string &name); +}; + enum VideoFlag { kNone = 0, kWatched = (1<<0), @@ -30,6 +48,8 @@ public: VideoFlag virtual_flag; bool virtual_flag_value; + int user_flags; + Channel(sqlite3_stmt *row); static Channel add(sqlite3 *db, const std::string &selector, const std::string &value); static Channel add_virtual(const std::string &name, const VideoFlag virtual_flag=kNone, const bool virtual_flag_value=true); @@ -40,6 +60,8 @@ public: void load_info(sqlite3 *db); bool is_valid() const; + void save_user_flags(sqlite3 *db); + unsigned int unwatched; size_t tui_name_width; private: