Add support for setting user defined flags per channel
This commit is contained in:
parent
697d09c635
commit
b77cd9c227
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
static std::string user_home;
|
static std::string user_home;
|
||||||
|
|
||||||
|
std::vector<UserFlag> userFlags;
|
||||||
std::vector<Channel> channels;
|
std::vector<Channel> channels;
|
||||||
std::unordered_map<std::string, std::vector<Video>> videos;
|
std::unordered_map<std::string, std::vector<Video>> videos;
|
||||||
|
|
||||||
|
@ -515,6 +516,97 @@ void action_show_video_detail() {
|
||||||
//message_box("Description", selected.description);
|
//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<action> 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<box_rows; row++) {
|
||||||
|
if(row<channels.size()) {
|
||||||
|
termpaint_surface_write_with_attr(surface, channel_name_pos, 1 + row, channels[row].name.c_str(), get_attr(ASNormal, selected_channel == row));
|
||||||
|
}
|
||||||
|
const char *box_char = box_chars[(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<<index); }) == userFlags.cend())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
current_channel.user_flags ^= (1 << index);
|
||||||
|
current_channel.save_user_flags(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!done);
|
||||||
|
}
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
std::optional<json> load_json(const std::string &filename) {
|
std::optional<json> load_json(const std::string &filename) {
|
||||||
std::ifstream ifs(filename);
|
std::ifstream ifs(filename);
|
||||||
|
@ -622,6 +714,8 @@ static void run()
|
||||||
std::chrono::system_clock::time_point last_user_action;
|
std::chrono::system_clock::time_point last_user_action;
|
||||||
|
|
||||||
db_init(database_filename);
|
db_init(database_filename);
|
||||||
|
|
||||||
|
userFlags = UserFlag::get_all(db);
|
||||||
make_virtual_unwatched_channel();
|
make_virtual_unwatched_channel();
|
||||||
for(Channel &channel: Channel::get_all(db)) {
|
for(Channel &channel: Channel::get_all(db)) {
|
||||||
add_channel_to_list(channel);
|
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, "ArrowLeft", 0, action_scroll_title_left, "Scroll title left"},
|
||||||
{TERMPAINT_EV_KEY, "ArrowRight", 0, action_scroll_title_right, "Scroll title right"},
|
{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_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;
|
bool draw = true;
|
||||||
|
|
5
db.cpp
5
db.cpp
|
@ -18,6 +18,11 @@ std::string get_string(sqlite3_stmt *row, int col)
|
||||||
return std::string((char*)sqlite3_column_text(row, 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_check_schema();
|
||||||
|
|
||||||
void db_init(const std::string &filename)
|
void db_init(const std::string &filename)
|
||||||
|
|
1
db.h
1
db.h
|
@ -16,6 +16,7 @@ public:
|
||||||
~db_transaction();
|
~db_transaction();
|
||||||
};
|
};
|
||||||
std::string get_string(sqlite3_stmt *row, int col);
|
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_init(const std::string &filename);
|
||||||
void db_shutdown();
|
void db_shutdown();
|
||||||
|
|
2
tui.cpp
2
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);
|
termpaint_surface_clear_rect(surface, x, y, w, h, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
|
||||||
const int fill = w - 2;
|
const int fill = w - 2;
|
||||||
|
|
1
tui.h
1
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);
|
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);
|
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<std::string> &choices, size_t selected=0, const Align align=Align::Center);
|
int get_selection(const std::string &caption, const std::vector<std::string> &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);
|
std::string get_string(const std::string &caption, const std::string &text=std::string(), const Align align=Align::Center);
|
||||||
|
|
||||||
|
|
74
yt.cpp
74
yt.cpp
|
@ -5,12 +5,68 @@
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
#include <cinttypes>
|
||||||
|
|
||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
#include "db.h"
|
#include "db.h"
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
struct yt_config yt_config;
|
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> UserFlag::get_all(sqlite3 *db)
|
||||||
|
{
|
||||||
|
std::vector<UserFlag> 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)
|
static size_t curl_writecallback(void *data, size_t size, size_t nmemb, void *userp)
|
||||||
{
|
{
|
||||||
size_t to_add = size * nmemb;
|
size_t to_add = size * nmemb;
|
||||||
|
@ -68,11 +124,13 @@ static json api_request(const std::string &url, std::map<std::string, std::strin
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
Channel::Channel(sqlite3_stmt *row): id(get_string(row, 0)), name(get_string(row, 1)), is_virtual(false), virtual_flag(kNone), virtual_flag_value(false), unwatched(0), tui_name_width(0)
|
Channel::Channel(sqlite3_stmt *row): id(get_string(row, 0)), name(get_string(row, 1)), is_virtual(false),
|
||||||
|
virtual_flag(kNone), virtual_flag_value(false), user_flags(get_int(row, 2)), unwatched(0), tui_name_width(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Channel::Channel(const std::string &id, const std::string &name): id(id), name(name), is_virtual(false), virtual_flag(kNone), virtual_flag_value(false), unwatched(0), tui_name_width(0)
|
Channel::Channel(const std::string &id, const std::string &name): id(id), name(name), is_virtual(false),
|
||||||
|
virtual_flag(kNone), virtual_flag_value(false), user_flags(0), unwatched(0), tui_name_width(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +160,7 @@ Channel Channel::add(sqlite3 *db, const std::string &selector, const std::string
|
||||||
const std::string channel_name = response["items"][0]["snippet"]["title"];
|
const std::string channel_name = response["items"][0]["snippet"]["title"];
|
||||||
|
|
||||||
sqlite3_stmt *query;
|
sqlite3_stmt *query;
|
||||||
SC(sqlite3_prepare_v2(db, "INSERT INTO channels(channelId, name) VALUES(?1, ?2);", -1, &query, nullptr));
|
SC(sqlite3_prepare_v2(db, "INSERT INTO channels(channelId, name, user_flags) VALUES(?1, ?2, 0);", -1, &query, nullptr));
|
||||||
SC(sqlite3_bind_text(query, 1, channel_id.c_str(), -1, SQLITE_TRANSIENT));
|
SC(sqlite3_bind_text(query, 1, channel_id.c_str(), -1, SQLITE_TRANSIENT));
|
||||||
SC(sqlite3_bind_text(query, 2, channel_name.c_str(), -1, SQLITE_TRANSIENT));
|
SC(sqlite3_bind_text(query, 2, channel_name.c_str(), -1, SQLITE_TRANSIENT));
|
||||||
sqlite3_step(query);
|
sqlite3_step(query);
|
||||||
|
@ -272,6 +330,16 @@ bool Channel::is_valid() const
|
||||||
return !id.empty() && !name.empty();
|
return !id.empty() && !name.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Channel::save_user_flags(sqlite3 *db)
|
||||||
|
{
|
||||||
|
sqlite3_stmt *query;
|
||||||
|
SC(sqlite3_prepare_v2(db, "UPDATE channels SET user_flags = ?2 WHERE channelID = ?1;", -1, &query, nullptr));
|
||||||
|
SC(sqlite3_bind_text(query, 1, id.c_str(), -1, SQLITE_TRANSIENT));
|
||||||
|
SC(sqlite3_bind_int(query, 2, user_flags));
|
||||||
|
SC(sqlite3_step(query));
|
||||||
|
SC(sqlite3_finalize(query));
|
||||||
|
}
|
||||||
|
|
||||||
Video::Video(sqlite3_stmt *row): id(get_string(row, 0)), channel_id(get_string(row, 1)), title(get_string(row, 2)),
|
Video::Video(sqlite3_stmt *row): id(get_string(row, 0)), channel_id(get_string(row, 1)), title(get_string(row, 2)),
|
||||||
description(get_string(row, 3)), flags(sqlite3_column_int(row, 4)), published(get_string(row, 5)),
|
description(get_string(row, 3)), flags(sqlite3_column_int(row, 4)), published(get_string(row, 5)),
|
||||||
tui_title_width(0)
|
tui_title_width(0)
|
||||||
|
|
22
yt.h
22
yt.h
|
@ -15,6 +15,24 @@ extern struct yt_config {
|
||||||
std::map<std::string, std::string> extra_headers;
|
std::map<std::string, std::string> extra_headers;
|
||||||
} yt_config;
|
} 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<UserFlag> get_all(sqlite3 *db);
|
||||||
|
|
||||||
|
static constexpr int max_flag_count = (sizeof(uint32_t)*8);
|
||||||
|
|
||||||
|
private:
|
||||||
|
UserFlag(int id, const std::string &name);
|
||||||
|
};
|
||||||
|
|
||||||
enum VideoFlag {
|
enum VideoFlag {
|
||||||
kNone = 0,
|
kNone = 0,
|
||||||
kWatched = (1<<0),
|
kWatched = (1<<0),
|
||||||
|
@ -30,6 +48,8 @@ public:
|
||||||
VideoFlag virtual_flag;
|
VideoFlag virtual_flag;
|
||||||
bool virtual_flag_value;
|
bool virtual_flag_value;
|
||||||
|
|
||||||
|
int user_flags;
|
||||||
|
|
||||||
Channel(sqlite3_stmt *row);
|
Channel(sqlite3_stmt *row);
|
||||||
static Channel add(sqlite3 *db, const std::string &selector, const std::string &value);
|
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);
|
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);
|
void load_info(sqlite3 *db);
|
||||||
bool is_valid() const;
|
bool is_valid() const;
|
||||||
|
|
||||||
|
void save_user_flags(sqlite3 *db);
|
||||||
|
|
||||||
unsigned int unwatched;
|
unsigned int unwatched;
|
||||||
size_t tui_name_width;
|
size_t tui_name_width;
|
||||||
private:
|
private:
|
||||||
|
|
Loading…
Reference in New Issue