2020-11-22 15:17:14 +00:00
# define _X_OPEN_SOURCE
# include "tui.h"
# include "yt.h"
2020-11-22 18:18:45 +00:00
# include "db.h"
2020-11-26 18:30:26 +00:00
# include "subprocess.h"
2020-11-22 15:17:14 +00:00
# include <algorithm>
# include <cstdlib>
# include <cstring>
# include <ctime>
# include <unordered_map>
# include <fstream>
# include <time.h>
2020-11-26 18:41:48 +00:00
# include <libgen.h>
2020-11-22 15:17:14 +00:00
# include <stdio.h>
# include <curl/curl.h>
# include <nlohmann/json.hpp>
static std : : string user_home ;
std : : vector < Channel > channels ;
std : : unordered_map < std : : string , std : : vector < Video > > videos ;
2020-11-26 12:27:11 +00:00
size_t selected_channel ;
2020-11-22 15:17:14 +00:00
size_t current_video_count = 0 ;
size_t selected_video = 0 ;
size_t videos_per_page = 0 ;
size_t current_page_count = 0 ;
size_t title_offset = 0 ;
bool any_title_in_next_half = false ;
2020-11-26 12:27:11 +00:00
bool clear_channels_on_change = false ;
2020-11-22 15:17:14 +00:00
static termpaint_attr * get_attr ( const AttributeSetType type , const bool highlight = false )
{
return highlight ? attributes [ type ] . highlight : attributes [ type ] . normal ;
}
void draw_channel_list ( const std : : vector < Video > & videos )
{
const size_t cols = termpaint_surface_width ( surface ) ;
const size_t rows = termpaint_surface_height ( surface ) ;
const size_t column_spacing = 2 ;
2020-11-26 12:28:51 +00:00
const size_t date_column = 0 ;
2020-11-22 15:17:14 +00:00
const size_t date_width = std : : string ( " xxxx-xx-xx xx:xx " ) . size ( ) ;
const size_t first_name_column = date_column + date_width + column_spacing ;
const size_t last_name_column = cols ;
const size_t name_quater = ( last_name_column - first_name_column ) / 4 ;
const size_t start_row = 2 ;
const size_t available_rows = rows - 2 ;
videos_per_page = available_rows ;
const int pages = videos . size ( ) / available_rows ;
const int cur_page = selected_video / available_rows ;
2020-11-26 12:27:11 +00:00
const std : : string channel_name = std : : string ( " Channel: " ) + channels [ selected_channel ] . name . c_str ( ) ;
2020-11-22 15:17:14 +00:00
termpaint_surface_write_with_attr ( surface , 0 , 0 , channel_name . c_str ( ) , get_attr ( ASNormal ) ) ;
if ( pages > 1 ) {
std : : string text = " (Page " + std : : to_string ( cur_page + 1 ) + " / " + std : : to_string ( pages + 1 ) + " ) " ;
const size_t w = string_width ( text ) ;
termpaint_surface_write_with_attr ( surface , cols - w , 0 , text . c_str ( ) , attributes [ ASNormal ] . normal ) ;
}
termpaint_surface_write_with_attr ( surface , date_column , 1 , " Date " , get_attr ( ASNormal ) ) ;
termpaint_surface_write_with_attr ( surface , first_name_column , 1 , " Title " , get_attr ( ASNormal ) ) ;
any_title_in_next_half = false ;
size_t cur_entry = 0 ;
for ( size_t i = cur_page * available_rows ; i < videos . size ( ) ; i + + ) {
const size_t row = start_row + cur_entry ;
const bool selected = i = = selected_video ;
const Video & video = videos . at ( i ) ;
termpaint_attr * attr = get_attr ( video . flags & kWatched ? ASWatched : ASUnwatched , selected ) ;
termpaint_surface_clear_rect_with_attr ( surface , 0 , row , cols , 1 , attr ) ;
std : : vector < char > dt ( date_width + 10 , 0 ) ;
struct tm tm ;
memset ( & tm , 0 , sizeof ( tm ) ) ;
if ( strptime ( video . published . c_str ( ) , " %FT%T%z " , & tm ) ! = nullptr ) {
strftime ( dt . data ( ) , date_width + 10 , " %F %H:%M " , & tm ) ;
}
termpaint_surface_write_with_attr ( surface , date_column , row , dt . data ( ) , attr ) ;
bool in_this_quater = title_offset * name_quater < video . tui_title_width ;
any_title_in_next_half = any_title_in_next_half | | ( ( title_offset + 2 ) * name_quater ) < video . tui_title_width ;
if ( in_this_quater )
termpaint_surface_write_with_attr_clipped ( surface , first_name_column , row , video . title . c_str ( ) + ( name_quater * title_offset ) , attr , first_name_column , last_name_column ) ;
else
termpaint_surface_write_with_attr ( surface , first_name_column , row , " ← " , attr ) ;
if ( + + cur_entry > available_rows )
break ;
}
if ( ! any_title_in_next_half & & title_offset > 0 )
title_offset - - ;
}
void draw_no_channels_msg ( )
{
const size_t cols = termpaint_surface_width ( surface ) ;
const size_t rows = termpaint_surface_height ( surface ) ;
const std : : string text ( " No configured channels. \n "
" Press the following keys to add one: \n "
2020-11-22 18:19:28 +00:00
" a Add channel by name \n "
" A Add channel by ID \n "
" Or press F1 for help or C-q to quit. " ) ;
2020-11-22 15:17:14 +00:00
{
std : : pair < size_t , size_t > size = string_size ( text ) ;
write_multiline_string ( ( cols - size . first ) / 2 , ( rows - size . second ) / 2 , text , get_attr ( ASNormal ) ) ;
}
}
2020-11-26 12:27:11 +00:00
void load_videos_for_channel ( const Channel & channel , bool force = false )
2020-11-22 15:17:14 +00:00
{
2020-11-26 12:27:11 +00:00
if ( ! force & & videos . find ( channel . id ) ! = videos . end ( ) & & ! videos [ channel . id ] . empty ( ) )
2020-11-22 15:17:14 +00:00
return ;
2020-11-26 12:27:11 +00:00
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
if ( channel . is_virtual ) {
channelVideos = Video : : get_all_with_flag_value ( channel . virtual_flag , channel . virtual_flag_value ) ;
} else {
channelVideos = Video : : get_all_for_channel ( channel . id ) ;
}
2020-11-22 20:29:45 +00:00
for ( Video & video : channelVideos ) {
2020-11-22 15:17:14 +00:00
video . tui_title_width = string_width ( video . title ) ;
}
2020-11-22 20:29:45 +00:00
2020-11-26 12:27:11 +00:00
if ( channels [ selected_channel ] . id = = channel . id )
2020-11-22 20:29:45 +00:00
selected_video = 0 ;
2020-11-22 15:17:14 +00:00
}
2020-11-26 12:27:11 +00:00
void fetch_videos_for_channel ( Channel & channel , bool name_in_title = false )
2020-11-22 15:17:14 +00:00
{
2020-11-26 12:27:11 +00:00
if ( channel . is_virtual ) {
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
channelVideos = Video : : get_all_with_flag_value ( channel . virtual_flag , channel . virtual_flag_value ) ;
for ( Video & video : channelVideos ) {
video . tui_title_width = string_width ( video . title ) ;
}
return ;
}
2020-11-22 15:17:14 +00:00
std : : string title ( " Refreshing " ) ;
if ( name_in_title )
2020-11-26 12:27:11 +00:00
title . append ( " " ) . append ( channel . name ) ;
2020-11-22 15:17:14 +00:00
title . append ( " … " ) ;
progress_info * info = begin_progress ( title , 30 ) ;
2020-11-26 12:27:11 +00:00
channel . fetch_new_videos ( db , info ) ;
2020-11-22 15:17:14 +00:00
end_progress ( info ) ;
2020-11-26 12:27:11 +00:00
load_videos_for_channel ( channels [ selected_channel ] , true ) ;
channel . load_info ( db ) ;
}
2020-11-26 18:40:39 +00:00
bool startswith ( const std : : string & str , const std : : string & with )
2020-11-26 12:27:11 +00:00
{
const size_t len = with . length ( ) ;
2020-11-26 18:40:39 +00:00
return str . substr ( 0 , len ) = = with ;
2020-11-22 15:17:14 +00:00
}
2020-11-26 18:30:26 +00:00
std : : string replace ( const std : : string & str , const std : : string & what , const std : : string & with )
{
const size_t replace_length = what . size ( ) ;
std : : string out = str ;
size_t pos = 0 ;
while ( ( pos = out . find ( what , pos ) ) ! = std : : string : : npos ) {
out . replace ( pos , replace_length , with ) ;
}
return out ;
}
2020-11-22 15:17:14 +00:00
void select_channel_by_index ( const int index ) {
2020-11-26 12:27:11 +00:00
if ( clear_channels_on_change ) {
for ( auto & [ k , v ] : videos ) {
v . clear ( ) ;
}
}
2020-11-22 15:17:14 +00:00
selected_channel = index ;
2020-11-26 12:27:11 +00:00
const Channel & channel = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
selected_video = 0 ;
2020-11-26 12:27:11 +00:00
load_videos_for_channel ( channel , clear_channels_on_change ) ;
2020-11-26 18:44:37 +00:00
current_video_count = videos [ channel . id ] . size ( ) ;
2020-11-26 12:27:11 +00:00
clear_channels_on_change = channel . is_virtual ;
2020-11-22 15:17:14 +00:00
}
void select_channel_by_name ( const std : : string & channel_name ) {
auto it = std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . name = = channel_name ; } ) ;
if ( it = = channels . cend ( ) )
return ;
select_channel_by_index ( std : : distance ( channels . cbegin ( ) , it ) ) ;
}
void select_channel_by_id ( const std : : string & channel_id ) {
auto it = std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . id = = channel_id ; } ) ;
if ( it = = channels . cend ( ) )
return ;
select_channel_by_index ( std : : distance ( channels . cbegin ( ) , it ) ) ;
}
void add_channel_to_list ( Channel & channel )
{
channel . load_info ( db ) ;
std : : string selected_channel_id ;
2020-11-26 12:27:11 +00:00
if ( selected_channel < channels . size ( ) )
selected_channel_id = channels [ selected_channel ] . id ;
2020-11-22 15:17:14 +00:00
channels . push_back ( channel ) ;
2020-11-26 12:27:11 +00:00
std : : sort ( channels . begin ( ) , channels . end ( ) , [ ] ( const Channel & a , const Channel & b ) { if ( a . is_virtual ! = b . is_virtual ) { return a . is_virtual > b . is_virtual ; } return a . name < b . name ; } ) ;
if ( ! selected_channel_id . empty ( ) ) {
2020-11-22 15:17:14 +00:00
const size_t new_index = std : : distance ( channels . cbegin ( ) , std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & ch ) { return ch . id = = selected_channel_id ; } ) ) ;
selected_channel = new_index ;
}
}
2020-11-26 12:27:11 +00:00
void make_virtual_unwatched_channel ( )
{
Channel channel = Channel : : add_virtual ( " All Unwatched " , kWatched , false ) ;
std : : vector < Video > & channelVideos = videos [ channel . id ] ;
channelVideos = Video : : get_all_with_flag_value ( channel . virtual_flag , channel . virtual_flag_value ) ;
for ( Video & video : channelVideos ) {
video . tui_title_width = string_width ( video . title ) ;
}
add_channel_to_list ( channel ) ;
}
2020-11-22 20:43:30 +00:00
void action_add_channel_by_name ( )
2020-11-22 15:17:14 +00:00
{
std : : string channel_name = get_string ( " Add new Channel " , " Enter channel name " ) ;
if ( channel_name . empty ( ) ) {
return ;
} else if ( std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . name = = channel_name ; } ) ! = channels . cend ( ) ) {
message_box ( " Can't add channel " , " A channel with this name is already in the list! " ) ;
return ;
} else {
Channel ch = Channel : : add ( db , " forUsername " , channel_name ) ;
if ( ch . is_valid ( ) ) {
add_channel_to_list ( ch ) ;
select_channel_by_id ( ch . id ) ;
tp_flush ( ) ;
if ( message_box ( " Update now? " , " Fetch videos for this channel now? " , Button : : Yes | Button : : No , Button : : Yes ) = = Button : : Yes ) {
fetch_videos_for_channel ( ch ) ;
}
} else {
message_box ( " Can't add channel " , " There is no channel with this name! " ) ;
}
}
}
2020-11-22 20:43:30 +00:00
void action_add_channel_by_id ( )
2020-11-22 15:17:14 +00:00
{
std : : string channel_id = get_string ( " Add new Channel " , " Enter channel ID " ) ;
if ( channel_id . empty ( ) ) {
return ;
} else if ( std : : find_if ( channels . cbegin ( ) , channels . cend ( ) , [ & ] ( const Channel & channel ) { return channel . id = = channel_id ; } ) ! = channels . cend ( ) ) {
message_box ( " Can't add channel " , " A channel with this ID is already in the list! " ) ;
return ;
} else {
Channel ch = Channel : : add ( db , " id " , channel_id ) ;
if ( ch . is_valid ( ) ) {
add_channel_to_list ( ch ) ;
select_channel_by_id ( ch . id ) ;
tp_flush ( ) ;
if ( message_box ( " Update now? " , " Fetch videos for this channel now? " , Button : : Yes | Button : : No , Button : : Yes ) = = Button : : Yes ) {
fetch_videos_for_channel ( ch ) ;
}
} else {
message_box ( " Can't add channel " , " There is no channel with this ID! " ) ;
}
}
}
void action_select_channel ( ) {
if ( channels . size ( ) ) {
std : : vector < std : : string > names ;
for ( const Channel & c : channels )
names . push_back ( c . name + " ( " + std : : to_string ( c . unwatched ) + " ) " ) ;
2020-11-26 12:27:11 +00:00
const int channel = get_selection ( " Switch Channel " , names , selected_channel , Align : : VCenter | Align : : Left ) ;
2020-11-22 15:17:14 +00:00
if ( channel ! = - 1 )
select_channel_by_index ( channel ) ;
} else {
message_box ( " Can't select channel " , " No channels configured. \n Please configure one. " ) ;
}
}
void action_refresh_channel ( ) {
2020-11-26 12:27:11 +00:00
fetch_videos_for_channel ( channels . at ( selected_channel ) ) ;
2020-11-22 15:17:14 +00:00
}
void action_refresh_all_channels ( ) {
if ( message_box ( " Refresh all channels? " , ( " Do you want to refresh all " + std : : to_string ( channels . size ( ) ) + " channels? " ) . c_str ( ) , Button : : Yes | Button : : No , Button : : No ) ! = Button : : Yes )
return ;
for ( Channel & channel : channels ) {
fetch_videos_for_channel ( channel , true ) ;
}
}
void action_mark_video_watched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & video = videos [ ch . id ] [ selected_video ] ;
video . set_flag ( db , kWatched ) ;
ch . load_info ( db ) ;
}
2020-11-26 18:30:26 +00:00
std : : vector < std : : string > watch_command = { " xdg-open " , " https://youtube.com/watch?v={{vid}} " } ;
2020-11-22 15:17:14 +00:00
void action_watch_video ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & video = videos [ ch . id ] [ selected_video ] ;
2020-11-26 18:30:26 +00:00
const char * cmdline [ watch_command . size ( ) + 1 ] ;
for ( size_t i = 0 ; i < watch_command . size ( ) ; i + + ) {
const std : : string arg = replace ( watch_command [ i ] , " {{vid}} " , video . id ) ;
cmdline [ i ] = strdup ( arg . c_str ( ) ) ;
}
cmdline [ watch_command . size ( ) ] = nullptr ;
subprocess_s proc ;
if ( int rc = subprocess_create ( cmdline , subprocess_option_inherit_environment , & proc ) ; rc ! = 0 ) {
const std : : string message = watch_command . at ( 0 ) + " failed with error " + std : : to_string ( rc ) ;
message_box ( " Failed to run watch command " , message ) ;
2020-11-22 15:17:14 +00:00
} else {
video . set_flag ( db , kWatched ) ;
ch . load_info ( db ) ;
2020-11-26 18:30:26 +00:00
subprocess_join ( & proc , nullptr ) ;
for ( size_t i = 0 ; i < watch_command . size ( ) ; i + + ) {
free ( ( void * ) cmdline [ i ] ) ;
}
2020-11-22 15:17:14 +00:00
}
}
void action_mark_video_unwatched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
Video & selected = videos [ ch . id ] [ selected_video ] ;
selected . set_flag ( db , kWatched , false ) ;
ch . load_info ( db ) ;
}
void action_mark_all_videos_watched ( ) {
2020-11-26 12:27:11 +00:00
Channel & ch = channels . at ( selected_channel ) ;
2020-11-22 15:17:14 +00:00
if ( message_box ( " Mark all as watched " , ( " Do you want to mark all videos of " + ch . name + " as watched? " ) . c_str ( ) , Button : : Yes | Button : : No , Button : : No ) ! = Button : : Yes )
return ;
2020-11-22 20:43:30 +00:00
{
db_transaction transaction ;
for ( Video & video : videos [ ch . id ] ) {
video . set_flag ( db , kWatched ) ;
}
}
ch . load_info ( db ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_prev_channel ( ) {
2020-11-26 12:27:11 +00:00
if ( selected_channel > 0 )
select_channel_by_index ( selected_channel - 1 ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_next_channel ( ) {
2020-11-26 12:27:11 +00:00
if ( selected_channel < channels . size ( ) - 1 )
select_channel_by_index ( selected_channel + 1 ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_prev_video ( ) {
if ( selected_video > 0 )
selected_video - - ;
}
void action_select_next_video ( ) {
if ( selected_video < current_video_count - 1 )
selected_video + + ;
}
void action_select_prev_video_page ( ) {
if ( selected_video > 0 )
selected_video - = std : : min ( selected_video , videos_per_page ) ;
}
void action_select_next_video_page ( ) {
if ( selected_video < current_video_count - 1 )
2020-11-22 23:02:51 +00:00
selected_video + = std : : min ( current_video_count - 1 - selected_video , videos_per_page ) ;
2020-11-22 15:17:14 +00:00
}
void action_select_first_video ( ) {
selected_video = 0 ;
}
void action_select_last_video ( ) {
selected_video = current_video_count - 1 ;
}
void action_scroll_title_left ( ) {
if ( title_offset > 0 )
title_offset - - ;
}
void action_scroll_title_right ( ) {
if ( any_title_in_next_half )
title_offset + + ;
}
void action_show_video_detail ( ) {
message_box ( " Details " , " Video details go here... " ) ;
}
using json = nlohmann : : json ;
std : : optional < json > load_json ( const std : : string & filename ) {
std : : ifstream ifs ( filename ) ;
if ( ! ifs . is_open ( ) )
return { } ;
try {
json data ;
ifs > > data ;
return data ;
} catch ( json : : parse_error & err ) {
fprintf ( stderr , " Failed to parse JSON file %s: %s \n " , filename . c_str ( ) , err . what ( ) ) ;
return { } ;
}
}
bool save_json ( const std : : string & filename , const json & data ) {
std : : ofstream ofs ( filename ) ;
if ( ! ofs . is_open ( ) )
return false ;
try {
ofs < < data ;
return true ;
} catch ( json : : exception & err ) {
return false ;
}
}
2020-11-26 18:41:48 +00:00
static std : : string get_module_path ( ) {
std : : string str ;
char * exe = realpath ( " /proc/self/exe " , nullptr ) ;
if ( exe ) {
str = std : : string ( exe ) ;
free ( exe ) ;
exe = nullptr ;
} else {
perror ( " realpath " ) ;
}
exe = dirname ( ( char * ) str . c_str ( ) ) ;
return std : : string ( exe ) ;
}
2020-11-22 18:18:45 +00:00
2020-11-22 15:17:14 +00:00
int main ( )
{
2020-11-26 18:41:48 +00:00
const std : : string module_path = get_module_path ( ) ;
2020-11-22 15:17:14 +00:00
user_home = std : : string ( std : : getenv ( " HOME " ) ) ;
2020-11-26 18:38:00 +00:00
std : : string database_filename = user_home + " /.local/share/yttui.db " ;
2020-11-22 15:17:14 +00:00
curl_global_init ( CURL_GLOBAL_ALL ) ;
tp_init ( ) ;
2020-11-26 18:41:48 +00:00
const std : : vector < std : : string > config_locations { user_home + " /.config " , module_path } ;
2020-11-26 18:40:11 +00:00
std : : string config_file ;
2020-11-22 15:17:14 +00:00
nlohmann : : json config ;
for ( const std : : string & location : config_locations ) {
2020-11-26 18:40:11 +00:00
config_file = location + " /yttui.conf " ;
2020-11-22 15:17:14 +00:00
auto config_data = load_json ( config_file ) ;
if ( config_data ) {
config = * config_data ;
break ;
}
}
2020-11-22 22:44:33 +00:00
if ( config . count ( " apiKey " ) & & config [ " apiKey " ] . is_string ( ) ) {
2020-11-22 18:28:51 +00:00
yt_config . api_key = config [ " apiKey " ] ;
} else {
2020-11-26 18:40:11 +00:00
tui_abort ( " A YouTube API key is required for this application to function. \n Please provide one in the config file. \n \n Current config file: \n " + config_file ) ;
2020-11-22 18:28:51 +00:00
}
2020-11-22 22:44:33 +00:00
if ( config . count ( " extraHeaders " ) & & config [ " extraHeaders " ] . is_array ( ) ) {
2020-11-22 18:28:51 +00:00
for ( const json & elem : config [ " extraHeaders " ] ) {
2020-11-22 22:44:33 +00:00
if ( elem . count ( " key " ) & & elem [ " key " ] . is_string ( ) & & elem . count ( " value " ) & & elem [ " value " ] . is_string ( ) ) {
2020-11-22 18:28:51 +00:00
yt_config . extra_headers . emplace ( elem [ " key " ] , elem [ " value " ] ) ;
2020-11-22 15:17:14 +00:00
}
}
2020-11-22 18:28:51 +00:00
}
2020-11-22 22:44:33 +00:00
if ( config . count ( " database " ) & & config [ " database " ] . is_string ( ) ) {
2020-11-26 18:38:00 +00:00
database_filename = replace ( config [ " database " ] , " $HOME " , user_home ) ;
2020-11-22 15:17:14 +00:00
}
2020-11-26 18:30:26 +00:00
if ( config . count ( " watchCommand " ) & & config [ " watchCommand " ] . is_array ( ) ) {
watch_command . clear ( ) ;
for ( const json & elem : config [ " watchCommand " ] ) {
if ( ! elem . is_string ( ) ) {
tui_abort ( " Configuration error: watchCommand element " + elem . dump ( ) + " is not a string but " + elem . type_name ( ) + " . " ) ;
}
watch_command . push_back ( elem ) ;
}
}
2020-11-22 15:17:14 +00:00
2020-11-22 20:29:45 +00:00
db_init ( database_filename ) ;
2020-11-26 12:27:11 +00:00
make_virtual_unwatched_channel ( ) ;
2020-11-22 20:29:45 +00:00
for ( Channel & channel : Channel : : get_all ( db ) ) {
2020-11-22 15:17:14 +00:00
add_channel_to_list ( channel ) ;
}
if ( channels . size ( ) )
select_channel_by_index ( 0 ) ;
bool exit = false ;
2020-11-26 12:25:25 +00:00
bool force_repaint = false ;
2020-11-22 15:17:14 +00:00
std : : vector < action > actions = {
2020-11-22 20:43:30 +00:00
{ TERMPAINT_EV_CHAR , " a " , 0 , action_add_channel_by_name , " Add channel by name " } ,
{ TERMPAINT_EV_CHAR , " A " , 0 , action_add_channel_by_id , " Add channel by Id " } ,
2020-11-22 15:17:14 +00:00
{ TERMPAINT_EV_CHAR , " c " , 0 , action_select_channel , " Select channel " } ,
{ TERMPAINT_EV_CHAR , " j " , 0 , action_select_prev_channel , " Select previous channel " } ,
{ TERMPAINT_EV_CHAR , " k " , 0 , action_select_next_channel , " Select next channel " } ,
{ TERMPAINT_EV_CHAR , " r " , 0 , action_refresh_channel , " Refresh selected channel " } ,
{ TERMPAINT_EV_CHAR , " R " , 0 , action_refresh_all_channels , " Refresh all channels " } ,
{ TERMPAINT_EV_CHAR , " w " , 0 , action_watch_video , " Watch video " } ,
{ TERMPAINT_EV_CHAR , " w " , TERMPAINT_MOD_ALT , action_mark_video_watched , " Mark video as watched " } ,
{ TERMPAINT_EV_CHAR , " u " , 0 , action_mark_video_unwatched , " Mark video as unwatched " } ,
{ TERMPAINT_EV_CHAR , " W " , 0 , action_mark_all_videos_watched , " Mark channel as watched " } ,
2020-11-26 18:40:39 +00:00
{ TERMPAINT_EV_CHAR , " q " , TERMPAINT_MOD_CTRL , [ & ] ( ) { exit = true ; } , " Quit " } ,
2020-11-22 15:17:14 +00:00
{ TERMPAINT_EV_KEY , " Space " , 0 , action_show_video_detail , " Show video details " } ,
{ TERMPAINT_EV_KEY , " ArrowUp " , 0 , action_select_prev_video , " Previous video " } ,
{ TERMPAINT_EV_KEY , " ArrowDown " , 0 , action_select_next_video , " Next video " } ,
{ TERMPAINT_EV_KEY , " PageUp " , 0 , action_select_prev_video_page , " Previous video page " } ,
{ TERMPAINT_EV_KEY , " PageDown " , 0 , action_select_next_video_page , " Next video page " } ,
{ TERMPAINT_EV_KEY , " Home " , 0 , action_select_first_video , " First video " } ,
{ TERMPAINT_EV_KEY , " End " , 0 , action_select_last_video , " Last video " } ,
{ TERMPAINT_EV_KEY , " ArrowLeft " , 0 , action_scroll_title_left , " Scroll title left " } ,
{ TERMPAINT_EV_KEY , " ArrowRight " , 0 , action_scroll_title_right , " Scroll title right " } ,
2020-11-26 12:25:25 +00:00
{ TERMPAINT_EV_CHAR , " l " , TERMPAINT_MOD_CTRL , [ & ] ( ) { force_repaint = true ; } , " Force redraw " } ,
2020-11-22 15:17:14 +00:00
} ;
do {
termpaint_surface_clear ( surface , TERMPAINT_DEFAULT_COLOR , TERMPAINT_DEFAULT_COLOR ) ;
2020-11-26 12:27:11 +00:00
draw_channel_list ( videos [ channels . at ( selected_channel ) . id ] ) ;
2020-11-26 12:25:25 +00:00
tp_flush ( force_repaint ) ;
force_repaint = false ;
2020-11-22 15:17:14 +00:00
auto event = tp_wait_for_event ( ) ;
if ( ! event )
abort ( ) ;
2020-11-22 17:09:17 +00:00
tui_handle_action ( * event , actions ) ;
2020-11-22 15:17:14 +00:00
} while ( ! exit ) ;
tp_shutdown ( ) ;
2020-11-22 20:29:45 +00:00
db_shutdown ( ) ;
2020-11-22 15:17:14 +00:00
curl_global_cleanup ( ) ;
return 0 ;
}