Improve Nomad network directory UX

This commit is contained in:
liu weikai
2026-07-09 23:53:43 +08:00
parent 027e05e90e
commit 0acd37fa2c
4 changed files with 242 additions and 7 deletions
@@ -129,6 +129,7 @@ struct NetworkPageState
DirectoryMode directory_mode = DirectoryMode::Announces;
bool immersive = false;
bool directory_collapsed = false;
bool browser_collapsed = false;
bool suppress_history = false;
rtdir::Status announce_status{};
rtdir::Status address_status{};
@@ -564,6 +565,7 @@ void rebuild_focus_group(lv_obj_t* preferred = nullptr);
void focus_browser_viewport();
void focus_directory_panel();
void apply_layout_state();
lv_coord_t resolve_directory_width();
void open_network_help_modal();
void close_network_help_modal();
@@ -650,6 +652,10 @@ void add_viewport_links_to_group()
void add_browser_focusables()
{
if (g_state.browser_collapsed && !g_state.immersive)
{
return;
}
if (!g_state.immersive)
{
add_visible_to_group(g_state.browser_back_btn);
@@ -1550,6 +1556,7 @@ void apply_layout_state()
{
const bool immersive = g_state.immersive;
const bool hide_directory = immersive || g_state.directory_collapsed;
const bool hide_browser = !immersive && g_state.browser_collapsed;
if (g_state.header)
{
immersive ? lv_obj_add_flag(g_state.header, LV_OBJ_FLAG_HIDDEN)
@@ -1559,6 +1566,14 @@ void apply_layout_state()
{
hide_directory ? lv_obj_add_flag(g_state.directory_panel, LV_OBJ_FLAG_HIDDEN)
: lv_obj_clear_flag(g_state.directory_panel, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_width(g_state.directory_panel,
hide_browser ? LV_PCT(100) : resolve_directory_width());
lv_obj_set_flex_grow(g_state.directory_panel, hide_browser ? 1 : 0);
}
if (g_state.browser_panel)
{
hide_browser ? lv_obj_add_flag(g_state.browser_panel, LV_OBJ_FLAG_HIDDEN)
: lv_obj_clear_flag(g_state.browser_panel, LV_OBJ_FLAG_HIDDEN);
}
if (g_state.browser_toolbar)
{
@@ -1571,7 +1586,9 @@ void apply_layout_state()
lv_obj_set_style_pad_right(g_state.content, immersive ? 0 : 2, LV_PART_MAIN);
lv_obj_set_style_pad_top(g_state.content, immersive ? 0 : 2, LV_PART_MAIN);
lv_obj_set_style_pad_bottom(g_state.content, immersive ? 0 : 2, LV_PART_MAIN);
lv_obj_set_style_pad_column(g_state.content, hide_directory ? 0 : 4, LV_PART_MAIN);
lv_obj_set_style_pad_column(g_state.content,
(hide_directory || hide_browser) ? 0 : 4,
LV_PART_MAIN);
}
if (g_state.browser_panel)
{
@@ -1583,12 +1600,12 @@ void apply_layout_state()
{
lv_obj_set_style_radius(g_state.viewport, immersive ? 0 : 8, LV_PART_MAIN);
lv_obj_set_style_border_width(g_state.viewport, immersive ? 0 : 1, LV_PART_MAIN);
if (app_g)
if (app_g && !hide_browser)
{
lv_group_focus_obj(g_state.viewport);
}
}
rebuild_focus_group(g_state.viewport);
rebuild_focus_group(hide_browser ? g_state.directory_list : g_state.viewport);
}
void toggle_immersive()
@@ -1603,7 +1620,12 @@ void toggle_directory_panel()
{
g_state.immersive = false;
}
g_state.directory_collapsed = !g_state.directory_collapsed;
const bool hide_directory = !g_state.directory_collapsed;
g_state.directory_collapsed = hide_directory;
if (hide_directory)
{
g_state.browser_collapsed = false;
}
apply_layout_state();
::ui::feedback::show_notice(g_state.directory_collapsed ? safe_tr("Directory hidden")
: safe_tr("Directory shown"),
@@ -1618,6 +1640,32 @@ void toggle_directory_panel()
}
}
void toggle_browser_panel()
{
if (g_state.immersive)
{
g_state.immersive = false;
}
const bool hide_browser = !g_state.browser_collapsed;
g_state.browser_collapsed = hide_browser;
if (hide_browser)
{
g_state.directory_collapsed = false;
}
apply_layout_state();
::ui::feedback::show_notice(g_state.browser_collapsed ? safe_tr("Browser hidden")
: safe_tr("Browser shown"),
1200);
if (g_state.browser_collapsed)
{
focus_directory_panel();
}
else
{
focus_browser_viewport();
}
}
bool is_help_shortcut_key(uint32_t key)
{
return key == 'h' || key == 'H';
@@ -1628,6 +1676,11 @@ bool is_directory_toggle_shortcut_key(uint32_t key)
return key == 'c' || key == 'C';
}
bool is_browser_toggle_shortcut_key(uint32_t key)
{
return key == 'b' || key == 'B';
}
void close_network_help_modal()
{
::ui::components::shortcut_help_modal::close(g_state.help_modal);
@@ -1645,10 +1698,11 @@ void open_network_help_modal()
return;
}
::ui::components::shortcut_help_modal::Row rows[10] = {};
::ui::components::shortcut_help_modal::Row rows[11] = {};
std::size_t row_count = 0;
rows[row_count++] = {"S", "/", "Search announces"};
rows[row_count++] = {"C", nullptr, "Show or hide left column"};
rows[row_count++] = {"B", nullptr, "Show or hide browser"};
rows[row_count++] = {"I", nullptr, "Immersive browser"};
rows[row_count++] = {"Left", "Right", "Switch pane focus"};
rows[row_count++] = {"Enter", nullptr, "Open selected item"};
@@ -1691,15 +1745,30 @@ void page_shortcut_event_cb(lv_event_t* event)
lv_event_stop_processing(event);
return;
}
if (is_browser_toggle_shortcut_key(key))
{
toggle_browser_panel();
lv_event_stop_processing(event);
return;
}
if (key == LV_KEY_RIGHT && object_in_subtree(g_state.directory_panel, target))
{
if (g_state.browser_collapsed)
{
g_state.browser_collapsed = false;
apply_layout_state();
}
focus_browser_viewport();
lv_event_stop_processing(event);
return;
}
if (key == LV_KEY_LEFT && object_in_subtree(g_state.browser_panel, target) &&
!g_state.directory_collapsed)
if (key == LV_KEY_LEFT && object_in_subtree(g_state.browser_panel, target))
{
if (g_state.directory_collapsed)
{
g_state.directory_collapsed = false;
apply_layout_state();
}
focus_directory_panel();
lv_event_stop_processing(event);
return;
@@ -464,6 +464,48 @@ void copyCString(char* out, size_t out_len, const char* in)
out[out_len - 1] = '\0';
}
bool copyTextAppDataDisplayName(const uint8_t* data,
size_t len,
char* out,
size_t out_len)
{
if (!data || len == 0 || len > 96 || !out || out_len == 0)
{
return false;
}
size_t used = 0;
bool has_visible = false;
for (size_t index = 0; index < len; ++index)
{
uint8_t byte = data[index];
if (byte == '\t' || byte == '\r' || byte == '\n')
{
byte = ' ';
}
else if (byte == 0 || byte < 0x20 || byte == 0x7F)
{
out[0] = '\0';
return false;
}
if (used + 1U < out_len)
{
out[used++] = static_cast<char>(byte);
}
if (byte != ' ')
{
has_visible = true;
}
}
while (used != 0 && out[used - 1U] == ' ')
{
--used;
}
out[used] = '\0';
return has_visible && used != 0;
}
uint32_t fnv1a32(const uint8_t* data, size_t len)
{
uint32_t hash = 2166136261UL;
@@ -2237,6 +2279,14 @@ bool LxmfAdapter::handleAnnouncePacket(const uint8_t* raw_packet, size_t raw_len
(void)has_stamp_cost;
(void)stamp_cost;
}
else if (!(delivery_announce || propagation_announce || call_audio_announce) &&
announce.app_data && announce.app_data_len != 0)
{
(void)copyTextAppDataDisplayName(announce.app_data,
announce.app_data_len,
announce_display_name,
sizeof(announce_display_name));
}
rtdir::AnnounceRecord directory_announce{};
directory_announce.valid = true;
@@ -207,6 +207,58 @@ bool parse_hex(std::string_view text, uint8_t* out, std::size_t out_len)
return true;
}
bool copy_text_app_data_display_name_from_hex(std::string_view text,
char* out,
std::size_t out_len)
{
if (!out || out_len == 0 || text.empty() || (text.size() % 2U) != 0)
{
return false;
}
const std::size_t byte_count = text.size() / 2U;
if (byte_count > 96)
{
return false;
}
std::size_t used = 0;
bool has_visible = false;
for (std::size_t index = 0; index < byte_count; ++index)
{
const uint8_t hi = hex_nibble(text[index * 2U]);
const uint8_t lo = hex_nibble(text[(index * 2U) + 1U]);
if (hi == 0xFF || lo == 0xFF)
{
out[0] = '\0';
return false;
}
uint8_t byte = static_cast<uint8_t>((hi << 4U) | lo);
if (byte == '\t' || byte == '\r' || byte == '\n')
{
byte = ' ';
}
else if (byte == 0 || byte < 0x20 || byte == 0x7F)
{
out[0] = '\0';
return false;
}
if (used + 1U < out_len)
{
out[used++] = static_cast<char>(byte);
}
if (byte != ' ')
{
has_visible = true;
}
}
while (used != 0 && out[used - 1U] == ' ')
{
--used;
}
out[used] = '\0';
return has_visible && used != 0;
}
std::string sanitize_field(const char* text)
{
std::string out = text ? text : "";
@@ -1063,6 +1115,12 @@ bool parse_announce_line(std::string_view line, AnnounceRecord& out)
parsed.delivery = truthy(fields.values[9]);
parsed.propagation = truthy(fields.values[10]);
copy_view(parsed.display_name, sizeof(parsed.display_name), fields.values[11]);
if (parsed.display_name[0] == '\0' && fields.count >= 14)
{
(void)copy_text_app_data_display_name_from_hex(fields.values[13],
parsed.display_name,
sizeof(parsed.display_name));
}
parsed.valid = true;
out = parsed;
return true;
@@ -151,6 +151,58 @@ bool parse_hex(std::string_view text, uint8_t* out, std::size_t out_len)
return true;
}
bool copy_text_app_data_display_name_from_hex(std::string_view text,
char* out,
std::size_t out_len)
{
if (!out || out_len == 0 || text.empty() || (text.size() % 2U) != 0)
{
return false;
}
const std::size_t byte_count = text.size() / 2U;
if (byte_count > 96)
{
return false;
}
std::size_t used = 0;
bool has_visible = false;
for (std::size_t index = 0; index < byte_count; ++index)
{
const uint8_t hi = hex_nibble(text[index * 2U]);
const uint8_t lo = hex_nibble(text[(index * 2U) + 1U]);
if (hi == 0xFF || lo == 0xFF)
{
out[0] = '\0';
return false;
}
uint8_t byte = static_cast<uint8_t>((hi << 4U) | lo);
if (byte == '\t' || byte == '\r' || byte == '\n')
{
byte = ' ';
}
else if (byte == 0 || byte < 0x20 || byte == 0x7F)
{
out[0] = '\0';
return false;
}
if (used + 1U < out_len)
{
out[used++] = static_cast<char>(byte);
}
if (byte != ' ')
{
has_visible = true;
}
}
while (used != 0 && out[used - 1U] == ' ')
{
--used;
}
out[used] = '\0';
return has_visible && used != 0;
}
std::string sanitize_field(const char* text)
{
std::string out = text ? text : "";
@@ -502,6 +554,12 @@ bool parse_announce_line(std::string_view line, AnnounceRecord& out)
parsed.delivery = truthy(fields.values[9]);
parsed.propagation = truthy(fields.values[10]);
copy_view(parsed.display_name, sizeof(parsed.display_name), fields.values[11]);
if (parsed.display_name[0] == '\0' && fields.count >= 14)
{
(void)copy_text_app_data_display_name_from_hex(fields.values[13],
parsed.display_name,
sizeof(parsed.display_name));
}
parsed.valid = true;
out = parsed;
return true;