summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--flake.lock12
-rw-r--r--flake.nix6
-rw-r--r--meson.build17
-rw-r--r--src/fruits/cmc/cmc.c129
-rw-r--r--src/fruits/cmc/cmc.h16
-rw-r--r--src/fruits/cmc/meson.build3
-rw-r--r--src/fruits/cmc/ui/pane_list.c157
-rw-r--r--src/fruits/cmc/ui/pane_search.c177
-rw-r--r--src/fruits/cmc/ui/ui.c204
-rw-r--r--src/fruits/cmc/ui/ui.h52
-rw-r--r--src/fruits/cmsrv/cmsrv.c12
-rw-r--r--src/fruits/cmsrv/meson.build2
-rw-r--r--src/fruits/cmv/cmv.c15
-rw-r--r--src/fruits/meson.build35
-rw-r--r--src/liana/list.c18
-rw-r--r--src/liana/list.h22
-rw-r--r--src/libclient/client.c40
-rw-r--r--src/libclient/client.h9
-rw-r--r--src/libclient/common.h3
-rw-r--r--src/server/server.c82
-rw-r--r--src/server/server.h2
-rw-r--r--subprojects/notcurses.wrap12
-rw-r--r--subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff166
24 files changed, 1039 insertions, 153 deletions
diff --git a/.gitignore b/.gitignore
index 56b6a8c..aed61f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,7 @@ subprojects/libxml2-*/
subprojects/libxml2.wrap
subprojects/linmath.h/
subprojects/miniaudio/
+subprojects/notcurses/
subprojects/packagecache/
subprojects/shaderc/
subprojects/stb/
diff --git a/flake.lock b/flake.lock
index 3a2e0ce..e36c9a4 100644
--- a/flake.lock
+++ b/flake.lock
@@ -23,11 +23,11 @@
]
},
"locked": {
- "lastModified": 1737221749,
- "narHash": "sha256-igllW0yG+UbetvhT11jnt9RppSHXYgMykYhZJeqfHs0=",
+ "lastModified": 1737480538,
+ "narHash": "sha256-rk/cmrvq3In0TegW9qaAxw+5YpJhRWt2p74/6JStrw0=",
"owner": "nix-community",
"repo": "home-manager",
- "rev": "97d7946b5e107dd03cc82f21165251d4e0159655",
+ "rev": "4481a16d1ac5bff4a77c608cefe08c9b9efe840d",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
},
"nixos-hardware": {
"locked": {
- "lastModified": 1736978406,
- "narHash": "sha256-oMr3PVIQ8XPDI8/x6BHxsWEPBRU98Pam6KGVwUh8MPk=",
+ "lastModified": 1737359802,
+ "narHash": "sha256-utplyRM6pqnN940gfaLFBb9oUCSzkan86IvmkhsVlN8=",
"owner": "NixOS",
"repo": "nixos-hardware",
- "rev": "b678606690027913f3434dea3864e712b862dde5",
+ "rev": "61c79181e77ef774ab0468b28a24bc2647d498d6",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 10738f4..7d31b5e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -130,7 +130,11 @@
wayland-protocols
wlr-protocols
egl-wayland
- (notcurses.override { multimediaSupport = false; qrcodegenSupport = false; })
+ # notcurses build
+ libdeflate
+ libunistring
+ ncurses
+ # discord
libsodium
];
meta = with lib; {
diff --git a/meson.build b/meson.build
index 235b16a..c04e6fa 100644
--- a/meson.build
+++ b/meson.build
@@ -50,16 +50,10 @@ endif
if get_option('server').enabled()
subdir('src/server')
- if not meson.is_subproject() and not is_windows
- subdir('src/fruits/cmsrv')
- endif
endif
if get_option('client').enabled()
subdir('src/libclient')
- if not meson.is_subproject() and not is_windows
- subdir('src/fruits/cmc')
- endif
endif
if get_option('sink').enabled()
@@ -71,13 +65,10 @@ if get_option('sink').enabled()
endif
subdir('src/libsink')
subdir('src/sink')
- if not meson.is_subproject()
- if is_android
- subdir('src/fruits/ctv')
- else
- subdir('src/fruits/cmv')
- endif
- endif
+endif
+
+if not meson.is_subproject()
+ subdir('src/fruits')
endif
if get_option('tests')
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
index 7691f99..58bb3b1 100644
--- a/src/fruits/cmc/cmc.c
+++ b/src/fruits/cmc/cmc.c
@@ -4,6 +4,7 @@
#include "../../portal/src/packet_ext.h"
#include "../../server/common.h"
+#include "../../liana/list.h"
#include "../common.h"
@@ -26,10 +27,22 @@ static struct cmc_search *get_search_by_id(struct cmc *c, s32 id)
return NULL;
}
-static void parse_user_state(struct cmc *c, struct nn_packet *packet)
+static void parse_list_entry(struct nn_packet *packet, struct cmc_list_entry *entry)
{
- u32 size = nn_packet_read_u32(packet);
- for (u32 i = 0; i < size; i++) {
+ entry->id = nn_packet_read_u32(packet);
+ entry->duration = nn_packet_read_u64(packet);
+ entry->start = nn_packet_read_u64(packet);
+ entry->paused_at = nn_packet_read_u64(packet);
+ entry->offset = nn_packet_read_u64(packet);
+ wstr w;
+ nn_packet_read_wstr(packet, &w);
+ al_wstr_clone(&entry->name, &w);
+}
+
+static void parse_initial_user_state(struct cmc *c, struct nn_packet *packet)
+{
+ u32 count = nn_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
struct cmc_search *search = al_alloc_object(struct cmc_search);
search->id = nn_packet_read_s32(packet);
str s;
@@ -40,28 +53,46 @@ static void parse_user_state(struct cmc *c, struct nn_packet *packet)
al_array_init(search->pages);
al_array_push(c->searches, search);
}
+ count = nn_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
+ struct cmc_list *list = al_alloc_object(struct cmc_list);
+ str s;
+ nn_packet_read_str(packet, &s);
+ al_str_clone(&list->name, &s);
+ list->current = nn_packet_read_s32(packet);
+ al_array_init(list->entries);
+ u32 inner_count = nn_packet_read_s32(packet);
+ for (u32 j = 0; j < inner_count; j++) {
+ struct cmc_list_entry entry;
+ parse_list_entry(packet, &entry);
+ al_array_push(list->entries, entry);
+ }
+ al_array_push(c->lists, list);
+ }
}
static void client_callback(void *userdata, u8 op, void *opaque)
{
struct cmc *c = (struct cmc *)userdata;
switch (op) {
- case CAMU_CLIENT_LOGIN: {
+ case CAMU_CLIENT_LOGGED_IN: {
struct nn_packet *packet = (struct nn_packet *)opaque;
switch (c->command) {
case CLI_OPEN_UI:
- parse_user_state(c, packet);
+ parse_initial_user_state(c, packet);
if (!cmc_ui_init(&c->ui, &c->loop, c)) {
nn_event_loop_break_one(&c->loop);
}
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ cmc_lp_add_list(&c->ui, list);
+ }
+ struct cmc_search *search;
+ al_array_foreach(c->searches, i, search) {
+ cmc_sp_add_search(&c->ui, search);
+ }
break;
case CLI_ADD: {
- str *arg;
- al_array_foreach_ptr(c->args, i, arg) {
- camu_client_add_from_path(&c->client, &al_str_c("default"), arg);
- al_str_free(arg);
- }
- camu_client_disconnect(&c->client);
break;
}
case CLI_SEARCH: {
@@ -81,32 +112,65 @@ static void client_callback(void *userdata, u8 op, void *opaque)
search->id = *(s32 *)opaque;
al_array_init(search->pages);
al_array_push(c->searches, search);
- camu_client_get_page(&c->client, search->id, 0);
+ cmc_sp_add_search(&c->ui, search);
+ break;
+ }
+ case CAMU_CLIENT_GOT_RESULTS: {
break;
}
- case CAMU_CLIENT_PAGE_RESULTS: {
+ case CAMU_CLIENT_GOT_META: {
struct nn_packet *packet = (struct nn_packet *)opaque;
- s32 id = nn_packet_read_s32(packet);
- struct cmc_search *search = get_search_by_id(c, id);
- if (!search) return;
- struct cmc_search_page *page = al_alloc_object(struct cmc_search_page);
- page->num = nn_packet_read_u32(packet);
- u32 posts = nn_packet_read_u32(packet);
- for (u32 i = 0; i < posts; i++) {
- struct camu_post post;
- nn_packet_read_post(packet, &post);
- camu_post_cache_push(&c->cache, &post);
+ u8 op = nn_packet_read_u8(packet);
+ switch (op) {
+ case LIANA_META_CURRENT_CHANGED: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ list->current = nn_packet_read_s32(packet);
+ struct cmc_list_entry *entry = &al_array_at(list->entries, list->current);
+ parse_list_entry(packet, entry);
+ break;
+ }
+ }
+ cmc_ui_queue_render(&c->ui);
+ break;
+ }
+ case LIANA_META_ADDED_ENTRY: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list_entry entry;
+ parse_list_entry(packet, &entry);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ al_array_push(list->entries, entry);
+ break;
+ }
+ }
+ cmc_ui_queue_render(&c->ui);
+ break;
+ }
+ case LIANA_META_ENTRY_SEEKED: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ u32 id = nn_packet_peek_u32(packet);
+ struct cmc_list_entry *entry;
+ al_array_foreach_ptr(list->entries, j, entry) {
+ if (entry->id == id) {
+ parse_list_entry(packet, entry);
+ break;
+ }
+ }
+ }
+ }
+ break;
}
- u32 ids = nn_packet_read_u32(packet);
- for (u32 i = 0; i < ids; i++) {
- str unique_id;
- nn_packet_read_str(packet, &unique_id);
- str s;
- al_str_clone(&s, &unique_id);
- al_array_push(page->list, s);
}
- al_array_push(search->pages, page);
- camu_client_add_from_post(&c->client, &al_str_c("default"), &al_array_at(page->list, 0), 0);
break;
}
}
@@ -166,6 +230,7 @@ s32 main(s32 argc, char *argv[])
al_array_init(c.args);
if (!parse_cmd(argc, argv)) return EXIT_FAILURE;
+ al_array_init(c.lists);
camu_post_cache_init(&c.cache);
al_array_init(c.searches);
diff --git a/src/fruits/cmc/cmc.h b/src/fruits/cmc/cmc.h
index 96de27b..0a3cbca 100644
--- a/src/fruits/cmc/cmc.h
+++ b/src/fruits/cmc/cmc.h
@@ -21,11 +21,27 @@ struct cmc_search {
array(struct cmc_search_page *) pages;
};
+struct cmc_list_entry {
+ u32 id;
+ u64 duration;
+ u64 start;
+ u64 paused_at;
+ u64 offset;
+ wstr name;
+};
+
+struct cmc_list {
+ str name;
+ s32 current;
+ array(struct cmc_list_entry) entries;
+};
+
struct cmc {
struct nn_event_loop loop;
u8 command;
array(str) args;
struct camu_client client;
+ array(struct cmc_list *) lists;
struct camu_post_cache cache;
array(struct cmc_search *) searches;
struct cmc_ui ui;
diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build
index 76efff5..6b7060d 100644
--- a/src/fruits/cmc/meson.build
+++ b/src/fruits/cmc/meson.build
@@ -10,9 +10,10 @@ use_tui = true
if use_tui
cmc_src += [
'ui/ui.c',
+ 'ui/pane_list.c',
'ui/pane_search.c',
]
- cmc_deps += [dependency('notcurses')]
+ cmc_deps += [notcurses, notcurses_core]
endif
if is_windows and meson.is_cross_build()
diff --git a/src/fruits/cmc/ui/pane_list.c b/src/fruits/cmc/ui/pane_list.c
new file mode 100644
index 0000000..9a04211
--- /dev/null
+++ b/src/fruits/cmc/ui/pane_list.c
@@ -0,0 +1,157 @@
+#include <nnwt/thread.h>
+
+#include "../../liana/list.h"
+
+#include "../cmc.h"
+
+#include "ui.h"
+
+void cmc_lp_init(struct cmc_ui *ui)
+{
+ al_array_init(ui->lp.tabs);
+ ui->lp.current = 0;
+}
+
+void cmc_lp_layout(struct cmc_ui *ui, struct ncplane *parent)
+{
+ if (ui->lp.n) ncplane_destroy(ui->lp.n);
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = ncplane_dim_y(parent);
+ nopts.cols = ncplane_dim_x(parent);
+ ui->lp.n = ncplane_create(parent, &nopts);
+}
+
+void cmc_lp_add_list(struct cmc_ui *ui, struct cmc_list *list)
+{
+ struct cmc_list_tab tab = {
+ .list = list,
+ .selected = 0
+ };
+ al_array_push(ui->lp.tabs, tab);
+}
+
+bool cmc_lp_handle_input(struct cmc_ui *ui, struct ncinput *input)
+{
+ if (!ui->lp.tabs.count) {
+ return false;
+ }
+
+ struct cmc_list_tab *tab = &al_array_at(ui->lp.tabs, ui->lp.current);
+ u32 count = tab->list->entries.count;
+ switch (input->id) {
+ case 'j':
+ if (!cmc_ui_consider_input(input, true) || tab->selected >= count - 1) {
+ return false;
+ }
+ tab->selected++;
+ break;
+ case 'k':
+ if (!cmc_ui_consider_input(input, true) || tab->selected == 0) {
+ return false;
+ }
+ tab->selected--;
+ break;
+ case NCKEY_RETURN:
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ camu_client_skipto(&ui->c->client, &tab->list->name, tab->selected);
+ break;
+ case NCKEY_BUTTON1: {
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ u32 height = ncplane_dim_y(ui->lp.n);
+ u32 width = ncplane_dim_x(ui->lp.n);
+ if (input->x > 0 && input->y > 0) {
+ u32 x = (u32)input->x;
+ u32 y = (u32)input->y;
+ if (y > height - 3 && x >= 2 && x <= width) {
+ struct cmc_list_entry *entry = &al_array_at(tab->list->entries, tab->list->current);
+ f64 percent = (x - 2) / (f64)(width - 2);
+ camu_client_seek(&ui->c->client, &tab->list->name, entry->id, percent);
+ }
+ ui->last_mouse_x = x;
+ ui->last_mouse_y = y;
+ }
+ break;
+ }
+ }
+
+ return true;
+}
+
+void cmc_lp_erase(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->lp.n);
+}
+
+static void render_list_entries(struct cmc_ui *ui, struct cmc_list_tab *tab, u32 max_height)
+{
+ struct cmc_list *list = tab->list;
+ u32 count = list->entries.count;
+ u32 index = tab->selected - (tab->selected % max_height);
+ u32 end = MIN(index + max_height, count);
+ struct cmc_list_entry *entry;
+ for (u32 i = index; i < end; i++) {
+ entry = &al_array_at(list->entries, i);
+ if (i == tab->selected) {
+ ncplane_set_styles(ui->lp.n, NCSTYLE_BOLD);
+ ncplane_set_fg_palindex(ui->lp.n, 6);
+ } else {
+ ncplane_set_styles(ui->lp.n, NCSTYLE_NONE);
+ ncplane_set_fg_default(ui->lp.n);
+ }
+ if (list->current >= 0 && i == (u32)list->current) {
+ ncplane_putchar_yx(ui->lp.n, i - index, 1, '>');
+ cmc_ui_putnwstr_yx(ui->lp.n, i - index, 3, entry->name.length, &entry->name);
+ } else {
+ cmc_ui_putnwstr_yx(ui->lp.n, i - index, 0, entry->name.length, &entry->name);
+ }
+ }
+}
+
+static void render_now_playing(struct cmc_ui *ui, struct cmc_list_tab *tab, u32 y, u32 height)
+{
+ u32 width = ncplane_dim_x(ui->lp.n);
+ if (tab && tab->list->entries.count > 0) {
+ struct cmc_list *list = tab->list;
+ struct cmc_list_entry *entry = &al_array_at(list->entries, list->current);
+ f32 percent = 1.0;
+ if (entry->duration > 0) {
+ u64 pos = entry->offset;
+ u64 now = nn_get_timestamp();
+ if (entry->start != LIANA_TIMESTAMP_INVALID && now > entry->start) {
+ pos += now - entry->start;
+ }
+ percent = MIN(pos / (f32)entry->duration, 1.f);
+ }
+ for (u32 i = 0; i < (width - 2) * percent; i++) {
+ ncplane_putchar_yx(ui->lp.n, height - 2, i + 1, '-');
+ }
+ cmc_ui_putnwstr_yx(ui->lp.n, height - 4, 1, entry->name.length, &entry->name);
+ }
+ char buf[24];
+ al_snprintf(buf, sizeof(buf), "%u, %u | %u, %u", ui->last_mouse_x, ui->last_mouse_y, width, height);
+ ncplane_putstr_yx(ui->lp.n, height - 5, 1, buf);
+ u64 c = 0;
+ ncchannels_set_fg_default(&c);
+ ncplane_cursor_move_yx(ui->lp.n, y, 0);
+ ncplane_rounded_box(ui->lp.n, NCSTYLE_NONE, c, height - 1, width - 1, 0);
+}
+
+void cmc_lp_render(struct cmc_ui *ui)
+{
+ u32 height = ncplane_dim_y(ui->lp.n);
+
+ struct cmc_list_tab *tab = NULL;
+ if (ui->lp.tabs.count > 0) {
+ tab = &al_array_at(ui->lp.tabs, ui->lp.current);
+ }
+
+ if (tab) {
+ render_list_entries(ui, tab, height - 8);
+ }
+
+ render_now_playing(ui, tab, height - 8, height);
+}
diff --git a/src/fruits/cmc/ui/pane_search.c b/src/fruits/cmc/ui/pane_search.c
index c3d1165..021c42f 100644
--- a/src/fruits/cmc/ui/pane_search.c
+++ b/src/fruits/cmc/ui/pane_search.c
@@ -4,36 +4,181 @@
void cmc_sp_init(struct cmc_ui *ui)
{
- if (ui->c->searches.count > 0) {
- ui->sp.search = al_array_at(ui->c->searches, 0);
- }
- cmc_sp_layout(ui, notcurses_stdplane(ui->nc));
+ al_array_init(ui->sp.tabs);
+ ui->sp.current = 0;
}
void cmc_sp_layout(struct cmc_ui *ui, struct ncplane *parent)
{
if (ui->sp.n) ncplane_destroy(ui->sp.n);
- struct ncplane_options nopts = {
- .rows = ui->term_rows,
- .cols = ui->term_cols,
- .x = 0,
- .y = 0
- };
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = ncplane_dim_y(parent);
+ nopts.cols = ncplane_dim_x(parent);
ui->sp.n = ncplane_create(parent, &nopts);
}
+void cmc_sp_add_search(struct cmc_ui *ui, struct cmc_search *search)
+{
+ struct cmc_search_tab tab = {
+ .search = search,
+ };
+ struct ncplane_options nopts = { 0 };
+ nopts.x = 3;
+ nopts.y = 2;
+ nopts.rows = 10;
+ nopts.cols = ncplane_dim_x(ui->sp.n) - 3;
+ tab.input = ncplane_create(ui->sp.n, &nopts);
+ tab.cursor = 0;
+ tab.input_active = false;
+ tab.input_text = al_wstr_null();
+ tab.input_changed = false;
+ al_array_push(ui->sp.tabs, tab);
+}
+
+static bool handle_text_input(struct cmc_search_tab *tab, struct ncinput *input)
+{
+ switch (input->id) {
+ case NCKEY_BACKSPACE:
+ if (tab->cursor > 0) {
+ al_wstr_remove_at(&tab->input_text, tab->cursor);
+ tab->cursor--;
+ tab->input_changed = true;
+ }
+ break;
+ case NCKEY_LEFT:
+ if (tab->cursor > 0) {
+ tab->cursor--;
+ }
+ break;
+ case NCKEY_RIGHT:
+ if (tab->cursor < tab->input_text.length) {
+ tab->cursor++;
+ }
+ break;
+ case NCKEY_RETURN:
+ tab->input_active = false;
+ // Erase text on next render.
+ tab->input_changed = true;
+ return true;
+ default:
+ if (ncinput_ctrl_p(input)) {
+ switch (input->id) {
+ case 'A':
+ tab->cursor = 0;
+ break;
+ case 'E':
+ tab->cursor = tab->input_text.length;
+ break;
+ case 'B':
+ if (tab->cursor > 0) {
+ tab->cursor--;
+ }
+ break;
+ case 'F':
+ if (tab->cursor < tab->input_text.length) {
+ tab->cursor++;
+ }
+ break;
+ }
+ break;
+ }
+ // https://github.com/dankamongmen/notcurses/blob/c11efe877f2245901f5c9ce110de7a2c83cfa2ed/src/lib/reader.c#L404
+ for (s32 c = 0; input->eff_text[c] != 0; c++){
+ uchar egc[5] = { 0 };
+ if (notcurses_ucs32_to_utf8(&input->eff_text[c], 1, egc, 4) >= 0) {
+ // This probably doesn't work on Windows.
+ al_wstr_insert(&tab->input_text, tab->cursor, &al_wstr_w((wchar_t *)egc, 0, 1));
+ tab->cursor++;
+ tab->input_changed = true;
+ }
+ }
+ break;
+ }
+ return false;
+}
+
bool cmc_sp_handle_input(struct cmc_ui *ui, struct ncinput *input)
{
- (void)ui;
- (void)input;
+ struct cmc_search_tab *tab = NULL;
+ if (ui->sp.tabs.count > 0) {
+ tab = &al_array_at(ui->sp.tabs, ui->sp.current);
+ }
+
+ if (tab) {
+ if (tab->input_active) {
+ if (cmc_ui_consider_input(input, true)) {
+ if (handle_text_input(tab, input)) {
+ notcurses_cursor_disable(ui->nc);
+ str query;
+ al_wstr_to_str(&tab->input_text, &query);
+ camu_client_create_search(&ui->c->client, &al_str_c("youtube"), &query);
+ al_str_free(&query);
+ }
+ }
+ } else {
+ switch (input->id) {
+ case 'i':
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ tab->input_active = true;
+ tab->input_changed = true;
+ s32 absx = ncplane_abs_x(tab->input);
+ s32 absy = ncplane_abs_y(tab->input);
+ notcurses_cursor_enable(ui->nc, absy, absx + tab->cursor);
+ break;
+ }
+ }
+ }
+
return true;
}
+void cmc_sp_erase(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->sp.n);
+}
+
void cmc_sp_render(struct cmc_ui *ui)
{
- if (ui->sp.search) {
- char *c_str = al_str_to_c_str(&ui->sp.search->query);
- ncplane_putstr_yx(ui->sp.n, 0, 0, c_str);
- al_free(c_str);
+ struct cmc_search_tab *tab = NULL;
+ if (ui->sp.tabs.count > 0) {
+ tab = &al_array_at(ui->sp.tabs, ui->sp.current);
+ }
+
+ if (tab) {
+ if (tab->input_changed) {
+ ncplane_erase(tab->input);
+ if (tab->input_active) {
+ cmc_ui_putnwstr_yx(tab->input, 0, 0, tab->input_text.length, &tab->input_text);
+ }
+ tab->input_changed = false;
+ }
+ if (tab->input_active) {
+ s32 absx = ncplane_abs_x(tab->input);
+ s32 absy = ncplane_abs_y(tab->input);
+ notcurses_cursor_enable(ui->nc, absy, absx + tab->cursor);
+ }
+ }
+
+ /*
+ if (ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ u32 max_height = ncplane_dim_y(ui->oln) - 2;
+ u32 count = ui->c->searches.count;
+ u32 index = ui->sp.overlay_index - (ui->sp.overlay_index % max_height);
+ u32 end = MIN(index + max_height, count);
+ for (u32 i = index; i < end; i++) {
+ struct cmc_search *search = al_array_at(ui->c->searches, i);
+ if (i == ui->sp.overlay_index) {
+ ncplane_set_fg_palindex(ui->oln, 6);
+ ncplane_putchar_yx(ui->oln, i - index + 1, 2, '>');
+ cmc_ui_putstr_maxwidth_yx(ui->oln, i - index + 1, 4, search->query.length, &search->query);
+ } else {
+ ncplane_set_fg_default(ui->oln);
+ cmc_ui_putstr_maxwidth_yx(ui->oln, i - index + 1, 2, search->query.length, &search->query);
+ }
+ }
}
+ */
}
diff --git a/src/fruits/cmc/ui/ui.c b/src/fruits/cmc/ui/ui.c
index 72e4df2..84e514e 100644
--- a/src/fruits/cmc/ui/ui.c
+++ b/src/fruits/cmc/ui/ui.c
@@ -2,11 +2,27 @@
#include "ui.h"
-static s32 resize_cb(struct ncplane *p)
+void cmc_ui_putnwstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, wstr *w)
{
- struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p);
- (void)ui;
- return 0;
+ ncplane_cursor_move_yx(n, y, x);
+ u32 end = MIN(w->length, width);
+ for (u32 i = 0; i < end; i++) {
+ ncplane_putwc(n, al_wstr_at(w, i));
+ }
+}
+
+void cmc_ui_putnstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, str *s)
+{
+ ncplane_cursor_move_yx(n, y, x);
+ u32 end = MIN(s->length, width);
+ for (u32 i = 0; i < end; i++) {
+ ncplane_putchar(n, al_str_at(s, i));
+ }
+}
+
+bool cmc_ui_consider_input(struct ncinput *input, bool repeat)
+{
+ return (input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN || (repeat && input->evtype == NCTYPE_REPEAT));
}
static bool read_input(struct cmc_ui *ui, struct ncinput *input)
@@ -16,51 +32,217 @@ static bool read_input(struct cmc_ui *ui, struct ncinput *input)
return !(ret == (u32)-1 || ret == 0);
}
+static void erase_previous_pane(struct cmc_ui *ui)
+{
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ ncplane_erase(ui->lp.n);
+ break;
+ case CMC_PANE_SEARCH:
+ ncplane_erase(ui->sp.n);
+ break;
+ }
+}
+
+static void switch_to_pane(struct cmc_ui *ui, u8 pane)
+{
+ erase_previous_pane(ui);
+ ui->pane = pane;
+}
+
+void cmc_ui_queue_render(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->n);
+
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ cmc_lp_erase(ui);
+ break;
+ case CMC_PANE_SEARCH:
+ cmc_sp_erase(ui);
+ break;
+ }
+
+ if (ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ }
+
+ notcurses_render(ui->nc);
+
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ cmc_lp_render(ui);
+ break;
+ case CMC_PANE_SEARCH:
+ cmc_sp_render(ui);
+ break;
+ }
+
+ if (ui->overlay_shown) {
+ /*
+ struct nccell ncl;
+ nccell_init(&ncl);
+ nccell_load(ui->oln, &ncl, "I");
+ //nccell_set_bg_palindex(&ncl, 3);
+ nccell_set_fg_palindex(&ncl, 3);
+ ncplane_polyfill_yx(ui->oln, 0, 0, &ncl);
+ */
+ u32 width = ncplane_dim_x(ui->oln);
+ u32 height = ncplane_dim_y(ui->oln);
+ u64 c = 0;
+ ncchannels_set_fg_default(&c);
+ ncplane_cursor_move_yx(ui->oln, 0, 0);
+ ncplane_rounded_box(ui->oln, NCSTYLE_NONE, c, height - 1, width - 1, 0);
+ }
+
+ notcurses_render(ui->nc);
+}
+
+void cmc_ui_toggle_overlay(struct cmc_ui *ui)
+{
+ ui->overlay_shown = !ui->overlay_shown;
+ if (!ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ }
+}
+
static void input_poll_callback(void *userdata, s32 revents)
{
struct cmc_ui *ui = (struct cmc_ui *)userdata;
(void)revents;
+ bool do_render;
struct ncinput input;
for (;;) {
- if (!read_input(ui, &input)) break;
- bool do_render = false;
- if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ if (!read_input(ui, &input)) {
+ break;
+ }
+
+ do_render = false;
+
+ if (cmc_ui_consider_input(&input, false)) {
switch (input.id) {
case 'q':
nn_event_loop_break_one(ui->loop);
break;
+ case '1':
+ switch_to_pane(ui, CMC_PANE_LIST);
+ break;
+ case '2':
+ switch_to_pane(ui, CMC_PANE_SEARCH);
+ break;
+ case '3':
+ break;
+ case '4':
+ break;
+ case NCKEY_TAB:
+ cmc_ui_toggle_overlay(ui);
+ break;
}
}
+
switch (ui->pane) {
+ case CMC_PANE_LIST:
+ if (cmc_lp_handle_input(ui, &input)) {
+ do_render |= true;
+ }
+ break;
case CMC_PANE_SEARCH:
if (cmc_sp_handle_input(ui, &input)) {
- cmc_sp_render(ui);
do_render |= true;
}
+ break;
}
+
if (do_render) {
+ cmc_ui_queue_render(ui);
notcurses_render(ui->nc);
}
}
}
+static void render_timer_callback(void *userdata, struct nn_timer *timer)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)userdata;
+ (void)timer;
+ cmc_ui_queue_render(ui);
+ notcurses_render(ui->nc);
+ nn_timer_again(&ui->render_timer);
+}
+
+static bool relayout(struct cmc_ui *ui)
+{
+ struct ncplane *stdplane = notcurses_stdplane(ui->nc);
+
+ notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols);
+
+ if (ui->n) ncplane_destroy(ui->n);
+ struct ncplane_options nopts = { 0 };
+ nopts.x = 2;
+ nopts.y = 1;
+ nopts.margin_r = 4;
+ nopts.margin_b = 1;
+ nopts.flags = NCPLANE_OPTION_MARGINALIZED;
+ ui->n = ncplane_create(stdplane, &nopts);
+
+ cmc_lp_layout(ui, ui->n);
+ cmc_sp_layout(ui, ui->n);
+
+ if (ui->oln) ncplane_destroy(ui->oln);
+ u32 width = ncplane_dim_x(ui->n);
+ u32 height = ncplane_dim_y(ui->n);
+ nopts.margin_r = MAX(6u, (u32)(width / 1.5));
+ nopts.margin_b = MAX(2u, height / 3);
+ nopts.x = nopts.margin_r / 2;
+ nopts.y = nopts.margin_b / 2;
+ ui->oln = ncplane_create(ui->n, &nopts);
+
+ return true;
+}
+
+static s32 resize_cb(struct ncplane *p)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p);
+ relayout(ui);
+ return 0;
+}
+
bool cmc_ui_init(struct cmc_ui *ui, struct nn_event_loop *loop, struct cmc *c)
{
al_memset(ui, 0, sizeof(struct cmc_ui));
+
ui->c = c;
- if (!(ui->nc = notcurses_init(NULL, stdin))) {
+ struct notcurses_options opts = { 0 };
+ opts.flags = NCOPTION_INHIBIT_SETLOCALE;
+ if (!(ui->nc = notcurses_init(&opts, stdin))) {
return false;
}
+ notcurses_mice_enable(ui->nc, NCKEY_BUTTON1);
+
ui->loop = loop;
- notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols);
+
+ cmc_lp_init(ui);
cmc_sp_init(ui);
+
+ ui->pending_layout = false;
+ ui->pending_render = false;
+
+ ui->pane = CMC_PANE_SEARCH;
+ ui->overlay_shown = false;
+
+ relayout(ui);
+
struct ncplane *stdplane = notcurses_stdplane(ui->nc);
ncplane_set_resizecb(stdplane, resize_cb);
ncplane_set_userptr(stdplane, ui);
+
nn_poll_init(&ui->input_poll, input_poll_callback, ui);
nn_poll_set(&ui->input_poll, notcurses_inputready_fd(ui->nc), NNWT_POLL_READ);
nn_poll_start(&ui->input_poll, ui->loop);
- ui->pane = CMC_PANE_SEARCH;
+
+ nn_timer_init(&ui->render_timer, ui->loop, render_timer_callback, ui);
+ nn_timer_set_repeat(&ui->render_timer, NNWT_TS_FROM_USEC(300000));
+ nn_timer_again(&ui->render_timer);
+
return true;
}
diff --git a/src/fruits/cmc/ui/ui.h b/src/fruits/cmc/ui/ui.h
index 1c3b9d9..996bb73 100644
--- a/src/fruits/cmc/ui/ui.h
+++ b/src/fruits/cmc/ui/ui.h
@@ -1,11 +1,29 @@
#pragma once
#include <al/types.h>
+#include <al/array.h>
+#include <al/wstr.h>
#include <nnwt/event_loop.h>
+#include <nnwt/timer.h>
#include <notcurses/notcurses.h>
enum {
- CMC_PANE_SEARCH = 0,
+ CMC_PANE_LIST = 0,
+ CMC_PANE_SEARCH,
+};
+
+struct cmc_list_tab {
+ struct cmc_list *list;
+ u32 selected;
+};
+
+struct cmc_search_tab {
+ struct cmc_search *search;
+ struct ncplane *input;
+ u32 cursor;
+ bool input_active;
+ wstr input_text;
+ bool input_changed;
};
struct cmc;
@@ -15,19 +33,47 @@ struct cmc_ui {
u32 term_cols;
u32 term_rows;
bool pending_layout;
- struct nn_poll input_poll;
+ bool pending_render;
u8 pane;
+ struct ncplane *n; // window.
+ struct ncplane *oln; // overlay.
+ bool overlay_shown;
+ u32 last_mouse_x;
+ u32 last_mouse_y;
struct {
struct ncplane *n;
- struct cmc_search *search;
+ array(struct cmc_list_tab) tabs;
+ u32 current;
+ } lp; // list pane.
+ struct {
+ struct ncplane *n;
+ array(struct cmc_search_tab) tabs;
+ u32 current;
} sp; // search pane.
+ struct nn_poll input_poll;
+ struct nn_timer render_timer;
struct cmc *c;
};
+void cmc_ui_putnwstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, wstr *w);
+void cmc_ui_putnstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, str *s);
+bool cmc_ui_consider_input(struct ncinput *input, bool repeat);
+
bool cmc_ui_init(struct cmc_ui *ui, struct nn_event_loop *loop, struct cmc *c);
+void cmc_ui_queue_render(struct cmc_ui *ui);
+void cmc_ui_toggle_overlay(struct cmc_ui *ui);
void cmc_ui_close(struct cmc_ui *ui);
+void cmc_lp_init(struct cmc_ui *ui);
+void cmc_lp_layout(struct cmc_ui *ui, struct ncplane *parent);
+void cmc_lp_add_list(struct cmc_ui *ui, struct cmc_list *list);
+bool cmc_lp_handle_input(struct cmc_ui *ui, struct ncinput *input);
+void cmc_lp_erase(struct cmc_ui *ui);
+void cmc_lp_render(struct cmc_ui *ui);
+
void cmc_sp_init(struct cmc_ui *ui);
void cmc_sp_layout(struct cmc_ui *ui, struct ncplane *parent);
+void cmc_sp_add_search(struct cmc_ui *ui, struct cmc_search *search);
bool cmc_sp_handle_input(struct cmc_ui *ui, struct ncinput *input);
+void cmc_sp_erase(struct cmc_ui *ui);
void cmc_sp_render(struct cmc_ui *ui);
diff --git a/src/fruits/cmsrv/cmsrv.c b/src/fruits/cmsrv/cmsrv.c
index 5b65275..2f8322c 100644
--- a/src/fruits/cmsrv/cmsrv.c
+++ b/src/fruits/cmsrv/cmsrv.c
@@ -120,16 +120,6 @@ static s32 log_callback(void *userdata, u8 level, char *message)
}
#endif
-static void server_meta_callback(void *userdata, u8 op, struct lia_list *list, struct lia_list_entry *entry)
-{
- struct cmsrv *s = (struct cmsrv *)userdata;
- (void)s;
- (void)list;
- if (op == LIANA_META_PLAYING) {
- al_log_info("cmsrv", "now playing: %ls.", entry->name.data);
- }
-}
-
static void quit_signal_callback(void *userdata)
{
struct cmsrv *s = (struct cmsrv *)userdata;
@@ -170,8 +160,6 @@ s32 main(s32 argc, char *argv[])
nn_signal_start(&s.quit_signal);
camu_server_init(&s.server, &s.loop);
- s.server.meta_callback = server_meta_callback;
- s.server.userdata = &s;
if (!camu_server_listen(&s.server, CAMU_TEST_TYPE, &CAMU_TEST_ADDR, CAMU_PORT)) {
return EXIT_FAILURE;
}
diff --git a/src/fruits/cmsrv/meson.build b/src/fruits/cmsrv/meson.build
index 2f33dee..dce3d94 100644
--- a/src/fruits/cmsrv/meson.build
+++ b/src/fruits/cmsrv/meson.build
@@ -5,7 +5,7 @@ cmsrv_args = []
use_tui = true
if use_tui
cmsrv_src += ['ui.c']
- cmsrv_deps += [dependency('notcurses-core')]
+ cmsrv_deps += [notcurses_core]
endif
if is_windows and meson.is_cross_build()
diff --git a/src/fruits/cmv/cmv.c b/src/fruits/cmv/cmv.c
index 65fc086..9926f32 100644
--- a/src/fruits/cmv/cmv.c
+++ b/src/fruits/cmv/cmv.c
@@ -26,18 +26,6 @@ struct cmv {
};
#ifndef CAMU_SINK_ONLY
-static void server_meta_callback(void *userdata, u8 op, struct lia_list *list, struct lia_list_entry *entry)
-{
- struct cmv *c = (struct cmv *)userdata;
- (void)c;
- (void)list;
- if (op == LIANA_META_PLAYING) {
- al_log_info("cmv", "now playing: %ls.", entry->name.data);
- }
-}
-#endif
-
-#ifndef CAMU_SINK_ONLY
static void exit_callback(void *userdata, struct camu_desktop *desktop)
{
struct cmv *c = (struct cmv *)userdata;
@@ -53,7 +41,6 @@ static nn_thread_result NNWT_THREADCALL event_loop_thread(void *userdata)
return 0;
}
-
static struct cmv c = { 0 };
static void sigint_handler(int signum)
@@ -106,8 +93,6 @@ s32 main(s32 argc, char *argv[])
type = NNWT_SOCKET_UNIX; addr = &CMV_UNIX_PATH;
#endif
camu_server_init(&c.server, &c.loop);
- c.server.meta_callback = server_meta_callback;
- c.server.userdata = &c;
#ifdef CAMU_DIRECT_MODE
camu_server_bind_direct(&c.server);
#else
diff --git a/src/fruits/meson.build b/src/fruits/meson.build
new file mode 100644
index 0000000..2ceeaa0
--- /dev/null
+++ b/src/fruits/meson.build
@@ -0,0 +1,35 @@
+if get_option('server').enabled() or get_option('client').enabled()
+ notcurses = dependency('notcurses', required: false, allow_fallback: false)
+ notcurses_core = dependency('notcurses-core', required: false, allow_fallback: false)
+ if not notcurses.found() or not notcurses_core.found()
+ notcurses_opts = cmake.subproject_options()
+ notcurses_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' })
+ notcurses_opts.add_cmake_defines({ 'USE_CXX': false })
+ notcurses_opts.add_cmake_defines({ 'USE_DOCTEST': false })
+ notcurses_opts.add_cmake_defines({ 'USE_PANDOC': false })
+ notcurses_opts.add_cmake_defines({ 'BUILD_EXECUTABLES': false })
+ notcurses_opts.add_cmake_defines({ 'BUILD_FFI_LIBRARY': false })
+ notcurses_opts.add_cmake_defines({ 'USE_POC': false })
+ notcurses_opts.add_cmake_defines({ 'USE_STATIC': false })
+ notcurses_opts.add_cmake_defines({ 'USE_MULTIMEDIA': 'ffmpeg' })
+ notcurses_proj = cmake.subproject('notcurses', options: notcurses_opts)
+ notcurses = notcurses_proj.dependency('notcurses')
+ notcurses_core = notcurses_proj.dependency('notcurses-core')
+ endif
+endif
+
+if get_option('server').enabled()
+ subdir('cmsrv')
+endif
+
+if get_option('client').enabled()
+ subdir('cmc')
+endif
+
+if get_option('sink').enabled()
+ if is_android
+ subdir('ctv')
+ else
+ subdir('cmv')
+ endif
+endif
diff --git a/src/liana/list.c b/src/liana/list.c
index fa21fe6..fc93fc4 100644
--- a/src/liana/list.c
+++ b/src/liana/list.c
@@ -96,8 +96,7 @@ static bool entry_load_and_get_duration(struct lia_list *list, struct lia_list_e
}
}
*error = true;
- u8 meta = LIANA_META_FAILED;
- list->callback(list->userdata, LIANA_LIST_META, entry, &meta);
+ list->callback(list->userdata, LIANA_LIST_META, entry, (u8[]){ LIANA_META_ENTRY_ERRORED });
return false;
}
*error = false;
@@ -230,12 +229,11 @@ static bool handle_add(struct lia_list *list, struct lia_list_entry *entry)
sink->set = list->current;
sink->callback(sink->userdata, LIANA_SINK_SET, entry, list->current, &time);
}
- u8 meta = LIANA_META_PLAYING;
- list->callback(list->userdata, LIANA_LIST_META, entry, &meta);
+ list->callback(list->userdata, LIANA_LIST_META, entry, (u8[]){ LIANA_META_ADDED_ENTRY });
+ list->callback(list->userdata, LIANA_LIST_META, entry, (u8[]){ LIANA_META_CURRENT_CHANGED });
} else {
entry->start = LIANA_TIMESTAMP_INVALID;
- u8 meta = LIANA_META_QUEUED;
- list->callback(list->userdata, LIANA_LIST_META, entry, &meta);
+ list->callback(list->userdata, LIANA_LIST_META, entry, (u8[]){ LIANA_META_ADDED_ENTRY });
}
al_array_push(list->entries, entry);
return true;
@@ -405,8 +403,7 @@ static bool handle_skipto(struct lia_list *list, s32 sequence, s32 index)
sink->callback(sink->userdata, LIANA_SINK_SET, target, index, &time);
}
- u8 meta = LIANA_META_PLAYING;
- list->callback(list->userdata, LIANA_LIST_META, target, &meta);
+ list->callback(list->userdata, LIANA_LIST_META, target, (u8[]){ LIANA_META_CURRENT_CHANGED });
return true;
}
@@ -521,6 +518,8 @@ static void handle_seek(struct lia_list *list, s32 sequence, u32 id, f64 percent
}
sink->callback(sink->userdata, LIANA_SINK_SEEK, entry, sequence, &time);
}
+
+ list->callback(list->userdata, LIANA_LIST_META, entry, (u8[]){ LIANA_META_ENTRY_SEEKED });
}
static bool handle_end(struct lia_list *list, u32 id, u32 reset_id)
@@ -567,8 +566,7 @@ static bool handle_end(struct lia_list *list, u32 id, u32 reset_id)
sink->set = list->current;
}
struct lia_list_entry *current = al_array_at(list->entries, list->current);
- u8 meta = LIANA_META_PLAYING;
- list->callback(list->userdata, LIANA_LIST_META, current, &meta);
+ list->callback(list->userdata, LIANA_LIST_META, current, (u8[]){ LIANA_META_CURRENT_CHANGED });
} else if (next < size) {
struct lia_list_cmd *cmd = list->cmd;
cmd->op = SKIPTO;
diff --git a/src/liana/list.h b/src/liana/list.h
index 3750d09..674f194 100644
--- a/src/liana/list.h
+++ b/src/liana/list.h
@@ -5,7 +5,7 @@
#include <al/array.h>
#define LIANA_SEQUENCE_ANY -1
-#define LIANA_TIMESTAMP_INVALID UINT64_MAX
+#define LIANA_TIMESTAMP_INVALID ((u64)-1)
#define LIANA_BASE_DELAY 575000Lu // 575ms
#define LIANA_BASE_PING 150000Lu // 150ms
@@ -16,6 +16,11 @@
//#define LIANA_LIST_SCUFFED_LOOP
+// NOTE: To handle an entry being queued right before a skip, keep a global
+// "max time until all sinks buffered" and used that instead of LIANA_PAUSE_DELAY (if greater).
+
+// TODO: Factor in LIANA_BASE_PING.
+
enum {
LIANA_SINK_SET = 0,
LIANA_SINK_UNSET,
@@ -41,16 +46,15 @@ enum {
};
enum {
- LIANA_META_PLAYING = 0,
- LIANA_META_QUEUED,
- LIANA_META_FAILED
+ LIANA_META_CURRENT_CHANGED = 0,
+ LIANA_META_ADDED_ENTRY,
+ LIANA_META_REMOVED_ENTRY,
+ LIANA_META_ENTRY_PAUSED,
+ LIANA_META_ENTRY_SEEKED,
+ LIANA_META_ENTRY_ERRORED,
+ LIANA_META_ORDER_CHANGED
};
-// NOTE: To handle an entry being queued right before a skip, keep a global
-// "max time until all sinks buffered" and used that instead of LIANA_PAUSE_DELAY (if greater).
-
-// TODO: Factor in LIANA_BASE_PING.
-
enum {
LIANA_PAUSE_NONE = 0,
LIANA_PAUSE_RESUME,
diff --git a/src/libclient/client.c b/src/libclient/client.c
index dcceb05..525b26e 100644
--- a/src/libclient/client.c
+++ b/src/libclient/client.c
@@ -4,6 +4,7 @@
#include "common.h"
#include "../server/common.h"
+#include "../liana/list.h"
static bool results_callback(void *userdata, struct nn_rpc_connection *conn,
struct nn_packet *packet, struct nn_packet *rpacket)
@@ -20,7 +21,7 @@ static bool results_callback(void *userdata, struct nn_rpc_connection *conn,
break;
}
case CAMU_CLIENT_GET_PAGE:
- client->callback(client->userdata, CAMU_CLIENT_PAGE_RESULTS, packet);
+ client->callback(client->userdata, CAMU_CLIENT_GOT_RESULTS, packet);
break;
}
@@ -29,14 +30,29 @@ static bool results_callback(void *userdata, struct nn_rpc_connection *conn,
return false;
}
+static bool meta_callback(void *userdata, struct nn_rpc_connection *conn,
+ struct nn_packet *packet, struct nn_packet *rpacket)
+{
+ struct camu_client *client = (struct camu_client *)userdata;
+ (void)conn;
+ (void)rpacket;
+
+ client->callback(client->userdata, CAMU_CLIENT_GOT_META, packet);
+
+ nn_packet_stream_return_packet(conn->stream, packet);
+
+ return false;
+}
+
static struct nn_rpc_command commands[] = {
- { .op = CAMU_CLIENT_RESULTS, .callback = results_callback, .userdata = NULL }
+ { .op = CAMU_CLIENT_RESULTS, .callback = results_callback, .userdata = NULL },
+ { .op = CAMU_CLIENT_META, .callback = meta_callback, .userdata = NULL }
};
static void idd_callback(void *userdata, struct nn_rpc_connection *conn, struct nn_packet *packet)
{
struct camu_client *client = (struct camu_client *)userdata;
- client->callback(client->userdata, CAMU_CLIENT_LOGIN, packet);
+ client->callback(client->userdata, CAMU_CLIENT_LOGGED_IN, packet);
nn_packet_stream_return_packet(conn->stream, packet);
}
@@ -119,24 +135,24 @@ void camu_client_get_page(struct camu_client *client, s32 id, u32 num)
nn_rpc_connection_command(client->conn, packet, NULL, NULL);
}
-void camu_client_add_from_path(struct camu_client *client, str *list, str *path)
+void camu_client_skipto(struct camu_client *client, str *list, s32 i)
{
struct nn_packet *packet = nn_rpc_get_packet(&client->client, CAMU_SERVER_LIST_ACTION);
nn_packet_write_str(packet, list);
- nn_packet_write_u8(packet, CAMU_LIST_ADD);
- nn_packet_write_u8(packet, CAMU_RESOURCE_FILE);
- nn_packet_write_str(packet, path);
+ nn_packet_write_u8(packet, CAMU_LIST_SKIPTO);
+ nn_packet_write_s32(packet, LIANA_SEQUENCE_ANY);
+ nn_packet_write_s32(packet, i);
nn_rpc_connection_command(client->conn, packet, NULL, NULL);
}
-void camu_client_add_from_post(struct camu_client *client, str *list, str *unique_id, u32 index)
+void camu_client_seek(struct camu_client *client, str *list, u32 id, f64 percent)
{
struct nn_packet *packet = nn_rpc_get_packet(&client->client, CAMU_SERVER_LIST_ACTION);
nn_packet_write_str(packet, list);
- nn_packet_write_u8(packet, CAMU_LIST_ADD);
- nn_packet_write_u8(packet, CAMU_RESOURCE_PORTAL);
- nn_packet_write_str(packet, unique_id);
- nn_packet_write_u32(packet, index);
+ nn_packet_write_u8(packet, CAMU_LIST_SEEK);
+ nn_packet_write_s32(packet, LIANA_SEQUENCE_ANY);
+ nn_packet_write_u32(packet, id);
+ nn_packet_write_f64(packet, percent);
nn_rpc_connection_command(client->conn, packet, NULL, NULL);
}
diff --git a/src/libclient/client.h b/src/libclient/client.h
index fee7de1..e6e80c8 100644
--- a/src/libclient/client.h
+++ b/src/libclient/client.h
@@ -3,9 +3,10 @@
#include <nnwt/rpc2.h>
enum {
- CAMU_CLIENT_LOGIN = 0,
+ CAMU_CLIENT_LOGGED_IN = 0,
CAMU_CLIENT_SEARCH_CREATED,
- CAMU_CLIENT_PAGE_RESULTS
+ CAMU_CLIENT_GOT_RESULTS,
+ CAMU_CLIENT_GOT_META
};
struct camu_client {
@@ -28,7 +29,7 @@ void camu_client_toggle_sink(struct camu_client *client, str *sink, str *list, b
void camu_client_create_search(struct camu_client *client, str *module, str *query);
void camu_client_get_page(struct camu_client *client, s32 id, u32 num);
-void camu_client_add_from_path(struct camu_client *client, str *list, str *path);
-void camu_client_add_from_post(struct camu_client *client, str *list, str *unique_id, u32 index);
+void camu_client_skipto(struct camu_client *client, str *list, s32 i);
+void camu_client_seek(struct camu_client *client, str *list, u32 id, f64 percent);
void camu_client_disconnect(struct camu_client *client);
diff --git a/src/libclient/common.h b/src/libclient/common.h
index 5b8dcbf..b074277 100644
--- a/src/libclient/common.h
+++ b/src/libclient/common.h
@@ -1,5 +1,6 @@
#pragma once
enum {
- CAMU_CLIENT_RESULTS = 0
+ CAMU_CLIENT_RESULTS = 0,
+ CAMU_CLIENT_META
};
diff --git a/src/server/server.c b/src/server/server.c
index 794030a..d75146e 100644
--- a/src/server/server.c
+++ b/src/server/server.c
@@ -54,7 +54,17 @@ static struct camu_server_sink *get_sink_from_name(struct camu_server *server, s
return NULL;
}
-static void write_user_state(struct camu_server *server, struct camu_user *user, struct nn_packet *packet)
+static void write_list_entry(struct lia_list_entry *entry, struct nn_packet *packet)
+{
+ nn_packet_write_u32(packet, entry->id);
+ nn_packet_write_u64(packet, entry->duration);
+ nn_packet_write_u64(packet, entry->start);
+ nn_packet_write_u64(packet, entry->paused_at);
+ nn_packet_write_u64(packet, entry->offset);
+ nn_packet_write_wstr(packet, &entry->name);
+}
+
+static void write_initial_user_state(struct camu_server *server, struct camu_user *user, struct nn_packet *packet)
{
(void)user;
#ifdef CAMU_HAVE_PORTAL
@@ -68,6 +78,17 @@ static void write_user_state(struct camu_server *server, struct camu_user *user,
#else
nn_packet_write_u32(packet, 0);
#endif
+ nn_packet_write_u32(packet, server->lists.count);
+ struct lia_list *list;
+ al_array_foreach(server->lists, i, list) {
+ nn_packet_write_str(packet, &list->name);
+ nn_packet_write_s32(packet, list->current);
+ nn_packet_write_u32(packet, list->entries.count);
+ struct lia_list_entry *entry;
+ al_array_foreach(list->entries, j, entry) {
+ write_list_entry(entry, packet);
+ }
+ }
}
static void handle_toggle_sink(struct camu_server *server, str *name, struct camu_server_sink *sink, bool enable);
@@ -100,7 +121,7 @@ static bool identify_callback(void *userdata, struct nn_rpc_connection *conn,
client->user = user;
al_array_push(server->clients, client);
al_log_info("server", "User \"%.*s\" logged in.", al_str_fmt(&user->name));
- write_user_state(server, user, rpacket);
+ write_initial_user_state(server, user, rpacket);
break;
}
case CAMU_SINK: {
@@ -207,6 +228,43 @@ static void node_callback(void *userdata, u8 op, u64 duration)
}
}
+static void send_clients_current_changed(struct camu_server *server, struct lia_list *list, struct lia_list_entry *entry)
+{
+ struct camu_server_client *client;
+ al_array_foreach(server->clients, i, client) {
+ struct nn_packet *packet = nn_rpc_get_packet(client->conn->rpc, CAMU_CLIENT_META);
+ nn_packet_write_u8(packet, LIANA_META_CURRENT_CHANGED);
+ nn_packet_write_str(packet, &list->name);
+ nn_packet_write_s32(packet, list->current);
+ write_list_entry(entry, packet);
+ nn_rpc_connection_command(client->conn, packet, NULL, NULL);
+ }
+}
+
+static void send_clients_added_entry(struct camu_server *server, struct lia_list *list, struct lia_list_entry *entry)
+{
+ struct camu_server_client *client;
+ al_array_foreach(server->clients, i, client) {
+ struct nn_packet *packet = nn_rpc_get_packet(client->conn->rpc, CAMU_CLIENT_META);
+ nn_packet_write_u8(packet, LIANA_META_ADDED_ENTRY);
+ nn_packet_write_str(packet, &list->name);
+ write_list_entry(entry, packet);
+ nn_rpc_connection_command(client->conn, packet, NULL, NULL);
+ }
+}
+
+static void send_clients_entry_seeked(struct camu_server *server, struct lia_list *list, struct lia_list_entry *entry)
+{
+ struct camu_server_client *client;
+ al_array_foreach(server->clients, i, client) {
+ struct nn_packet *packet = nn_rpc_get_packet(client->conn->rpc, CAMU_CLIENT_META);
+ nn_packet_write_u8(packet, LIANA_META_ENTRY_SEEKED);
+ nn_packet_write_str(packet, &list->name);
+ write_list_entry(entry, packet);
+ nn_rpc_connection_command(client->conn, packet, NULL, NULL);
+ }
+}
+
static void list_callback(void *userdata, u8 op, struct lia_list_entry *entry, void *opaque)
{
struct camu_server *server = (struct camu_server *)userdata;
@@ -236,8 +294,24 @@ static void list_callback(void *userdata, u8 op, struct lia_list_entry *entry, v
case LIANA_UNLOAD_ENTRY:
break;
case LIANA_LIST_META:
- if (server->meta_callback) {
- server->meta_callback(server->userdata, *(u8 *)opaque, entry->list, entry);
+ switch (*(u8 *)opaque) {
+ case LIANA_META_CURRENT_CHANGED:
+ send_clients_current_changed(server, entry->list, entry);
+ break;
+ case LIANA_META_ADDED_ENTRY:
+ send_clients_added_entry(server, entry->list, entry);
+ break;
+ case LIANA_META_REMOVED_ENTRY:
+ break;
+ case LIANA_META_ENTRY_PAUSED:
+ break;
+ case LIANA_META_ENTRY_SEEKED:
+ send_clients_entry_seeked(server, entry->list, entry);
+ break;
+ case LIANA_META_ENTRY_ERRORED:
+ break;
+ case LIANA_META_ORDER_CHANGED:
+ break;
}
break;
}
diff --git a/src/server/server.h b/src/server/server.h
index 1b250dc..210f8a4 100644
--- a/src/server/server.h
+++ b/src/server/server.h
@@ -45,8 +45,6 @@ struct camu_server {
struct camu_portal_bridge bridge;
struct camu_post_cache cache;
#endif
- void (*meta_callback)(void *, u8, struct lia_list *, struct lia_list_entry *);
- void *userdata;
};
void camu_server_init(struct camu_server *server, struct nn_event_loop *loop);
diff --git a/subprojects/notcurses.wrap b/subprojects/notcurses.wrap
new file mode 100644
index 0000000..f946c58
--- /dev/null
+++ b/subprojects/notcurses.wrap
@@ -0,0 +1,12 @@
+[wrap-git]
+url = https://github.com/dankamongmen/notcurses.git
+revision = c11efe877f2245901f5c9ce110de7a2c83cfa2ed
+depth = 1
+
+patch_directory = notcurses
+diff_files = notcurses/duration_and_ffmpeg_fix.diff
+
+method = cmake
+
+[provide]
+dependency_names = notcurses, notcurses-core
diff --git a/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff b/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff
new file mode 100644
index 0000000..71680f7
--- /dev/null
+++ b/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff
@@ -0,0 +1,166 @@
+diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
+index 9e09cb3..f4da60f 100644
+--- a/include/notcurses/notcurses.h
++++ b/include/notcurses/notcurses.h
+@@ -3406,6 +3406,10 @@ API int ncvisual_geom(const struct notcurses* nc, const struct ncvisual* n,
+ const struct ncvisual_options* vopts, ncvgeom* geom)
+ __attribute__ ((nonnull (4)));
+
++
++API uint64_t ncvisual_get_duration(const struct ncvisual* n)
++ __attribute__ ((nonnull (1)));
++
+ // Destroy an ncvisual. Rendered elements will not be disrupted, but the visual
+ // can be neither decoded nor rendered any further.
+ API void ncvisual_destroy(struct ncvisual* ncv);
+diff --git a/src/lib/visual-details.h b/src/lib/visual-details.h
+index e9347e3..6852b76 100644
+--- a/src/lib/visual-details.h
++++ b/src/lib/visual-details.h
+@@ -25,6 +25,7 @@ typedef struct ncvisual {
+ unsigned pixx, pixy; // pixel geometry, *not* cell geometry
+ // lines are sometimes padded. this many true bytes per row in data.
+ unsigned rowstride;
++ uint64_t duration;
+ bool owndata; // we own data iff owndata == true
+ } ncvisual;
+
+diff --git a/src/lib/visual.c b/src/lib/visual.c
+index 047b68e..25e8422 100644
+--- a/src/lib/visual.c
++++ b/src/lib/visual.c
+@@ -458,6 +458,10 @@ int ncvisual_geom(const notcurses* nc, const ncvisual* n,
+ &disppxy, &disppxx, &outy, &outx, &placey, &placex);
+ }
+
++uint64_t ncvisual_get_duration(const ncvisual* n){
++ return n->duration;
++}
++
+ void* rgb_loose_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha){
+ if(*rowstride % 4){ // must be a multiple of 4 bytes
+ return NULL;
+diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
+index 655f269..d2bf399 100644
+--- a/src/media/ffmpeg.c
++++ b/src/media/ffmpeg.c
+@@ -33,6 +33,7 @@ typedef struct ncvisual_details {
+ struct SwsContext* swsctx;
+ struct SwsContext* rgbactx;
+ AVSubtitle subtitle;
++ AVRational timebase;
+ int stream_index; // match against this following av_read_frame()
+ int sub_stream_index; // subtitle stream index, can be < 0 if no subtitles
+ bool packet_outstanding;
+@@ -224,9 +225,7 @@ struct ncplane* ffmpeg_subtitle(ncplane* parent, const ncvisual* ncv){
+
+ static int
+ averr2ncerr(int averr){
+- if(averr == AVERROR_EOF){
+- return 1;
+- }
++ (void)averr;
+ // FIXME need to map averror codes to ncerrors
+ //fprintf(stderr, "AVERR: %d/%x %d/%x\n", averr, averr, -averr, -averr);
+ return -1;
+@@ -326,6 +325,11 @@ ffmpeg_decode(ncvisual* n){
+ /*if(averr != AVERROR_EOF){
+ fprintf(stderr, "Error reading frame info (%s)\n", av_err2str(averr));
+ }*/
++ if(averr == AVERROR_EOF){
++ // Start flush.
++ unref = false;
++ break;
++ }
+ return averr2ncerr(averr);
+ }
+ unref = true;
+@@ -339,10 +343,10 @@ ffmpeg_decode(ncvisual* n){
+ }
+ }while(n->details->packet->stream_index != n->details->stream_index);
+ n->details->packet_outstanding = true;
+- int averr = avcodec_send_packet(n->details->codecctx, n->details->packet);
++ int averr = avcodec_send_packet(n->details->codecctx, unref ? n->details->packet : NULL);
+ if(averr < 0){
+- n->details->packet_outstanding = false;
+ av_packet_unref(n->details->packet);
++ n->details->packet_outstanding = false;
+ //fprintf(stderr, "Error processing AVPacket\n");
+ return averr2ncerr(averr);
+ }
+@@ -350,10 +354,16 @@ ffmpeg_decode(ncvisual* n){
+ int averr = avcodec_receive_frame(n->details->codecctx, n->details->frame);
+ if(averr >= 0){
+ have_frame = true;
++ }else if(averr == AVERROR_EOF){
++ n->details->packet_outstanding = false;
++ // Where EOF gets returned during normal operation.
++ return 1;
+ }else if(averr < 0){
+- av_packet_unref(n->details->packet);
+- have_frame = false;
++ if(unref){
++ av_packet_unref(n->details->packet);
++ }
+ n->details->packet_outstanding = false;
++ have_frame = false;
+ if(averr != AVERROR(EAGAIN)){
+ return averr2ncerr(averr);
+ }
+@@ -365,6 +375,8 @@ ffmpeg_decode(ncvisual* n){
+ n->rowstride = f->linesize[0];
+ n->pixx = n->details->frame->width;
+ n->pixy = n->details->frame->height;
++ const uint64_t pktduration = ffmpeg_pkt_duration(f);
++ n->duration = av_rescale_q(pktduration, n->details->timebase, AV_TIME_BASE_Q);
+ //fprintf(stderr, "good decode! %d/%d %d %p\n", n->details->frame->height, n->details->frame->width, n->rowstride, f->data);
+ ncvisual_set_data(n, f->data[0], false);
+ force_rgba(n);
+@@ -457,6 +469,7 @@ ffmpeg_from_file(const char* filename){
+ goto err;
+ }
+ AVStream* st = ncv->details->fmtctx->streams[ncv->details->stream_index];
++ ncv->details->timebase = st->time_base;
+ if((ncv->details->codecctx = avcodec_alloc_context3(ncv->details->codec)) == NULL){
+ //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename);
+ goto err;
+@@ -501,14 +514,6 @@ ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale,
+ memcpy(&activevopts, vopts, sizeof(*vopts));
+ int ncerr;
+ do{
+- // codecctx seems to be off by a factor of 2 regularly. instead, go with
+- // the time_base from the avformatctx. except ts isn't properly reset for
+- // all media when we loop =[. we seem to be accurate enough now with the
+- // tbase/ppd. see https://github.com/dankamongmen/notcurses/issues/1352.
+- double tbase = av_q2d(ncv->details->fmtctx->streams[ncv->details->stream_index]->time_base);
+- if(isnan(tbase)){
+- tbase = 0;
+- }
+ if(activevopts.n){
+ ncplane_erase(activevopts.n); // new frame could be partially transparent
+ }
+@@ -529,7 +534,7 @@ ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale,
+ }
+ // display duration in units of time_base
+ const uint64_t pktduration = ffmpeg_pkt_duration(ncv->details->frame);
+- uint64_t duration = pktduration * tbase * NANOSECS_IN_SEC;
++ uint64_t duration = av_rescale_q(pktduration, ncv->details->timebase, (AVRational){1, NANOSECS_IN_SEC});
+ double schedns = nsbegin;
+ sum_duration += (duration * timescale);
+ schedns += sum_duration;
+@@ -561,11 +566,13 @@ static int
+ ffmpeg_decode_loop(ncvisual* ncv){
+ int r = ffmpeg_decode(ncv);
+ if(r == 1){
+- if(av_seek_frame(ncv->details->fmtctx, ncv->details->stream_index, 0, AVSEEK_FLAG_FRAME) < 0){
++ avformat_flush(ncv->details->fmtctx);
++ if(avformat_seek_file(ncv->details->fmtctx, ncv->details->stream_index, 0, 0, 0, AVSEEK_FLAG_FRAME) < 0){
+ // FIXME log error
+ return -1;
+ }
+- if(ffmpeg_decode(ncv) < 0){
++ avcodec_flush_buffers(ncv->details->codecctx);
++ if(ffmpeg_decode(ncv) != 0){
+ return -1;
+ }
+ }