Add support for setting user defined flags per channel

This commit is contained in:
Daniel Schulte 2021-07-18 16:11:16 +02:00
parent 697d09c635
commit b77cd9c227
7 changed files with 196 additions and 4 deletions

View File

@ -26,6 +26,7 @@
static std::string user_home;
std::vector<UserFlag> userFlags;
std::vector<Channel> channels;
std::unordered_map<std::string, std::vector<Video>> 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<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 &current_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;
std::optional<json> 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;

5
db.cpp
View File

@ -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)

1
db.h
View File

@ -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();

View File

@ -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;

1
tui.h
View File

@ -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<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);

74
yt.cpp
View File

@ -5,12 +5,68 @@
#include <nlohmann/json.hpp>
#include <curl/curl.h>
#include <cinttypes>
#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> 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)
{
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 {};
}
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"];
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, 2, channel_name.c_str(), -1, SQLITE_TRANSIENT));
sqlite3_step(query);
@ -272,6 +330,16 @@ bool Channel::is_valid() const
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)),
description(get_string(row, 3)), flags(sqlite3_column_int(row, 4)), published(get_string(row, 5)),
tui_title_width(0)

22
yt.h
View File

@ -15,6 +15,24 @@ extern struct yt_config {
std::map<std::string, std::string> 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<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 {
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: