Files
Asuro/src/app/app.filebrowser.pas
T

3314 lines
122 KiB
ObjectPascal

{
App->FileBrowser - GUI file browser / manager
Phases 2-7: window, VFS loading, navigation, detail list with
type column, breadcrumb bar, file operations (delete, rename,
create folder/file), right-click context menu.
@author(Aaron Hance <[email protected]>)
}
unit app.filebrowser;
interface
procedure init();
implementation
uses
driver.video.desktop,
driver.video.windows,
driver.video.lvgl,
driver.video,
driver.storage.vfs,
driver.storage.types,
driver.storage.filedispatch,
core.ds.hashmap,
memory.heap,
proc.mgr,
proc.types,
core.strings,
core.strings.helpers,
core.gfx.fileicons,
debug.tracer,
core.util,
arch.x86.util;
const
WIN_W = 800;
WIN_H = 600;
TOOLBAR_H = 36;
STATUSBAR_H = 24;
ENTRY_MAX = 256;
HIST_MAX = 32;
MAX_TABS = 8;
MAX_BOOKMARKS = 16;
PATH_MAX = 256;
{ LVGL FontAwesome symbol UTF-8 strings (from lv_symbol_def.h) }
SYM_LEFT = #$EF#$81#$93; { U+F053 }
SYM_RIGHT = #$EF#$81#$94; { U+F054 }
SYM_UP = #$EF#$81#$B7; { U+F077 }
SYM_REFRESH = #$EF#$80#$A1; { U+F021 }
SYM_PLUS = #$EF#$81#$A7; { U+F067 }
SYM_DIRECTORY = #$EF#$81#$BB; { U+F07B }
SYM_FILE = #$EF#$85#$9B; { U+F158 }
SYM_EDIT = #$EF#$8C#$84; { U+F304 }
SYM_TRASH = #$EF#$8B#$AD; { U+F2ED }
SYM_CLOSE = #$EF#$80#$8D; { U+F00D }
SYM_EYE_OPEN = #$EF#$81#$AE; { U+F06E }
SYM_IMAGE = #$EF#$80#$BE; { U+F03E }
SYM_LIST = #$EF#$80#$8B; { U+F00B }
SYM_HOME = #$EF#$80#$95; { U+F015 }
SYM_DRIVE = #$EF#$80#$9C; { U+F01C }
type
TViewMode = (vmDetail, vmCompact);
{ Heap-allocated name/size buffers (Lesson #11: no large stack arrays) }
TNameBuf = array[0..ENTRY_MAX - 1] of pchar;
PNameBuf = ^TNameBuf;
TSizeBuf = array[0..ENTRY_MAX - 1] of uint32;
PSizeBuf = ^TSizeBuf;
{ Collect context for hashmap forEach callback }
PFBCollectCtx = ^TFBCollectCtx;
TFBCollectCtx = record
dir_names : PNameBuf;
file_names : PNameBuf;
file_sizes : PSizeBuf;
dir_count : uint32;
file_count : uint32;
end;
{ Per-tab state snapshot }
PTabSnapshot = ^TTabSnapshot;
TTabSnapshot = record
cur_path : pchar;
history : array[0..HIST_MAX - 1] of pchar;
hist_len : uint32;
hist_pos : uint32;
sel_path : pchar;
sel_is_dir : boolean;
item_count : uint32;
filter_text: pchar;
end;
PFileBrowserState = ^TFileBrowserState;
TFileBrowserState = record
win_id : uint32;
pid : uint32;
{ Main widget pointers }
toolbar : Plv_obj;
tab_bar : Plv_obj;
content_area : Plv_obj;
status_bar : Plv_obj;
status_label : Plv_obj;
{ Navigation }
cur_path : pchar;
history : array[0..HIST_MAX - 1] of pchar;
hist_len : uint32;
hist_pos : uint32;
{ Nav buttons }
back_btn : Plv_obj;
fwd_btn : Plv_obj;
up_btn : Plv_obj;
{ Breadcrumb / path edit }
breadcrumb : Plv_obj;
path_ta : Plv_obj;
path_editing : boolean;
{ Selection }
sel_path : pchar; { full path of selected item (heap-alloc'd) }
sel_is_dir : boolean; { true if selected item is a directory }
sel_row : Plv_obj; { the highlighted row object }
{ File ops state }
op_err : TError; { result from async ops }
{ Context menu }
ctx_menu : Plv_obj; { popup panel, nil when hidden }
{ Tabs }
tabs : array[0..MAX_TABS - 1] of PTabSnapshot;
tab_btns : array[0..MAX_TABS - 1] of Plv_obj;
tab_count : uint32;
active_tab : uint32;
{ View state }
view_mode : TViewMode;
item_count : uint32;
{ Bookmarks }
body_row : Plv_obj;
sidebar : Plv_obj;
bookmarks : array[0..MAX_BOOKMARKS - 1] of pchar;
bm_count : uint32;
{ Filter }
filter_ta : Plv_obj;
filter_text : pchar;
{ Type-ahead }
ta_buf : array[0..31] of char;
ta_len : uint32;
ta_timer : Plv_timer;
{ Preview panel }
preview_panel : Plv_obj;
preview_visible: boolean;
{ Sorting }
sort_col : uint32; { 0=name, 1=size, 2=type }
sort_asc : boolean;
{ Hidden files }
show_hidden : boolean;
{ Directory watch }
watch_id : uint32;
watch_dirty : boolean;
watch_timer : Plv_timer;
{ Async refresh pipeline }
refresh_pending : uint32; { 1 = fb_entry should load data }
rd_dir_names : PNameBuf;
rd_file_names : PNameBuf;
rd_file_sizes : PSizeBuf;
rd_dir_count : uint32;
rd_file_count : uint32;
rd_ready : uint32; { 1 = data ready for UI build }
rd_failed : boolean; { true = VFS returned nil }
refresh_poll : Plv_timer; { 50ms poll timer }
{ Worker VFS operations (mkdir, rename) }
wop_pending : uint32; { 0=none, 1=mkdir, 2=rename }
wop_path1 : pchar; { path arg 1 }
wop_path2 : pchar; { path arg 2 (rename new-name) }
wop_done : uint32; { 1 = op finished }
wop_result : TError; { result from op }
wop_kind : uint32; { copy of pending kind for UI callback }
{ File dispatch (open file via handler) }
dispatch_pending : uint32; { 1 = fb_entry should dispatch }
dispatch_path : pchar; { abs path to dispatch }
dispatch_done : uint32; { 1 = dispatch finished }
dispatch_result : uint32; { PID returned by dispatch, 0 = no handler }
end;
var
g_win_id : uint32;
g_state : PFileBrowserState;
{ Forward declarations }
procedure fb_entry(ctx: PProcessContext); forward;
procedure launch; forward;
procedure onClose(wid: uint32); forward;
procedure onResize(wid: uint32; new_w, new_h: sint32); forward;
procedure freeState(state: PFileBrowserState); forward;
procedure do_refresh_load(state: PFileBrowserState); forward;
procedure do_refresh_build(state: PFileBrowserState); forward;
procedure schedule_refresh(state: PFileBrowserState); forward;
procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl; forward;
function fb_error_name(err : TError) : pchar; forward;
procedure fb_collect_cb(key: pchar; data: void; ud: void); forward;
procedure do_refresh(state: PFileBrowserState); forward;
procedure navigate_to(state: PFileBrowserState; newDir: pchar); forward;
procedure push_history(state: PFileBrowserState; path: pchar); forward;
procedure update_nav_btns(state: PFileBrowserState); forward;
procedure update_status_bar(state: PFileBrowserState); forward;
procedure free_children_userdata(container: Plv_obj); forward;
procedure fb_dir_cb(e: Plv_event); cdecl; forward;
procedure fb_file_cb(e: Plv_event); cdecl; forward;
procedure fb_back_cb(e: Plv_event); cdecl; forward;
procedure fb_fwd_cb(e: Plv_event); cdecl; forward;
procedure fb_up_cb(e: Plv_event); cdecl; forward;
procedure fb_refresh_cb(e: Plv_event); cdecl; forward;
procedure build_breadcrumbs(state: PFileBrowserState); forward;
procedure fb_crumb_cb(e: Plv_event); cdecl; forward;
procedure fb_crumb_area_cb(e: Plv_event); cdecl; forward;
procedure fb_path_ta_cb(e: Plv_event); cdecl; forward;
{ Phase 6 — file operations }
procedure fb_mbox_close_cb(e: Plv_event); cdecl; forward;
procedure select_item(state: PFileBrowserState; row: Plv_obj;
path: pchar; isDir: boolean); forward;
procedure clear_selection(state: PFileBrowserState); forward;
procedure fb_delete_cb(e: Plv_event); cdecl; forward;
procedure fb_delete_confirm_cb(e: Plv_event); cdecl; forward;
procedure fb_delete_done(error: TError; userdata: pointer); forward;
procedure fb_delete_done_timer(tmr: Plv_timer); cdecl; forward;
procedure fb_newfolder_cb(e: Plv_event); cdecl; forward;
procedure fb_newfolder_ok_cb(e: Plv_event); cdecl; forward;
procedure fb_rename_cb(e: Plv_event); cdecl; forward;
procedure fb_rename_ok_cb(e: Plv_event); cdecl; forward;
procedure fb_newfile_cb(e: Plv_event); cdecl; forward;
procedure fb_newfile_ok_cb(e: Plv_event); cdecl; forward;
procedure fb_newfile_opened(error: TError; userdata: pointer); forward;
procedure fb_newfile_written(error: TError; userdata: pointer); forward;
procedure fb_newfile_done_timer(tmr: Plv_timer); cdecl; forward;
{ Phase 7 — context menu }
procedure fb_row_long_press_cb(e: Plv_event); cdecl; forward;
procedure show_context_menu(state: PFileBrowserState; mx, my: sint32); forward;
procedure destroy_context_menu(state: PFileBrowserState); forward;
procedure fb_ctx_item_cb(e: Plv_event); cdecl; forward;
procedure fb_ctx_dismiss_timer(tmr: Plv_timer); cdecl; forward;
{ Phase 8 — tabs }
procedure save_tab(state: PFileBrowserState; idx: uint32); forward;
procedure restore_tab(state: PFileBrowserState; idx: uint32); forward;
procedure switch_tab(state: PFileBrowserState; idx: uint32); forward;
procedure add_tab(state: PFileBrowserState; path: pchar); forward;
procedure close_tab(state: PFileBrowserState; idx: uint32); forward;
procedure rebuild_tab_bar(state: PFileBrowserState); forward;
procedure fb_tab_cb(e: Plv_event); cdecl; forward;
procedure fb_tab_close_cb(e: Plv_event); cdecl; forward;
procedure fb_tab_new_cb(e: Plv_event); cdecl; forward;
procedure free_tab_snapshot(snap: PTabSnapshot); forward;
{ Phase 9 — bookmarks }
procedure rebuild_bookmarks(state: PFileBrowserState); forward;
procedure add_bookmark_entry(state: PFileBrowserState; path: pchar); forward;
procedure remove_bookmark_entry(state: PFileBrowserState; idx: uint32); forward;
procedure fb_bookmark_click_cb(e: Plv_event); cdecl; forward;
procedure fb_bookmark_add_cb(e: Plv_event); cdecl; forward;
procedure fb_bookmark_ctx_cb(e: Plv_event); cdecl; forward;
{ Phase 10 — quick filter }
procedure fb_filter_changed_cb(e: Plv_event); cdecl; forward;
procedure fb_filter_clear_cb(e: Plv_event); cdecl; forward;
{ Phase 11 — type-ahead jump }
procedure fb_typeahead_key_cb(e: Plv_event); cdecl; forward;
procedure fb_typeahead_reset_timer(tmr: Plv_timer); cdecl; forward;
{ Phase 12 — file preview panel }
procedure fb_preview_toggle_cb(e: Plv_event); cdecl; forward;
procedure update_preview(state: PFileBrowserState); forward;
{ Phase 13 — compact view }
procedure fb_viewmode_toggle_cb(e: Plv_event); cdecl; forward;
{ Phase 14 — column header sorting }
procedure fb_header_name_cb(e: Plv_event); cdecl; forward;
procedure fb_header_size_cb(e: Plv_event); cdecl; forward;
procedure fb_header_type_cb(e: Plv_event); cdecl; forward;
{ Phase 15 — hidden files toggle }
procedure fb_hidden_toggle_cb(e: Plv_event); cdecl; forward;
{ Phase 17 — directory watch live refresh }
procedure fb_watch_cb(event: TVFSWatchEvent; path: pchar; userdata: pointer); forward;
procedure fb_watch_poll_timer(tmr: Plv_timer); cdecl; forward;
{ ============================================================
free_tab_snapshot — release a TTabSnapshot and its contents
============================================================ }
procedure free_tab_snapshot(snap: PTabSnapshot);
var
i: uint32;
begin
if snap = nil then exit;
if snap^.cur_path <> nil then kfree(void(snap^.cur_path));
if snap^.sel_path <> nil then kfree(void(snap^.sel_path));
if snap^.hist_len > 0 then
for i := 0 to snap^.hist_len - 1 do
if snap^.history[i] <> nil then begin
kfree(void(snap^.history[i]));
snap^.history[i] := nil;
end;
if snap^.filter_text <> nil then kfree(void(snap^.filter_text));
kfree(void(snap));
end;
{ ============================================================
freeState — release all heap-allocated fields and the record
============================================================ }
procedure freeState(state: PFileBrowserState);
var
i: uint32;
begin
if state = nil then exit;
if state^.cur_path <> nil then kfree(void(state^.cur_path));
if state^.sel_path <> nil then kfree(void(state^.sel_path));
{ Free history entries }
if state^.hist_len > 0 then
for i := 0 to state^.hist_len - 1 do
if state^.history[i] <> nil then begin
kfree(void(state^.history[i]));
state^.history[i] := nil;
end;
{ Free tab snapshots }
if state^.tab_count > 0 then
for i := 0 to state^.tab_count - 1 do
if state^.tabs[i] <> nil then begin
free_tab_snapshot(state^.tabs[i]);
state^.tabs[i] := nil;
end;
{ Free bookmarks }
if state^.bm_count > 0 then
for i := 0 to state^.bm_count - 1 do
if state^.bookmarks[i] <> nil then begin
kfree(void(state^.bookmarks[i]));
state^.bookmarks[i] := nil;
end;
if state^.filter_text <> nil then kfree(void(state^.filter_text));
if state^.ta_timer <> nil then begin
lv_timer_delete(state^.ta_timer);
state^.ta_timer := nil;
end;
{ Unwatch directory }
if state^.watch_id <> 0 then begin
driver.storage.vfs.UnwatchDirectory(state^.watch_id);
state^.watch_id := 0;
end;
if state^.watch_timer <> nil then begin
lv_timer_delete(state^.watch_timer);
state^.watch_timer := nil;
end;
if state^.refresh_poll <> nil then begin
lv_timer_delete(state^.refresh_poll);
state^.refresh_poll := nil;
end;
{ Free rd_* buffers }
if state^.rd_dir_names <> nil then begin
if state^.rd_dir_count > 0 then
for i := 0 to state^.rd_dir_count - 1 do
if state^.rd_dir_names^[i] <> nil then
kfree(void(state^.rd_dir_names^[i]));
kfree(void(state^.rd_dir_names));
end;
if state^.rd_file_names <> nil then begin
if state^.rd_file_count > 0 then
for i := 0 to state^.rd_file_count - 1 do
if state^.rd_file_names^[i] <> nil then
kfree(void(state^.rd_file_names^[i]));
kfree(void(state^.rd_file_names));
end;
if state^.rd_file_sizes <> nil then
kfree(void(state^.rd_file_sizes));
{ Free worker-op paths }
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
if state^.wop_path2 <> nil then kfree(void(state^.wop_path2));
{ Free dispatch path }
if state^.dispatch_path <> nil then kfree(void(state^.dispatch_path));
kfree(void(state));
end;
{ ============================================================
free_children_userdata — kfree stringCopy'd pchar in user_data
============================================================ }
procedure free_children_userdata(container: Plv_obj);
var
n : uint32;
i : uint32;
obj : Plv_obj;
ud : pchar;
begin
if container = nil then exit;
n := lv_obj_get_child_count(container);
if n = 0 then exit;
for i := 0 to n - 1 do begin
obj := lv_obj_get_child(container, sint32(i));
if obj = nil then continue;
ud := pchar(lv_obj_get_user_data(obj));
if ud <> nil then kfree(void(ud));
end;
end;
{ ============================================================
fb_entry — process entry point (keeps PS listing alive)
============================================================ }
procedure fb_entry(ctx: PProcessContext);
var
state : PFileBrowserState;
opKind : uint32;
err : TError;
dpath : pchar;
dpid : uint32;
begin
state := PFileBrowserState(ctx^.Local);
while (ctx^.State <> psFinished) and
(ctx^.PendingMsg <> smKill) and
(ctx^.PendingMsg <> smTerminate) do begin
{ Check if a refresh was requested (volatile read per Lesson #6) }
if puint32(@state^.refresh_pending)^ = 1 then begin
puint32(@state^.refresh_pending)^ := 0;
do_refresh_load(state);
puint32(@state^.rd_ready)^ := 1;
end;
{ Check for worker VFS operations (mkdir, rename) }
opKind := puint32(@state^.wop_pending)^;
if opKind <> 0 then begin
puint32(@state^.wop_pending)^ := 0;
state^.wop_kind := opKind;
err := eNone;
case opKind of
1: err := driver.storage.vfs.CreateDirectory(
state^.wop_path1, @err);
2: err := driver.storage.vfs.RenameFile(
state^.wop_path1, state^.wop_path2, @err);
end;
state^.wop_result := err;
puint32(@state^.wop_done)^ := 1;
end;
{ Check for file dispatch request }
if puint32(@state^.dispatch_pending)^ = 1 then begin
puint32(@state^.dispatch_pending)^ := 0;
dpath := state^.dispatch_path;
state^.dispatch_path := nil;
if dpath <> nil then begin
dpid := driver.storage.filedispatch.dispatch(
dpath, nil, nil, nil, nil);
state^.dispatch_result := dpid;
kfree(void(dpath));
end else
state^.dispatch_result := 0;
puint32(@state^.dispatch_done)^ := 1;
end;
proc.mgr.proc_yield;
end;
end;
{ ============================================================
onClose — called by the window manager when X is pressed
============================================================ }
procedure onClose(wid: uint32);
begin
debug.tracer.push_trace('filebrowser.onClose');
if g_state <> nil then begin
{ 1. Kill worker process so it stops touching state }
if g_state^.pid <> 0 then begin
proc.mgr.kill(g_state^.pid);
g_state^.pid := 0;
end;
{ 2. Delete LVGL timers early to prevent callbacks during teardown }
if g_state^.ta_timer <> nil then begin
lv_timer_delete(g_state^.ta_timer);
g_state^.ta_timer := nil;
end;
if g_state^.watch_timer <> nil then begin
lv_timer_delete(g_state^.watch_timer);
g_state^.watch_timer := nil;
end;
if g_state^.refresh_poll <> nil then begin
lv_timer_delete(g_state^.refresh_poll);
g_state^.refresh_poll := nil;
end;
{ 3. Clear selection — nil sel_row before deletion (Lesson #16) }
clear_selection(g_state);
{ 4. Destroy context menu (lives on lv_layer_top, not in frame) }
destroy_context_menu(g_state);
{ 5. Free user_data strings from LVGL children }
free_children_userdata(g_state^.breadcrumb);
free_children_userdata(g_state^.content_area);
if g_state^.sidebar <> nil then
free_children_userdata(g_state^.sidebar);
{ 6. Destroy window — deletes all LVGL widget objects }
driver.video.windows.destroyWindow(g_win_id);
g_win_id := 0;
{ 7. Now free remaining heap data and the state record }
freeState(g_state);
g_state := nil;
end else begin
driver.video.windows.destroyWindow(g_win_id);
g_win_id := 0;
end;
debug.tracer.pop_trace;
end;
{ ============================================================
onResize — called when the user resizes the window
============================================================ }
procedure onResize(wid: uint32; new_w, new_h: sint32);
begin
{ LVGL flex layout handles child resizing automatically. }
end;
{ ============================================================
History helpers
============================================================ }
procedure push_history(state: PFileBrowserState; path: pchar);
var
i: sint32;
begin
{ Trim forward entries }
if state^.hist_pos + 1 < state^.hist_len then
for i := sint32(state^.hist_pos) + 1 to sint32(state^.hist_len) - 1 do
if state^.history[i] <> nil then begin
kfree(void(state^.history[i]));
state^.history[i] := nil;
end;
state^.hist_len := state^.hist_pos + 1;
{ Drop oldest when full }
if state^.hist_len = HIST_MAX then begin
if state^.history[0] <> nil then kfree(void(state^.history[0]));
for i := 0 to HIST_MAX - 2 do
state^.history[i] := state^.history[i + 1];
state^.history[HIST_MAX - 1] := nil;
state^.hist_len := HIST_MAX - 1;
state^.hist_pos := HIST_MAX - 2;
end;
state^.history[state^.hist_len] := stringCopy(path);
state^.hist_len := state^.hist_len + 1;
state^.hist_pos := state^.hist_len - 1;
end;
procedure update_nav_btns(state: PFileBrowserState);
begin
if state^.back_btn <> nil then begin
if state^.hist_pos > 0 then begin
lv_obj_add_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.back_btn, LV_OPA_COVER, 0);
end else begin
lv_obj_remove_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.back_btn, 80, 0);
end;
end;
if state^.fwd_btn <> nil then begin
if state^.hist_pos + 1 < state^.hist_len then begin
lv_obj_add_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.fwd_btn, LV_OPA_COVER, 0);
end else begin
lv_obj_remove_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.fwd_btn, 80, 0);
end;
end;
end;
{ ============================================================
navigate_to — central navigation point
============================================================ }
procedure navigate_to(state: PFileBrowserState; newDir: pchar);
begin
{ Unwatch old directory }
if state^.watch_id <> 0 then begin
driver.storage.vfs.UnwatchDirectory(state^.watch_id);
state^.watch_id := 0;
end;
push_history(state, newDir);
if state^.cur_path <> nil then kfree(void(state^.cur_path));
state^.cur_path := stringCopy(newDir);
update_nav_btns(state);
build_breadcrumbs(state);
if state^.tab_bar <> nil then rebuild_tab_bar(state);
{ Watch new directory }
state^.watch_dirty := false;
state^.watch_id := driver.storage.vfs.WatchDirectory(newDir, @fb_watch_cb, state);
schedule_refresh(state);
end;
{ ============================================================
schedule_refresh — signal the fb_entry process to do VFS I/O
============================================================ }
procedure schedule_refresh(state: PFileBrowserState);
begin
{ Signal the worker process to load data (volatile write) }
puint32(@state^.refresh_pending)^ := 1;
end;
function fb_error_name(err : TError) : pchar;
begin
case err of
eNone: fb_error_name := 'eNone';
eUnknown: fb_error_name := 'eUnknown';
eNotSupported: fb_error_name := 'eNotSupported';
eOutOfMemory: fb_error_name := 'eOutOfMemory';
eInvalidArgument: fb_error_name := 'eInvalidArgument';
eFileInUse: fb_error_name := 'eFileInUse';
eFileDoesNotExist: fb_error_name := 'eFileDoesNotExist';
eInvalidFileName: fb_error_name := 'eInvalidFileName';
eInvalidFileExtension: fb_error_name := 'eInvalidFileExtension';
eFilenameTooLong: fb_error_name := 'eFilenameTooLong';
eDirectoryDoesNotExist: fb_error_name := 'eDirectoryDoesNotExist';
eDirectoryAlreadyExists: fb_error_name := 'eDirectoryAlreadyExists';
eDirectoryNotEmpty: fb_error_name := 'eDirectoryNotEmpty';
eDirectoryFull: fb_error_name := 'eDirectoryFull';
eNotADirectory: fb_error_name := 'eNotADirectory';
eWriteOnly: fb_error_name := 'eWriteOnly';
eReadOnly: fb_error_name := 'eReadOnly';
ePermissionDenied: fb_error_name := 'ePermissionDenied';
eInvalidPath: fb_error_name := 'eInvalidPath';
eTooManyOpenFiles: fb_error_name := 'eTooManyOpenFiles';
eInvalidHandle: fb_error_name := 'eInvalidHandle';
eFileNotLoaded: fb_error_name := 'eFileNotLoaded';
eAlreadyExists: fb_error_name := 'eAlreadyExists';
eDiskFull: fb_error_name := 'eDiskFull';
eIOError: fb_error_name := 'eIOError';
eIOTimeout: fb_error_name := 'eIOTimeout';
eIOCancelled: fb_error_name := 'eIOCancelled';
eDeviceNotReady: fb_error_name := 'eDeviceNotReady';
eDeviceRemoved: fb_error_name := 'eDeviceRemoved';
eDeviceNotFound: fb_error_name := 'eDeviceNotFound';
eQueueFull: fb_error_name := 'eQueueFull';
eNoFreeSlot: fb_error_name := 'eNoFreeSlot';
eCorruptFilesystem: fb_error_name := 'eCorruptFilesystem';
eBadSector: fb_error_name := 'eBadSector';
eAlreadyMounted: fb_error_name := 'eAlreadyMounted';
eNotMounted: fb_error_name := 'eNotMounted';
eUnsupportedFilesystem: fb_error_name := 'eUnsupportedFilesystem';
eInvalidPartitionTable: fb_error_name := 'eInvalidPartitionTable';
eVolumeNotFound: fb_error_name := 'eVolumeNotFound';
end;
end;
{ fb_refresh_poll_cb — 50ms LVGL poll timer; when rd_ready=1, build UI }
procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
ok_b : Plv_obj;
begin
state := PFileBrowserState(lv_timer_get_user_data(tmr));
if state = nil then exit;
if puint32(@state^.rd_ready)^ = 1 then begin
puint32(@state^.rd_ready)^ := 0;
do_refresh_build(state);
end;
{ Check for completed worker VFS operation }
if puint32(@state^.wop_done)^ = 1 then begin
puint32(@state^.wop_done)^ := 0;
if state^.wop_result <> eNone then begin
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'Error');
case state^.wop_kind of
1: lv_msgbox_add_text(mbox, 'Could not create folder.');
2: lv_msgbox_add_text(mbox, 'Could not rename item.');
3: lv_msgbox_add_text(mbox, 'Could not create file.');
4: lv_msgbox_add_text(mbox, 'Could not delete item.');
else
lv_msgbox_add_text(mbox, 'Operation failed.');
end;
lv_msgbox_add_text(mbox, fb_error_name(state^.wop_result));
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
{ Free op paths }
if state^.wop_path1 <> nil then begin
kfree(void(state^.wop_path1));
state^.wop_path1 := nil;
end;
if state^.wop_path2 <> nil then begin
kfree(void(state^.wop_path2));
state^.wop_path2 := nil;
end;
{ For rename/delete, clear selection }
if (state^.wop_kind = 2) or (state^.wop_kind = 4) then
clear_selection(state);
schedule_refresh(state);
end;
end;
{ ============================================================
fb_collect_cb — hashmap forEach: classify dirs/files
============================================================ }
procedure fb_collect_cb(key: pchar; data: void; ud: void);
var
ctx : PFBCollectCtx;
obj : PVFSObject;
begin
ctx := PFBCollectCtx(ud);
obj := PVFSObject(data);
if (ctx = nil) or (obj = nil) then exit;
{ Always hide '.' and '..' entries }
if stringEquals(key, '.') or stringEquals(key, '..') then exit;
case obj^.ObjectType of
otVDIRECTORY, otDRIVE, otDIRECTORY, otMOUNT, otSYMLINK:
if ctx^.dir_count < ENTRY_MAX then begin
ctx^.dir_names^[ctx^.dir_count] := key;
ctx^.dir_count := ctx^.dir_count + 1;
end;
otFILE, otVFILE:
if ctx^.file_count < ENTRY_MAX then begin
ctx^.file_names^[ctx^.file_count] := key;
ctx^.file_sizes^[ctx^.file_count] := obj^.FileSize;
ctx^.file_count := ctx^.file_count + 1;
end;
end;
end;
{ ============================================================
update_status_bar — show item count and current path
============================================================ }
procedure update_status_bar(state: PFileBrowserState);
var
s1, s2, s3, s4, s5 : pchar;
begin
if state^.status_label = nil then exit;
s1 := intToString(state^.item_count);
s2 := stringConcat(s1, ' items');
kfree(void(s1));
{ If something is selected, show its name }
if state^.sel_path <> nil then begin
s3 := stringConcat(s2, ' | Sel: ');
kfree(void(s2));
s4 := stringConcat(s3, state^.sel_path);
kfree(void(s3));
s5 := stringConcat(s4, ' | ');
kfree(void(s4));
end else begin
s5 := stringConcat(s2, ' | ');
kfree(void(s2));
end;
s3 := stringConcat(s5, state^.cur_path);
kfree(void(s5));
lv_label_set_text(state^.status_label, s3);
kfree(void(s3));
end;
{ ============================================================
do_refresh_load — VFS I/O + sort (runs in fb_entry process)
Stores results in state^.rd_* fields.
============================================================ }
procedure do_refresh_load(state: PFileBrowserState);
var
Map : PHashMap;
ctx : TFBCollectCtx;
dir_names : PNameBuf;
file_names : PNameBuf;
file_sizes : PSizeBuf;
dir_count : uint32;
file_count : uint32;
shown : uint32;
i : uint32;
extStr : pchar;
sep : pchar;
s1 : pchar;
begin
debug.tracer.push_trace('filebrowser.do_refresh_load');
{ Free any previous rd_* buffers }
if state^.rd_dir_names <> nil then begin
{ Free stringCopy'd entries }
if state^.rd_dir_count > 0 then
for i := 0 to state^.rd_dir_count - 1 do
if state^.rd_dir_names^[i] <> nil then
kfree(void(state^.rd_dir_names^[i]));
kfree(void(state^.rd_dir_names));
state^.rd_dir_names := nil;
end;
if state^.rd_file_names <> nil then begin
if state^.rd_file_count > 0 then
for i := 0 to state^.rd_file_count - 1 do
if state^.rd_file_names^[i] <> nil then
kfree(void(state^.rd_file_names^[i]));
kfree(void(state^.rd_file_names));
state^.rd_file_names := nil;
end;
if state^.rd_file_sizes <> nil then begin
kfree(void(state^.rd_file_sizes));
state^.rd_file_sizes := nil;
end;
state^.rd_dir_count := 0;
state^.rd_file_count := 0;
state^.rd_failed := false;
{ Heap-alloc name/size buffers (Lesson #11) }
dir_names := PNameBuf(kalloc(sizeof(TNameBuf)));
file_names := PNameBuf(kalloc(sizeof(TNameBuf)));
file_sizes := PSizeBuf(kalloc(sizeof(TSizeBuf)));
memset(uint32(dir_names), 0, sizeof(TNameBuf));
memset(uint32(file_names), 0, sizeof(TNameBuf));
memset(uint32(file_sizes), 0, sizeof(TSizeBuf));
{ Load directory from VFS — this may do disk I/O }
Map := driver.storage.vfs.GetDirectoryListingFrom(state^.cur_path, '/');
if Map = nil then begin
state^.rd_failed := true;
kfree(void(dir_names));
kfree(void(file_names));
kfree(void(file_sizes));
debug.tracer.pop_trace; exit;
end;
if Map^.Table = nil then begin
driver.storage.vfs.FreeDirectoryListing(Map);
state^.rd_failed := true;
kfree(void(dir_names));
kfree(void(file_names));
kfree(void(file_sizes));
debug.tracer.pop_trace; exit;
end;
{ Collect entries }
ctx.dir_names := dir_names;
ctx.file_names := file_names;
ctx.file_sizes := file_sizes;
ctx.dir_count := 0;
ctx.file_count := 0;
core.ds.hashmap.forEach(Map, @fb_collect_cb, void(@ctx));
dir_count := ctx.dir_count;
file_count := ctx.file_count;
{ Sort }
if dir_count > 0 then sortStringArray(@dir_names^[0], dir_count);
if file_count > 0 then begin
case state^.sort_col of
1: begin { Sort by size — bubble sort on sizes, move names along }
if file_count > 1 then
for i := 0 to file_count - 2 do
for shown := 0 to file_count - i - 2 do
if file_sizes^[shown] > file_sizes^[shown + 1] then begin
{ swap sizes }
dir_count := file_sizes^[shown];
file_sizes^[shown] := file_sizes^[shown + 1];
file_sizes^[shown + 1] := dir_count;
{ swap names }
s1 := file_names^[shown];
file_names^[shown] := file_names^[shown + 1];
file_names^[shown + 1] := s1;
end;
dir_count := ctx.dir_count; { restore dir_count }
end;
2: begin { Sort by extension — sort by extension string }
sortStringArrayWithData(@file_names^[0], @file_sizes^[0], file_count);
{ Re-sort by extension: bubble sort }
if file_count > 1 then
for i := 0 to file_count - 2 do
for shown := 0 to file_count - i - 2 do begin
extStr := getFileExtension(file_names^[shown]);
sep := getFileExtension(file_names^[shown + 1]);
if extStr = nil then extStr := '';
if sep = nil then sep := '';
if stringCompare(extStr, sep) > 0 then begin
{ swap names }
s1 := file_names^[shown];
file_names^[shown] := file_names^[shown + 1];
file_names^[shown + 1] := s1;
{ swap sizes }
dir_count := file_sizes^[shown];
file_sizes^[shown] := file_sizes^[shown + 1];
file_sizes^[shown + 1] := dir_count;
end;
end;
dir_count := ctx.dir_count;
end;
else { sort_col=0: default name sort }
sortStringArrayWithData(@file_names^[0], @file_sizes^[0], file_count);
end;
end;
{ Reverse if descending (dirs and files separately) }
if not state^.sort_asc then begin
if dir_count > 1 then
for i := 0 to (dir_count div 2) - 1 do begin
s1 := dir_names^[i];
dir_names^[i] := dir_names^[dir_count - 1 - i];
dir_names^[dir_count - 1 - i] := s1;
end;
if file_count > 1 then
for i := 0 to (file_count div 2) - 1 do begin
s1 := file_names^[i];
file_names^[i] := file_names^[file_count - 1 - i];
file_names^[file_count - 1 - i] := s1;
shown := file_sizes^[i];
file_sizes^[i] := file_sizes^[file_count - 1 - i];
file_sizes^[file_count - 1 - i] := shown;
end;
end;
{ Free the VFS snapshot — we only need name copies now }
{ But the names in dir_names/file_names point INTO the snapshot!
We must stringCopy them before freeing. }
if dir_count > 0 then
for i := 0 to dir_count - 1 do
dir_names^[i] := stringCopy(dir_names^[i]);
if file_count > 0 then
for i := 0 to file_count - 1 do
file_names^[i] := stringCopy(file_names^[i]);
driver.storage.vfs.FreeDirectoryListing(Map);
{ Store sorted results for UI build }
state^.rd_dir_names := dir_names;
state^.rd_file_names := file_names;
state^.rd_file_sizes := file_sizes;
state^.rd_dir_count := dir_count;
state^.rd_file_count := file_count;
debug.tracer.pop_trace;
end;
{ ============================================================
do_refresh_build — create LVGL widgets from rd_* data
(runs in LVGL poll timer — NO disk I/O!)
============================================================ }
procedure do_refresh_build(state: PFileBrowserState);
var
dir_names : PNameBuf;
file_names : PNameBuf;
file_sizes : PSizeBuf;
dir_count : uint32;
file_count : uint32;
shown : uint32;
has_filter : boolean;
i : uint32;
row : Plv_obj;
icon_lbl : Plv_obj;
name_lbl : Plv_obj;
size_lbl : Plv_obj;
type_lbl : Plv_obj;
szStr : pchar;
extStr : pchar;
s1, s2 : pchar;
ico_color : uint32;
begin
debug.tracer.push_trace('filebrowser.do_refresh_build');
if state^.content_area = nil then begin
debug.tracer.pop_trace; exit;
end;
{ Release old UI entries — clear_selection BEFORE lv_obj_clean,
otherwise we write styles to a freed LVGL object (sel_row) }
clear_selection(state);
free_children_userdata(state^.content_area);
lv_obj_clean(state^.content_area);
{ Handle failure case }
if state^.rd_failed then begin
name_lbl := lv_label_create(state^.content_area);
lv_label_set_text(name_lbl, 'Could not read directory');
lv_obj_set_style_text_color(name_lbl, lv_color_make(220, 60, 60), 0);
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
state^.item_count := 0;
update_status_bar(state);
debug.tracer.pop_trace; exit;
end;
dir_names := state^.rd_dir_names;
file_names := state^.rd_file_names;
file_sizes := state^.rd_file_sizes;
dir_count := state^.rd_dir_count;
file_count := state^.rd_file_count;
if (dir_names = nil) and (file_names = nil) then begin
state^.item_count := 0;
update_status_bar(state);
debug.tracer.pop_trace; exit;
end;
has_filter := (state^.filter_text <> nil) and (stringSize(state^.filter_text) > 0);
shown := 0;
{ Set layout based on view mode }
if state^.view_mode = vmCompact then begin
lv_obj_set_flex_flow(state^.content_area, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_pad_row(state^.content_area, 4, 0);
lv_obj_set_style_pad_column(state^.content_area, 4, 0);
end else begin
lv_obj_set_flex_flow(state^.content_area, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(state^.content_area, 2, 0);
end;
{ --- Detail header row --- }
if state^.view_mode = vmDetail then begin
row := lv_obj_create(state^.content_area);
lv_obj_remove_style_all(row);
lv_obj_set_size(row, lv_pct(100), 26);
lv_obj_set_style_bg_color(row, lv_color_make(28, 31, 42), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_pad_left(row, 8, 0);
lv_obj_set_style_pad_right(row, 8, 0);
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_border_color(row, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_border_width(row, 1, 0);
{ Icon column spacer }
icon_lbl := lv_label_create(row);
lv_label_set_text(icon_lbl, '');
lv_obj_set_size(icon_lbl, 36, LV_SIZE_CONTENT);
{ Name header }
name_lbl := lv_label_create(row);
if (state^.sort_col = 0) and state^.sort_asc then
lv_label_set_text(name_lbl, 'Name ^')
else if (state^.sort_col = 0) and (not state^.sort_asc) then
lv_label_set_text(name_lbl, 'Name v')
else
lv_label_set_text(name_lbl, 'Name');
lv_obj_set_style_text_color(name_lbl, lv_color_make(170, 178, 200), 0);
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_flex_grow(name_lbl, 1);
lv_obj_add_flag(name_lbl, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(name_lbl, @fb_header_name_cb, LV_EVENT_CLICKED, state);
{ Size header }
size_lbl := lv_label_create(row);
if (state^.sort_col = 1) and state^.sort_asc then
lv_label_set_text(size_lbl, 'Size ^')
else if (state^.sort_col = 1) and (not state^.sort_asc) then
lv_label_set_text(size_lbl, 'Size v')
else
lv_label_set_text(size_lbl, 'Size');
lv_obj_set_style_text_color(size_lbl, lv_color_make(170, 178, 200), 0);
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
lv_obj_add_flag(size_lbl, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(size_lbl, @fb_header_size_cb, LV_EVENT_CLICKED, state);
{ Type header }
type_lbl := lv_label_create(row);
if (state^.sort_col = 2) and state^.sort_asc then
lv_label_set_text(type_lbl, 'Type ^')
else if (state^.sort_col = 2) and (not state^.sort_asc) then
lv_label_set_text(type_lbl, 'Type v')
else
lv_label_set_text(type_lbl, 'Type');
lv_obj_set_style_text_color(type_lbl, lv_color_make(170, 178, 200), 0);
lv_obj_set_style_text_font(type_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_size(type_lbl, 60, LV_SIZE_CONTENT);
lv_obj_add_flag(type_lbl, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(type_lbl, @fb_header_type_cb, LV_EVENT_CLICKED, state);
end;
{ --- Build directory rows --- }
if (dir_names <> nil) and (dir_count > 0) then
for i := 0 to dir_count - 1 do begin
if dir_names^[i] = nil then continue;
if (not state^.show_hidden) and (dir_names^[i][0] = '.') then continue;
if has_filter then
if not stringContainsCI(dir_names^[i], state^.filter_text) then continue;
row := lv_obj_create(state^.content_area);
lv_obj_remove_style_all(row);
if state^.view_mode = vmCompact then begin
lv_obj_set_size(row, 120, 34);
end else begin
lv_obj_set_size(row, lv_pct(100), 34);
end;
lv_obj_set_style_bg_color(row, lv_color_make(38, 42, 56), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(row, lv_color_make(50, 55, 72), LV_STATE_PRESSED);
lv_obj_set_style_radius(row, 4, 0);
lv_obj_set_style_pad_left(row, 8, 0);
lv_obj_set_style_pad_right(row, 8, 0);
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
{ Folder icon — 32px PUA glyph }
icon_lbl := lv_label_create(row);
lv_label_set_text(icon_lbl, ICO_FOLDER);
lv_obj_set_style_text_color(icon_lbl, lv_color_make(255, 210, 60), 0);
lv_obj_set_style_text_font(icon_lbl, @asuro_icons_32, 0);
lv_obj_set_size(icon_lbl, 36, 32);
{ Folder name }
name_lbl := lv_label_create(row);
lv_label_set_text(name_lbl, dir_names^[i]);
lv_obj_set_style_text_color(name_lbl, lv_color_make(255, 210, 60), 0);
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_flex_grow(name_lbl, 1);
if state^.view_mode = vmCompact then
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_CLIP);
if state^.view_mode = vmDetail then begin
{ Size column — show DIR }
size_lbl := lv_label_create(row);
lv_label_set_text(size_lbl, 'DIR');
lv_obj_set_style_text_color(size_lbl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
end;
{ Build full path in user_data for navigation }
if stringEquals(state^.cur_path, '/') then begin
s1 := stringConcat('/', dir_names^[i]);
end else begin
s1 := stringConcat(state^.cur_path, '/');
s2 := stringConcat(s1, dir_names^[i]);
kfree(void(s1));
s1 := s2;
end;
lv_obj_set_user_data(row, s1);
lv_obj_add_flag(row, LV_OBJ_FLAG_USER_1); { mark as directory row }
lv_obj_add_event_cb(row, @fb_dir_cb, LV_EVENT_CLICKED, g_state);
lv_obj_add_event_cb(row, @fb_row_long_press_cb, LV_EVENT_LONG_PRESSED, g_state);
shown := shown + 1;
end;
{ --- Build file rows --- }
if (file_names <> nil) and (file_count > 0) then
for i := 0 to file_count - 1 do begin
if file_names^[i] = nil then continue;
if (not state^.show_hidden) and (file_names^[i][0] = '.') then continue;
if has_filter then
if not stringContainsCI(file_names^[i], state^.filter_text) then continue;
row := lv_obj_create(state^.content_area);
lv_obj_remove_style_all(row);
if state^.view_mode = vmCompact then
lv_obj_set_size(row, 120, 34)
else
lv_obj_set_size(row, lv_pct(100), 34);
lv_obj_set_style_bg_color(row, lv_color_make(32, 35, 48), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(row, lv_color_make(50, 55, 72), LV_STATE_PRESSED);
lv_obj_set_style_radius(row, 4, 0);
lv_obj_set_style_pad_left(row, 8, 0);
lv_obj_set_style_pad_right(row, 8, 0);
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
{ File icon — 32px PUA glyph, color by type }
ico_color := getFileTypeColor(file_names^[i]);
icon_lbl := lv_label_create(row);
lv_label_set_text(icon_lbl, getFileTypeIcon(file_names^[i]));
lv_obj_set_style_text_color(icon_lbl, lv_color_make(
(ico_color shr 16) and $FF,
(ico_color shr 8) and $FF,
ico_color and $FF), 0);
lv_obj_set_style_text_font(icon_lbl, @asuro_icons_32, 0);
lv_obj_set_size(icon_lbl, 36, 32);
{ File name }
name_lbl := lv_label_create(row);
lv_label_set_text(name_lbl, file_names^[i]);
lv_obj_set_style_text_color(name_lbl, lv_color_make(210, 218, 240), 0);
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_flex_grow(name_lbl, 1);
if state^.view_mode = vmCompact then
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_CLIP);
if state^.view_mode = vmDetail then begin
{ Size label }
szStr := fmtFileSize(file_sizes^[i]);
size_lbl := lv_label_create(row);
lv_label_set_text(size_lbl, szStr);
kfree(void(szStr));
lv_obj_set_style_text_color(size_lbl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
{ Type/extension label }
extStr := getFileExtension(file_names^[i]);
type_lbl := lv_label_create(row);
if extStr <> nil then
lv_label_set_text(type_lbl, extStr)
else
lv_label_set_text(type_lbl, '');
lv_obj_set_style_text_color(type_lbl, lv_color_make(120, 130, 150), 0);
lv_obj_set_style_text_font(type_lbl, @lv_font_montserrat_14, 0);
lv_obj_set_size(type_lbl, 60, LV_SIZE_CONTENT);
end;
{ Full path in user_data }
if stringEquals(state^.cur_path, '/') then begin
s1 := stringConcat('/', file_names^[i]);
end else begin
s1 := stringConcat(state^.cur_path, '/');
s2 := stringConcat(s1, file_names^[i]);
kfree(void(s1));
s1 := s2;
end;
lv_obj_set_user_data(row, s1);
lv_obj_add_event_cb(row, @fb_file_cb, LV_EVENT_CLICKED, g_state);
lv_obj_add_event_cb(row, @fb_row_long_press_cb, LV_EVENT_LONG_PRESSED, g_state);
shown := shown + 1;
end;
state^.item_count := shown;
update_status_bar(state);
debug.tracer.pop_trace;
end;
{ ============================================================
do_refresh — convenience: trigger load+build for callers that
run in the fb_entry process context (not used from LVGL)
============================================================ }
procedure do_refresh(state: PFileBrowserState);
begin
do_refresh_load(state);
do_refresh_build(state);
end;
{ ============================================================
Row click callbacks
============================================================ }
procedure fb_dir_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
row : Plv_obj;
path : pchar;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
row := lv_event_get_target_obj(e);
path := pchar(lv_obj_get_user_data(row));
if path = nil then exit;
{ If already selected, navigate into it (double-click effect) }
if (state^.sel_path <> nil) and stringEquals(state^.sel_path, path) then begin
navigate_to(state, path);
exit;
end;
{ Otherwise select }
select_item(state, row, path, true);
end;
procedure fb_file_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
row : Plv_obj;
path : pchar;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
row := lv_event_get_target_obj(e);
path := pchar(lv_obj_get_user_data(row));
if path = nil then exit;
{ If clicking the already-selected file, treat as "open" via dispatch.
Schedule dispatch in the worker process (Lesson #10: no sync disk I/O
from LVGL callbacks). }
if (state^.sel_path <> nil) and stringEquals(path, state^.sel_path)
and (not state^.sel_is_dir) then begin
if puint32(@state^.dispatch_pending)^ = 0 then begin
state^.dispatch_path := stringCopy(path);
puint32(@state^.dispatch_done)^ := 0;
puint32(@state^.dispatch_pending)^ := 1;
end;
exit;
end;
select_item(state, row, path, false);
end;
{ ============================================================
Navigation button callbacks
============================================================ }
procedure fb_back_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.hist_pos = 0) then exit;
state^.hist_pos := state^.hist_pos - 1;
if state^.cur_path <> nil then kfree(void(state^.cur_path));
state^.cur_path := stringCopy(state^.history[state^.hist_pos]);
update_nav_btns(state);
build_breadcrumbs(state);
schedule_refresh(state);
end;
procedure fb_fwd_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.hist_pos + 1 >= state^.hist_len) then exit;
state^.hist_pos := state^.hist_pos + 1;
if state^.cur_path <> nil then kfree(void(state^.cur_path));
state^.cur_path := stringCopy(state^.history[state^.hist_pos]);
update_nav_btns(state);
build_breadcrumbs(state);
schedule_refresh(state);
end;
procedure fb_up_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
cur : pchar;
len : uint32;
i : sint32;
parent : pchar;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
cur := state^.cur_path;
if cur = nil then exit;
len := stringSize(cur);
if (len <= 1) then exit; { already at root }
{ Find last '/' before end }
i := sint32(len) - 1;
{ Skip trailing slash }
if cur[i] = '/' then dec(i);
while (i > 0) and (cur[i] <> '/') do dec(i);
if i = 0 then
parent := stringCopy('/')
else begin
parent := pchar(kalloc(uint32(i) + 1));
memcpy(uint32(cur), uint32(parent), uint32(i));
parent[i] := char(0);
end;
navigate_to(state, parent);
kfree(void(parent));
end;
procedure fb_refresh_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state <> nil then
schedule_refresh(state);
end;
{ ============================================================
build_breadcrumbs — parse cur_path into clickable segments
============================================================ }
procedure build_breadcrumbs(state: PFileBrowserState);
var
bar : Plv_obj;
btn : Plv_obj;
lbl : Plv_obj;
sep : Plv_obj;
path : pchar;
len : uint32;
segStart : uint32;
segEnd : uint32;
prefix : pchar;
segBuf : pchar;
n : uint32;
begin
bar := state^.breadcrumb;
if bar = nil then exit;
{ Free old crumb user_data (path prefixes) }
n := lv_obj_get_child_count(bar);
if n > 0 then begin
free_children_userdata(bar);
end;
lv_obj_clean(bar);
{ Hide textarea if visible }
if state^.path_ta <> nil then begin
lv_obj_add_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
end;
lv_obj_remove_flag(bar, LV_OBJ_FLAG_HIDDEN);
state^.path_editing := false;
path := state^.cur_path;
if path = nil then exit;
len := stringSize(path);
{ Root "/" button }
btn := lv_button_create(bar);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(btn, lv_color_make(70, 78, 100), LV_STATE_PRESSED);
lv_obj_set_style_radius(btn, 3, 0);
lv_obj_set_style_pad_left(btn, 6, 0);
lv_obj_set_style_pad_right(btn, 6, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lbl := lv_label_create(btn);
lv_label_set_text(lbl, '/');
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_center(lbl);
lv_obj_set_user_data(btn, stringCopy('/'));
lv_obj_add_event_cb(btn, @fb_crumb_cb, LV_EVENT_CLICKED, g_state);
{ Parse remaining segments }
if len <= 1 then exit; { root only }
segStart := 1; { skip leading '/' }
while segStart < len do begin
{ Find end of segment }
segEnd := segStart;
while (segEnd < len) and (path[segEnd] <> '/') do
segEnd := segEnd + 1;
if segEnd = segStart then begin
segStart := segEnd + 1;
continue;
end;
{ Separator label }
sep := lv_label_create(bar);
lv_label_set_text(sep, '>');
lv_obj_set_style_text_color(sep, lv_color_make(100, 108, 130), 0);
lv_obj_set_style_text_font(sep, @lv_font_montserrat_14, 0);
lv_obj_set_user_data(sep, nil);
{ Segment button }
btn := lv_button_create(bar);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(btn, lv_color_make(70, 78, 100), LV_STATE_PRESSED);
lv_obj_set_style_radius(btn, 3, 0);
lv_obj_set_style_pad_left(btn, 6, 0);
lv_obj_set_style_pad_right(btn, 6, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
{ Segment name label }
segBuf := pchar(kalloc(segEnd - segStart + 1));
memcpy(uint32(@path[segStart]), uint32(segBuf), segEnd - segStart);
segBuf[segEnd - segStart] := char(0);
lbl := lv_label_create(btn);
lv_label_set_text(lbl, segBuf);
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_center(lbl);
kfree(void(segBuf));
{ Build prefix path for this segment }
prefix := pchar(kalloc(segEnd + 1));
memcpy(uint32(path), uint32(prefix), segEnd);
prefix[segEnd] := char(0);
lv_obj_set_user_data(btn, prefix);
lv_obj_add_event_cb(btn, @fb_crumb_cb, LV_EVENT_CLICKED, g_state);
segStart := segEnd + 1;
end;
end;
{ ============================================================
fb_crumb_cb — breadcrumb segment clicked → navigate
============================================================ }
procedure fb_crumb_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
btn : Plv_obj;
path : pchar;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
btn := lv_event_get_target_obj(e);
path := pchar(lv_obj_get_user_data(btn));
if path = nil then exit;
navigate_to(state, path);
end;
{ ============================================================
fb_crumb_area_cb — click the breadcrumb area background
to switch to path text editing mode
============================================================ }
procedure fb_crumb_area_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
target : Plv_obj;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
{ Only activate on direct click on the breadcrumb bar itself }
target := lv_event_get_target_obj(e);
if target <> state^.breadcrumb then exit;
if state^.path_editing then exit;
state^.path_editing := true;
{ Hide breadcrumb buttons, show textarea }
lv_obj_add_flag(state^.breadcrumb, LV_OBJ_FLAG_HIDDEN);
if state^.path_ta <> nil then begin
lv_textarea_set_text(state^.path_ta, state^.cur_path);
lv_obj_remove_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
end;
end;
{ ============================================================
fb_path_ta_cb — Enter navigates, Escape reverts to breadcrumbs
============================================================ }
procedure fb_path_ta_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
key : uint32;
txt : pchar;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_KEY then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
key := lv_event_get_key(e);
if key = LV_KEY_ENTER then begin
txt := lv_textarea_get_text(state^.path_ta);
if txt <> nil then
navigate_to(state, txt);
end
else if key = LV_KEY_ESC then begin
{ Revert to breadcrumbs }
build_breadcrumbs(state);
end;
end;
{ ============================================================
Selection helpers
============================================================ }
procedure clear_selection(state: PFileBrowserState);
begin
if state^.sel_row <> nil then begin
{ Reset row background to its default colour }
if state^.sel_is_dir then
lv_obj_set_style_bg_color(state^.sel_row,
lv_color_make(38, 42, 56), 0)
else
lv_obj_set_style_bg_color(state^.sel_row,
lv_color_make(32, 35, 48), 0);
state^.sel_row := nil;
end;
if state^.sel_path <> nil then begin
kfree(void(state^.sel_path));
state^.sel_path := nil;
end;
end;
procedure select_item(state: PFileBrowserState; row: Plv_obj;
path: pchar; isDir: boolean);
begin
clear_selection(state);
state^.sel_row := row;
state^.sel_path := stringCopy(path);
state^.sel_is_dir := isDir;
{ Highlight with selection colour }
lv_obj_set_style_bg_color(row, lv_color_make(55, 75, 120), 0);
{ Update preview panel if visible }
if state^.preview_visible then
update_preview(state);
end;
{ ============================================================
fb_mbox_close_cb — generic: close the parent msgbox
============================================================ }
procedure fb_mbox_close_cb(e: Plv_event); cdecl;
var
mbox : Plv_obj;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
mbox := Plv_obj(lv_event_get_user_data(e));
if mbox <> nil then lv_msgbox_close_async(mbox);
end;
{ ============================================================
Phase 6a — Delete
============================================================ }
{ Callback from toolbar Delete button }
procedure fb_delete_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
yes_b : Plv_obj;
no_b : Plv_obj;
s1 : pchar;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.sel_path = nil) then exit;
{ Confirmation msgbox }
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'Delete');
s1 := stringConcat('Delete "', state^.sel_path);
lv_msgbox_add_text(mbox, s1);
kfree(void(s1));
lv_msgbox_add_text(mbox, '"?');
yes_b := lv_msgbox_add_footer_button(mbox, 'Delete');
no_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
lv_obj_add_event_cb(yes_b, @fb_delete_confirm_cb, LV_EVENT_CLICKED, state);
lv_obj_add_event_cb(no_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
{ User confirmed deletion }
procedure fb_delete_confirm_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.sel_path = nil) then exit;
{ Close the confirmation dialog }
mbox := lv_obj_get_parent(lv_event_get_target_obj(e));
if mbox <> nil then begin
mbox := lv_obj_get_parent(mbox); { footer → msgbox }
lv_msgbox_close_async(mbox);
end;
{ Start async delete }
state^.op_err := eNone;
if state^.sel_is_dir then
driver.storage.vfs.DeleteDirectoryAsync(
state^.sel_path, @state^.op_err,
@fb_delete_done, state)
else
driver.storage.vfs.DeleteFileAsync(
state^.sel_path, @state^.op_err,
@fb_delete_done, state);
end;
{ Async completion callback — runs outside the LVGL thread, so only set flags. }
procedure fb_delete_done(error: TError; userdata: pointer);
var
state: PFileBrowserState;
begin
state := PFileBrowserState(userdata);
if state = nil then exit;
state^.wop_kind := 4;
state^.wop_result := error;
puint32(@state^.wop_done)^ := 1;
end;
{ Deferred UI update after delete }
procedure fb_delete_done_timer(tmr: Plv_timer); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
ok_b : Plv_obj;
begin
state := PFileBrowserState(lv_timer_get_user_data(tmr));
lv_timer_delete(tmr);
if state = nil then exit;
if state^.op_err <> eNone then begin
{ Show error }
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'Error');
lv_msgbox_add_text(mbox, 'Could not delete item.');
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
clear_selection(state);
schedule_refresh(state);
end;
{ ============================================================
Phase 6b — Create Folder
============================================================ }
procedure fb_newfolder_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
cont : Plv_obj;
ta : Plv_obj;
ok_b : Plv_obj;
cancel_b : Plv_obj;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'New Folder');
cont := lv_msgbox_get_content(mbox);
ta := lv_textarea_create(cont);
lv_textarea_set_one_line(ta, true);
lv_textarea_set_text(ta, '');
lv_obj_set_width(ta, lv_pct(100));
lv_obj_set_user_data(mbox, ta); { stash textarea ptr in msgbox }
ok_b := lv_msgbox_add_footer_button(mbox, 'Create');
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
lv_obj_add_event_cb(ok_b, @fb_newfolder_ok_cb, LV_EVENT_CLICKED, state);
lv_obj_set_user_data(ok_b, mbox); { stash msgbox in OK button }
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
procedure fb_newfolder_ok_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
ta : Plv_obj;
name : pchar;
fp : pchar;
sep : pchar;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
if mbox = nil then exit;
ta := Plv_obj(lv_obj_get_user_data(mbox));
if ta = nil then exit;
name := lv_textarea_get_text(ta);
if (name = nil) or (stringSize(name) = 0) then exit;
{ Build full path }
if stringEquals(state^.cur_path, '/') then
fp := stringConcat('/', name)
else begin
sep := stringConcat(state^.cur_path, '/');
fp := stringConcat(sep, name);
kfree(void(sep));
end;
{ Close dialog }
lv_msgbox_close_async(mbox);
{ Signal worker process to create the directory (no disk I/O in LVGL) }
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
state^.wop_path1 := fp;
state^.wop_path2 := nil;
state^.wop_result := eNone;
puint32(@state^.wop_pending)^ := 1; { 1 = mkdir }
end;
{ ============================================================
Phase 6c — Rename
============================================================ }
procedure fb_rename_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
cont : Plv_obj;
ta : Plv_obj;
ok_b : Plv_obj;
cancel_b : Plv_obj;
basename : pchar;
len : uint32;
i : sint32;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.sel_path = nil) then exit;
{ Extract basename from sel_path }
len := stringSize(state^.sel_path);
i := sint32(len) - 1;
while (i > 0) and (state^.sel_path[i] <> '/') do dec(i);
basename := @state^.sel_path[i + 1];
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'Rename');
cont := lv_msgbox_get_content(mbox);
ta := lv_textarea_create(cont);
lv_textarea_set_one_line(ta, true);
lv_textarea_set_text(ta, basename);
lv_obj_set_width(ta, lv_pct(100));
lv_obj_set_user_data(mbox, ta);
ok_b := lv_msgbox_add_footer_button(mbox, 'Rename');
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
lv_obj_add_event_cb(ok_b, @fb_rename_ok_cb, LV_EVENT_CLICKED, state);
lv_obj_set_user_data(ok_b, mbox);
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
procedure fb_rename_ok_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
ta : Plv_obj;
name : pchar;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if (state = nil) or (state^.sel_path = nil) then exit;
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
if mbox = nil then exit;
ta := Plv_obj(lv_obj_get_user_data(mbox));
if ta = nil then exit;
name := lv_textarea_get_text(ta);
if (name = nil) or (stringSize(name) = 0) then exit;
lv_msgbox_close_async(mbox);
{ Signal worker process to rename (no disk I/O in LVGL) }
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
if state^.wop_path2 <> nil then kfree(void(state^.wop_path2));
state^.wop_path1 := stringCopy(state^.sel_path);
state^.wop_path2 := stringCopy(name);
state^.wop_result := eNone;
puint32(@state^.wop_pending)^ := 2; { 2 = rename }
end;
{ ============================================================
Phase 6d — Create File
============================================================ }
type
PNewFileCtx = ^TNewFileCtx;
TNewFileCtx = record
state : PFileBrowserState;
handle : TFileHandle;
err : TError;
end;
procedure fb_newfile_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
cont : Plv_obj;
ta : Plv_obj;
ok_b : Plv_obj;
cancel_b : Plv_obj;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'New File');
cont := lv_msgbox_get_content(mbox);
ta := lv_textarea_create(cont);
lv_textarea_set_one_line(ta, true);
lv_textarea_set_text(ta, '');
lv_obj_set_width(ta, lv_pct(100));
lv_obj_set_user_data(mbox, ta);
ok_b := lv_msgbox_add_footer_button(mbox, 'Create');
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
lv_obj_add_event_cb(ok_b, @fb_newfile_ok_cb, LV_EVENT_CLICKED, state);
lv_obj_set_user_data(ok_b, mbox);
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
procedure fb_newfile_ok_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
mbox : Plv_obj;
ta : Plv_obj;
name : pchar;
fp : pchar;
sep : pchar;
nfctx : PNewFileCtx;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
if mbox = nil then exit;
ta := Plv_obj(lv_obj_get_user_data(mbox));
if ta = nil then exit;
name := lv_textarea_get_text(ta);
if (name = nil) or (stringSize(name) = 0) then exit;
{ Build full path }
if stringEquals(state^.cur_path, '/') then
fp := stringConcat('/', name)
else begin
sep := stringConcat(state^.cur_path, '/');
fp := stringConcat(sep, name);
kfree(void(sep));
end;
lv_msgbox_close_async(mbox);
{ Use async open to create the file }
nfctx := PNewFileCtx(kalloc(sizeof(TNewFileCtx)));
nfctx^.state := state;
nfctx^.err := eNone;
nfctx^.handle := 0;
driver.storage.vfs.OpenFileAsync(fp, omCreate, nfctx^.handle,
@nfctx^.err, @fb_newfile_opened, nfctx);
kfree(void(fp));
end;
{ Async step 1: file descriptor opened — now write 0 bytes to create dir entry }
procedure fb_newfile_opened(error: TError; userdata: pointer);
var
nfctx : PNewFileCtx;
begin
nfctx := PNewFileCtx(userdata);
if nfctx = nil then exit;
nfctx^.err := error;
if (error = eNone) and (nfctx^.handle <> 0) then begin
{ WriteFileAsync needs a non-nil buffer; Length=0 so it is never read }
driver.storage.vfs.WriteFileAsync(nfctx^.handle, 0,
puint8(@nfctx^.err), 0, nil, @fb_newfile_written, nfctx);
end else begin
if nfctx^.handle <> 0 then
driver.storage.vfs.CloseFile(nfctx^.handle);
if nfctx^.state <> nil then begin
nfctx^.state^.wop_kind := 3;
nfctx^.state^.wop_result := nfctx^.err;
puint32(@nfctx^.state^.wop_done)^ := 1;
end;
kfree(void(nfctx));
end;
end;
{ Async step 2: write complete — close handle and defer UI work via poll flags }
procedure fb_newfile_written(error: TError; userdata: pointer);
var
nfctx : PNewFileCtx;
begin
nfctx := PNewFileCtx(userdata);
if nfctx = nil then exit;
if error <> eNone then nfctx^.err := error;
if nfctx^.handle <> 0 then
driver.storage.vfs.CloseFile(nfctx^.handle);
if nfctx^.state <> nil then begin
nfctx^.state^.wop_kind := 3;
nfctx^.state^.wop_result := nfctx^.err;
puint32(@nfctx^.state^.wop_done)^ := 1;
end;
kfree(void(nfctx));
end;
procedure fb_newfile_done_timer(tmr: Plv_timer); cdecl;
var
nfctx : PNewFileCtx;
mbox : Plv_obj;
ok_b : Plv_obj;
begin
nfctx := PNewFileCtx(lv_timer_get_user_data(tmr));
lv_timer_delete(tmr);
if nfctx = nil then exit;
if nfctx^.err <> eNone then begin
mbox := lv_msgbox_create(lv_layer_top);
lv_msgbox_add_title(mbox, 'Error');
lv_msgbox_add_text(mbox, 'Could not create file.');
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
end;
if nfctx^.state <> nil then
schedule_refresh(nfctx^.state);
kfree(void(nfctx));
end;
{ ============================================================
Phase 8 — Tabs
============================================================ }
{ save_tab — snapshot current live state into tabs[idx] }
procedure save_tab(state: PFileBrowserState; idx: uint32);
var
snap : PTabSnapshot;
i : uint32;
begin
if idx >= MAX_TABS then exit;
{ Free old snapshot if any }
if state^.tabs[idx] <> nil then
free_tab_snapshot(state^.tabs[idx]);
snap := PTabSnapshot(kalloc(sizeof(TTabSnapshot)));
memset(uint32(snap), 0, sizeof(TTabSnapshot));
if state^.cur_path <> nil then
snap^.cur_path := stringCopy(state^.cur_path)
else
snap^.cur_path := stringCopy('/');
snap^.hist_len := state^.hist_len;
snap^.hist_pos := state^.hist_pos;
snap^.item_count := state^.item_count;
if snap^.hist_len > 0 then
for i := 0 to snap^.hist_len - 1 do
if state^.history[i] <> nil then
snap^.history[i] := stringCopy(state^.history[i])
else
snap^.history[i] := nil;
if state^.sel_path <> nil then
snap^.sel_path := stringCopy(state^.sel_path)
else
snap^.sel_path := nil;
snap^.sel_is_dir := state^.sel_is_dir;
if state^.filter_text <> nil then
snap^.filter_text := stringCopy(state^.filter_text)
else
snap^.filter_text := nil;
state^.tabs[idx] := snap;
end;
{ restore_tab — load tabs[idx] into the live state (clears old live state) }
procedure restore_tab(state: PFileBrowserState; idx: uint32);
var
snap : PTabSnapshot;
i : uint32;
begin
if idx >= MAX_TABS then exit;
snap := state^.tabs[idx];
if snap = nil then exit;
{ Free current live history }
if state^.hist_len > 0 then
for i := 0 to state^.hist_len - 1 do
if state^.history[i] <> nil then begin
kfree(void(state^.history[i]));
state^.history[i] := nil;
end;
if state^.cur_path <> nil then kfree(void(state^.cur_path));
if state^.sel_path <> nil then begin
kfree(void(state^.sel_path));
state^.sel_path := nil;
end;
state^.sel_row := nil;
{ Restore from snapshot }
if snap^.cur_path <> nil then
state^.cur_path := stringCopy(snap^.cur_path)
else
state^.cur_path := stringCopy('/');
state^.hist_len := snap^.hist_len;
state^.hist_pos := snap^.hist_pos;
state^.item_count := snap^.item_count;
if snap^.hist_len > 0 then
for i := 0 to snap^.hist_len - 1 do
if snap^.history[i] <> nil then
state^.history[i] := stringCopy(snap^.history[i])
else
state^.history[i] := nil;
if snap^.sel_path <> nil then
state^.sel_path := stringCopy(snap^.sel_path)
else
state^.sel_path := nil;
state^.sel_is_dir := snap^.sel_is_dir;
{ Restore filter text }
if state^.filter_text <> nil then begin
kfree(void(state^.filter_text));
state^.filter_text := nil;
end;
if snap^.filter_text <> nil then
state^.filter_text := stringCopy(snap^.filter_text);
{ Update filter textarea if present }
if state^.filter_ta <> nil then begin
if state^.filter_text <> nil then
lv_textarea_set_text(state^.filter_ta, state^.filter_text)
else
lv_textarea_set_text(state^.filter_ta, '');
end;
end;
{ switch_tab — save current, restore target, refresh UI }
procedure switch_tab(state: PFileBrowserState; idx: uint32);
begin
if idx >= state^.tab_count then exit;
if idx = state^.active_tab then exit;
{ Save current tab state }
save_tab(state, state^.active_tab);
{ Restore target tab }
restore_tab(state, idx);
state^.active_tab := idx;
update_nav_btns(state);
build_breadcrumbs(state);
rebuild_tab_bar(state);
schedule_refresh(state);
end;
{ add_tab — create a new tab with given path }
procedure add_tab(state: PFileBrowserState; path: pchar);
var
snap : PTabSnapshot;
begin
if state^.tab_count >= MAX_TABS then exit;
{ Save current tab first }
save_tab(state, state^.active_tab);
{ Create snapshot for new tab }
snap := PTabSnapshot(kalloc(sizeof(TTabSnapshot)));
memset(uint32(snap), 0, sizeof(TTabSnapshot));
snap^.cur_path := stringCopy(path);
snap^.hist_len := 0;
snap^.hist_pos := 0;
state^.tabs[state^.tab_count] := snap;
state^.active_tab := state^.tab_count;
state^.tab_count := state^.tab_count + 1;
{ Restore it into live state }
restore_tab(state, state^.active_tab);
{ Navigate (pushes history) }
navigate_to(state, path);
rebuild_tab_bar(state);
end;
{ close_tab — close tab at idx, switch to neighbour }
procedure close_tab(state: PFileBrowserState; idx: uint32);
var
i : uint32;
newIdx: uint32;
begin
if state^.tab_count <= 1 then exit; { must keep >=1 tab }
if idx >= state^.tab_count then exit;
{ If closing the active tab, save first so we don't lose data }
if idx = state^.active_tab then begin
{ Don't save — we're discarding it }
end else begin
save_tab(state, state^.active_tab);
end;
{ Free the snapshot }
if state^.tabs[idx] <> nil then begin
free_tab_snapshot(state^.tabs[idx]);
state^.tabs[idx] := nil;
end;
{ Shift tabs down }
if idx < state^.tab_count - 1 then
for i := idx to state^.tab_count - 2 do begin
state^.tabs[i] := state^.tabs[i + 1];
state^.tab_btns[i] := state^.tab_btns[i + 1];
end;
state^.tabs[state^.tab_count - 1] := nil;
state^.tab_btns[state^.tab_count - 1] := nil;
state^.tab_count := state^.tab_count - 1;
{ Determine new active index }
if idx = state^.active_tab then begin
if idx >= state^.tab_count then
newIdx := state^.tab_count - 1
else
newIdx := idx;
state^.active_tab := newIdx;
restore_tab(state, newIdx);
update_nav_btns(state);
build_breadcrumbs(state);
schedule_refresh(state);
end else begin
{ Active tab index may have shifted }
if state^.active_tab > idx then
state^.active_tab := state^.active_tab - 1;
restore_tab(state, state^.active_tab);
end;
rebuild_tab_bar(state);
end;
{ rebuild_tab_bar — recreate tab buttons to reflect current state }
procedure rebuild_tab_bar(state: PFileBrowserState);
var
bar : Plv_obj;
btn : Plv_obj;
lbl : Plv_obj;
close_b : Plv_obj;
plus_b : Plv_obj;
i : uint32;
snap : PTabSnapshot;
name : pchar;
len : uint32;
j : sint32;
begin
bar := state^.tab_bar;
if bar = nil then exit;
lv_obj_clean(bar);
if state^.tab_count > 0 then
for i := 0 to state^.tab_count - 1 do begin
{ For active tab, use live state; for others, snapshot }
if i = state^.active_tab then
name := state^.cur_path
else begin
snap := state^.tabs[i];
if (snap <> nil) and (snap^.cur_path <> nil) then
name := snap^.cur_path
else
name := '/';
end;
{ Extract last segment for display }
len := stringSize(name);
if len > 1 then begin
j := sint32(len) - 1;
if name[j] = '/' then dec(j);
while (j > 0) and (name[j] <> '/') do dec(j);
name := @name[j + 1];
end;
btn := lv_button_create(bar);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
lv_obj_set_style_pad_left(btn, 8, 0);
lv_obj_set_style_pad_right(btn, 4, 0);
lv_obj_set_style_radius(btn, 4, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(btn, 4, 0);
if i = state^.active_tab then begin
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
end else begin
lv_obj_set_style_bg_color(btn, lv_color_make(35, 38, 50), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
end;
lbl := lv_label_create(btn);
lv_label_set_text(lbl, name);
lv_obj_set_style_text_color(lbl, lv_color_make(190, 198, 220), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_set_user_data(btn, pointer(i));
lv_obj_add_event_cb(btn, @fb_tab_cb, LV_EVENT_CLICKED, state);
state^.tab_btns[i] := btn;
{ Close button (x) on each tab }
if state^.tab_count > 1 then begin
close_b := lv_label_create(btn);
lv_label_set_text(close_b, 'x');
lv_obj_set_style_text_color(close_b, lv_color_make(140, 140, 160), 0);
lv_obj_set_style_text_font(close_b, @lv_font_montserrat_14, 0);
lv_obj_add_flag(close_b, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_user_data(close_b, pointer(i));
lv_obj_add_event_cb(close_b, @fb_tab_close_cb, LV_EVENT_CLICKED, state);
end;
end;
{ "+" button to add new tab }
if state^.tab_count < MAX_TABS then begin
plus_b := lv_button_create(bar);
lv_obj_remove_style_all(plus_b);
lv_obj_set_size(plus_b, 24, 22);
lv_obj_set_style_bg_color(plus_b, lv_color_make(35, 38, 50), 0);
lv_obj_set_style_bg_opa(plus_b, LV_OPA_COVER, 0);
lv_obj_set_style_radius(plus_b, 4, 0);
lv_obj_add_flag(plus_b, LV_OBJ_FLAG_CLICKABLE);
lbl := lv_label_create(plus_b);
lv_label_set_text(lbl, '+');
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_center(lbl);
lv_obj_add_event_cb(plus_b, @fb_tab_new_cb, LV_EVENT_CLICKED, state);
end;
end;
{ Tab click — switch to this tab }
procedure fb_tab_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
btn : Plv_obj;
idx : uint32;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
btn := lv_event_get_target_obj(e);
idx := uint32(lv_obj_get_user_data(btn));
switch_tab(state, idx);
end;
{ Tab close (x) click }
procedure fb_tab_close_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
lbl : Plv_obj;
idx : uint32;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
lbl := lv_event_get_target_obj(e);
idx := uint32(lv_obj_get_user_data(lbl));
close_tab(state, idx);
end;
{ "+" button — new tab at current path }
procedure fb_tab_new_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
add_tab(state, state^.cur_path);
end;
{ ============================================================
Phase 10 — Quick filter
============================================================ }
{ fb_filter_changed_cb — re-render rows with current filter text }
procedure fb_filter_changed_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
txt : pchar;
begin
if lv_event_get_code(e) <> LV_EVENT_VALUE_CHANGED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
{ Update filter_text from textarea }
if state^.filter_text <> nil then begin
kfree(void(state^.filter_text));
state^.filter_text := nil;
end;
txt := lv_textarea_get_text(state^.filter_ta);
if (txt <> nil) and (stringSize(txt) > 0) then
state^.filter_text := stringCopy(txt);
schedule_refresh(state);
end;
{ fb_filter_clear_cb — clear filter text and refresh }
procedure fb_filter_clear_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.filter_text <> nil then begin
kfree(void(state^.filter_text));
state^.filter_text := nil;
end;
if state^.filter_ta <> nil then
lv_textarea_set_text(state^.filter_ta, '');
schedule_refresh(state);
end;
{ ============================================================
Phase 11 — Type-ahead jump-to
============================================================ }
{ fb_typeahead_reset_timer — clear type-ahead buffer after 1s inactivity }
procedure fb_typeahead_reset_timer(tmr: Plv_timer); cdecl;
var
state: PFileBrowserState;
begin
state := PFileBrowserState(lv_timer_get_user_data(tmr));
lv_timer_delete(tmr);
if state <> nil then begin
state^.ta_len := 0;
state^.ta_buf[0]:= #0;
state^.ta_timer := nil;
end;
end;
{ fb_typeahead_key_cb — handle key presses on content area for jump-to }
procedure fb_typeahead_key_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
key : uint32;
ch : char;
n, i : uint32;
child : Plv_obj;
firstLbl: Plv_obj;
lblText : pchar;
isDir : boolean;
ud : pchar;
baseName: pchar;
bLen : uint32;
j : sint32;
begin
if lv_event_get_code(e) <> LV_EVENT_KEY then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
key := lv_event_get_key(e);
{ Only handle printable ASCII }
if (key < 32) or (key > 126) then exit;
ch := char(key);
{ Append to buf (guard overflow) }
if state^.ta_len >= 31 then exit;
state^.ta_buf[state^.ta_len] := ch;
state^.ta_len := state^.ta_len + 1;
state^.ta_buf[state^.ta_len] := #0;
{ Reset/restart the 1-second timer }
if state^.ta_timer <> nil then
lv_timer_delete(state^.ta_timer);
state^.ta_timer := lv_timer_create(@fb_typeahead_reset_timer, 1000, state);
{ Search children of content_area for first match }
n := lv_obj_get_child_count(state^.content_area);
if n = 0 then exit;
for i := 0 to n - 1 do begin
child := lv_obj_get_child(state^.content_area, sint32(i));
if child = nil then continue;
ud := pchar(lv_obj_get_user_data(child));
if ud = nil then continue;
{ Extract basename from full path in user_data }
bLen := stringSize(ud);
if bLen = 0 then continue;
j := sint32(bLen) - 1;
if ud[j] = '/' then dec(j);
while (j > 0) and (ud[j] <> '/') do dec(j);
if ud[j] = '/' then inc(j);
baseName := @ud[j];
if stringStartsWithCI(baseName, @state^.ta_buf[0]) then begin
lv_obj_scroll_to_view(child, LV_ANIM_ON);
{ Dir rows have first label text starting with '[' }
isDir := false;
if lv_obj_get_child_count(child) > 0 then begin
firstLbl := lv_obj_get_child(child, 0);
if firstLbl <> nil then begin
lblText := lv_label_get_text(firstLbl);
if (lblText <> nil) and (lblText[0] = '[') then
isDir := true;
end;
end;
select_item(state, child, ud, isDir);
exit;
end;
end;
end;
{ ============================================================
Phase 13 — Compact view toggle
============================================================ }
procedure fb_viewmode_toggle_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.view_mode = vmDetail then
state^.view_mode := vmCompact
else
state^.view_mode := vmDetail;
schedule_refresh(state);
end;
{ ============================================================
Phase 14 — Column header sorting
============================================================ }
procedure fb_header_name_cb(e: Plv_event); cdecl;
var state: PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.sort_col = 0 then
state^.sort_asc := not state^.sort_asc
else begin
state^.sort_col := 0;
state^.sort_asc := true;
end;
schedule_refresh(state);
end;
procedure fb_header_size_cb(e: Plv_event); cdecl;
var state: PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.sort_col = 1 then
state^.sort_asc := not state^.sort_asc
else begin
state^.sort_col := 1;
state^.sort_asc := true;
end;
schedule_refresh(state);
end;
procedure fb_header_type_cb(e: Plv_event); cdecl;
var state: PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
if state^.sort_col = 2 then
state^.sort_asc := not state^.sort_asc
else begin
state^.sort_col := 2;
state^.sort_asc := true;
end;
schedule_refresh(state);
end;
{ ============================================================
Phase 15 — Hidden files toggle
============================================================ }
procedure fb_hidden_toggle_cb(e: Plv_event); cdecl;
var state: PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
state^.show_hidden := not state^.show_hidden;
schedule_refresh(state);
end;
{ ============================================================
Phase 17 — WatchDirectory live refresh
============================================================ }
{ fb_watch_cb — called from VFS context when directory changes.
MUST NOT call LVGL or disk I/O; just sets a flag. }
procedure fb_watch_cb(event: TVFSWatchEvent; path: pchar; userdata: pointer);
var state: PFileBrowserState;
begin
state := PFileBrowserState(userdata);
if state <> nil then
state^.watch_dirty := true;
end;
{ fb_watch_poll_timer — 500ms LVGL timer; checks dirty flag and refreshes }
procedure fb_watch_poll_timer(tmr: Plv_timer); cdecl;
var state: PFileBrowserState;
begin
state := PFileBrowserState(lv_timer_get_user_data(tmr));
if state = nil then exit;
if state^.watch_dirty then begin
state^.watch_dirty := false;
schedule_refresh(state);
end;
end;
{ ============================================================
Phase 12 — File preview panel
============================================================ }
{ update_preview — refresh the preview panel content }
procedure update_preview(state: PFileBrowserState);
var
panel : Plv_obj;
lbl : Plv_obj;
sep : Plv_obj;
baseName : pchar;
szStr : pchar;
extStr : pchar;
len : uint32;
j : sint32;
begin
panel := state^.preview_panel;
if panel = nil then exit;
lv_obj_clean(panel);
if not state^.preview_visible then exit;
if (state^.sel_path = nil) or (stringSize(state^.sel_path) = 0) then begin
lbl := lv_label_create(panel);
lv_label_set_text(lbl, 'No selection');
lv_obj_set_style_text_color(lbl, lv_color_make(100, 110, 130), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
exit;
end;
{ Header }
lbl := lv_label_create(panel);
lv_label_set_text(lbl, 'Preview');
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
sep := lv_obj_create(panel);
lv_obj_remove_style_all(sep);
lv_obj_set_size(sep, lv_pct(100), 1);
lv_obj_set_style_bg_color(sep, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_bg_opa(sep, LV_OPA_COVER, 0);
{ Extract basename }
len := stringSize(state^.sel_path);
j := sint32(len) - 1;
if (j > 0) and (state^.sel_path[j] = '/') then dec(j);
while (j > 0) and (state^.sel_path[j] <> '/') do dec(j);
if state^.sel_path[j] = '/' then inc(j);
baseName := @state^.sel_path[j];
{ Name }
lbl := lv_label_create(panel);
lv_label_set_text(lbl, baseName);
lv_obj_set_style_text_color(lbl, lv_color_make(220, 225, 240), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl, lv_pct(100));
{ Type }
lbl := lv_label_create(panel);
if state^.sel_is_dir then
lv_label_set_text(lbl, 'Type: Directory')
else begin
extStr := getFileExtension(baseName);
if extStr <> nil then begin
szStr := stringConcat('Type: ', extStr);
lv_label_set_text(lbl, szStr);
kfree(void(szStr));
end else
lv_label_set_text(lbl, 'Type: File');
end;
lv_obj_set_style_text_color(lbl, lv_color_make(160, 168, 190), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
{ Full path }
lbl := lv_label_create(panel);
lv_label_set_text(lbl, state^.sel_path);
lv_obj_set_style_text_color(lbl, lv_color_make(120, 128, 150), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lbl, lv_pct(100));
{ For directories: show type indicator (no disk I/O per Lesson #10) }
if state^.sel_is_dir then begin
lbl := lv_label_create(panel);
lv_label_set_text(lbl, SYM_DIRECTORY + ' Folder');
lv_obj_set_style_text_color(lbl, lv_color_make(160, 168, 190), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
end;
end;
{ fb_preview_toggle_cb — toggle preview panel visibility }
procedure fb_preview_toggle_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
state^.preview_visible := not state^.preview_visible;
if state^.preview_panel <> nil then begin
if state^.preview_visible then begin
lv_obj_remove_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
update_preview(state);
end else
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
end;
end;
{ ============================================================
Phase 9 — Bookmarks sidebar
============================================================ }
{ add_bookmark_entry — add a path to the bookmarks list }
procedure add_bookmark_entry(state: PFileBrowserState; path: pchar);
var
i : uint32;
begin
if state^.bm_count >= MAX_BOOKMARKS then exit;
{ Check for duplicates }
if state^.bm_count > 0 then
for i := 0 to state^.bm_count - 1 do
if stringEquals(state^.bookmarks[i], path) then exit;
state^.bookmarks[state^.bm_count] := stringCopy(path);
state^.bm_count := state^.bm_count + 1;
rebuild_bookmarks(state);
end;
{ remove_bookmark_entry — remove bookmark at idx }
procedure remove_bookmark_entry(state: PFileBrowserState; idx: uint32);
var
i : uint32;
begin
if idx >= state^.bm_count then exit;
if state^.bookmarks[idx] <> nil then
kfree(void(state^.bookmarks[idx]));
{ Shift remainder down }
if idx < state^.bm_count - 1 then
for i := idx to state^.bm_count - 2 do
state^.bookmarks[i] := state^.bookmarks[i + 1];
state^.bookmarks[state^.bm_count - 1] := nil;
state^.bm_count := state^.bm_count - 1;
rebuild_bookmarks(state);
end;
{ fb_bookmark_click_cb — navigate to bookmarked path }
procedure fb_bookmark_click_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
btn : Plv_obj;
path : pchar;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
btn := lv_event_get_target_obj(e);
path := pchar(lv_obj_get_user_data(btn));
if path <> nil then
navigate_to(state, path);
end;
{ fb_bookmark_ctx_cb — right-click (long press) to remove bookmark }
procedure fb_bookmark_ctx_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
btn : Plv_obj;
path : pchar;
i : uint32;
begin
if lv_event_get_code(e) <> LV_EVENT_LONG_PRESSED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
btn := lv_event_get_target_obj(e);
path := pchar(lv_obj_get_user_data(btn));
if path = nil then exit;
{ Find matching bookmark index }
if state^.bm_count > 0 then
for i := 0 to state^.bm_count - 1 do
if stringEquals(state^.bookmarks[i], path) then begin
remove_bookmark_entry(state, i);
exit;
end;
end;
{ fb_bookmark_add_cb — "+" button adds current directory }
procedure fb_bookmark_add_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
begin
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
add_bookmark_entry(state, state^.cur_path);
end;
{ rebuild_bookmarks — recreate sidebar bookmark list }
procedure rebuild_bookmarks(state: PFileBrowserState);
var
bar : Plv_obj;
btn : Plv_obj;
lbl : Plv_obj;
plus_b : Plv_obj;
sep : Plv_obj;
i : uint32;
name : pchar;
len : uint32;
j : sint32;
begin
bar := state^.sidebar;
if bar = nil then exit;
{ Free user_data (stringCopy'd paths) from old buttons }
free_children_userdata(bar);
lv_obj_clean(bar);
{ Header label }
lbl := lv_label_create(bar);
lv_label_set_text(lbl, 'Bookmarks');
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_set_style_pad_bottom(lbl, 4, 0);
{ Separator line }
sep := lv_obj_create(bar);
lv_obj_remove_style_all(sep);
lv_obj_set_size(sep, lv_pct(100), 1);
lv_obj_set_style_bg_color(sep, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_bg_opa(sep, LV_OPA_COVER, 0);
if state^.bm_count > 0 then
for i := 0 to state^.bm_count - 1 do begin
name := state^.bookmarks[i];
if name = nil then continue;
btn := lv_obj_create(bar);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, lv_pct(100), 24);
lv_obj_set_style_bg_color(btn, lv_color_make(35, 38, 50), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_80, 0);
lv_obj_set_style_radius(btn, 4, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), uint32(LV_STATE_PRESSED));
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, uint32(LV_STATE_PRESSED));
lv_obj_set_style_pad_left(btn, 6, 0);
{ Extract last path segment for display }
len := stringSize(name);
if len <= 1 then
name := '/'
else begin
j := sint32(len) - 1;
if name[j] = '/' then dec(j);
while (j > 0) and (name[j] <> '/') do dec(j);
name := @state^.bookmarks[i][j + 1];
end;
lbl := lv_label_create(btn);
lv_label_set_text(lbl, name);
lv_obj_set_style_text_color(lbl, lv_color_make(190, 198, 220), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_user_data(btn, void(stringCopy(state^.bookmarks[i])));
lv_obj_add_event_cb(btn, @fb_bookmark_click_cb, LV_EVENT_CLICKED, state);
lv_obj_add_event_cb(btn, @fb_bookmark_ctx_cb, LV_EVENT_LONG_PRESSED, state);
end;
{ "+" Add Bookmark button }
plus_b := lv_obj_create(bar);
lv_obj_remove_style_all(plus_b);
lv_obj_set_size(plus_b, lv_pct(100), 24);
lv_obj_set_style_bg_color(plus_b, lv_color_make(30, 33, 44), 0);
lv_obj_set_style_bg_opa(plus_b, LV_OPA_80, 0);
lv_obj_set_style_radius(plus_b, 4, 0);
lv_obj_add_flag(plus_b, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_color(plus_b, lv_color_make(50, 55, 72), uint32(LV_STATE_PRESSED));
lv_obj_set_style_bg_opa(plus_b, LV_OPA_COVER, uint32(LV_STATE_PRESSED));
lv_obj_set_style_pad_left(plus_b, 6, 0);
lbl := lv_label_create(plus_b);
lv_label_set_text(lbl, '+ Add');
lv_obj_set_style_text_color(lbl, lv_color_make(100, 110, 140), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_add_event_cb(plus_b, @fb_bookmark_add_cb, LV_EVENT_CLICKED, state);
end;
{ ============================================================
Phase 7 — Context menu
============================================================ }
const
CTX_ACT_OPEN = 1;
CTX_ACT_RENAME = 2;
CTX_ACT_DELETE = 3;
CTX_ACT_NEWFOLD = 4;
CTX_ACT_NEWFILE = 5;
CTX_ACT_REFRESH = 6;
procedure destroy_context_menu(state: PFileBrowserState);
begin
if state^.ctx_menu <> nil then begin
lv_obj_delete(state^.ctx_menu);
state^.ctx_menu := nil;
end;
end;
{ Helper: add one menu row }
function ctx_add_item(parent: Plv_obj; text: pchar;
action: uint32; state: PFileBrowserState): Plv_obj;
var
btn : Plv_obj;
lbl : Plv_obj;
begin
btn := lv_button_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, lv_pct(100), 26);
lv_obj_set_style_bg_color(btn, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_color(btn, lv_color_make(60, 68, 90), LV_STATE_PRESSED);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_STATE_PRESSED);
lv_obj_set_style_pad_left(btn, 10, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lbl := lv_label_create(btn);
lv_label_set_text(lbl, text);
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
lv_obj_set_user_data(btn, pointer(action));
lv_obj_add_event_cb(btn, @fb_ctx_item_cb, LV_EVENT_CLICKED, state);
ctx_add_item := btn;
end;
procedure show_context_menu(state: PFileBrowserState; mx, my: sint32);
var
menu : Plv_obj;
begin
destroy_context_menu(state);
menu := lv_obj_create(lv_layer_top);
lv_obj_remove_style_all(menu);
lv_obj_set_pos(menu, mx, my);
lv_obj_set_size(menu, 140, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(menu, lv_color_make(38, 42, 56), 0);
lv_obj_set_style_bg_opa(menu, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(menu, lv_color_make(70, 78, 100), 0);
lv_obj_set_style_border_width(menu, 1, 0);
lv_obj_set_style_radius(menu, 6, 0);
lv_obj_set_style_pad_all(menu, 4, 0);
lv_obj_set_style_pad_row(menu, 1, 0);
lv_obj_set_style_layout(menu, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(menu, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_shadow_width(menu, 8, 0);
lv_obj_set_style_shadow_opa(menu, 120, 0);
lv_obj_set_style_shadow_color(menu, lv_color_make(0, 0, 0), 0);
{ Contextual items when an item is selected }
if state^.sel_path <> nil then begin
if state^.sel_is_dir then
ctx_add_item(menu, 'Open', CTX_ACT_OPEN, state);
ctx_add_item(menu, 'Rename', CTX_ACT_RENAME, state);
ctx_add_item(menu, 'Delete', CTX_ACT_DELETE, state);
end;
{ Always-visible items }
ctx_add_item(menu, 'New Folder', CTX_ACT_NEWFOLD, state);
ctx_add_item(menu, 'New File', CTX_ACT_NEWFILE, state);
ctx_add_item(menu, 'Refresh', CTX_ACT_REFRESH, state);
state^.ctx_menu := menu;
end;
procedure fb_ctx_item_cb(e: Plv_event); cdecl;
var
code : lv_event_code_t;
state : PFileBrowserState;
btn : Plv_obj;
action : uint32;
begin
code := lv_event_get_code(e);
if code <> LV_EVENT_CLICKED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
btn := lv_event_get_target_obj(e);
action := uint32(lv_obj_get_user_data(btn));
{ Close context menu first }
destroy_context_menu(state);
case action of
CTX_ACT_OPEN: begin
if (state^.sel_path <> nil) and state^.sel_is_dir then
navigate_to(state, state^.sel_path);
end;
CTX_ACT_RENAME: begin
if state^.sel_path <> nil then begin
{ Synthesize a fake LVGL event to reuse fb_rename_cb
— simpler to just inline the rename dialog creation }
fb_rename_cb(e);
end;
end;
CTX_ACT_DELETE: begin
if state^.sel_path <> nil then
fb_delete_cb(e);
end;
CTX_ACT_NEWFOLD: fb_newfolder_cb(e);
CTX_ACT_NEWFILE: fb_newfile_cb(e);
CTX_ACT_REFRESH: schedule_refresh(state);
end;
end;
{ LVGL long-press callback: select row and show context menu }
procedure fb_row_long_press_cb(e: Plv_event); cdecl;
var
state : PFileBrowserState;
row : Plv_obj;
path : pchar;
isDir : boolean;
coords : lv_area_t;
begin
if lv_event_get_code(e) <> LV_EVENT_LONG_PRESSED then exit;
state := PFileBrowserState(lv_event_get_user_data(e));
if state = nil then exit;
row := lv_event_get_target_obj(e);
if row = nil then exit;
path := pchar(lv_obj_get_user_data(row));
if path <> nil then begin
isDir := lv_obj_has_flag(row, LV_OBJ_FLAG_USER_1);
select_item(state, row, path, isDir);
end;
{ Position context menu at the row's screen coordinates }
lv_obj_get_coords(row, @coords);
show_context_menu(state, coords.x2, coords.y1);
end;
{ Timer to dismiss context menu when clicking elsewhere }
procedure fb_ctx_dismiss_timer(tmr: Plv_timer); cdecl;
var
state: PFileBrowserState;
begin
state := PFileBrowserState(lv_timer_get_user_data(tmr));
lv_timer_delete(tmr);
if state <> nil then
destroy_context_menu(state);
end;
{ ============================================================
makeToolBtn — compact toolbar button helper
============================================================ }
function makeToolBtn(parent: Plv_obj; text: pchar; w: sint32;
ud: pointer; cb: lv_event_cb_t): Plv_obj;
var
btn, lbl_obj : Plv_obj;
begin
btn := lv_button_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, w, 28);
lv_obj_set_style_bg_color(btn, lv_color_make(45, 50, 68), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 4, 0);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lbl_obj := lv_label_create(btn);
lv_label_set_text(lbl_obj, text);
lv_obj_set_style_text_color(lbl_obj, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(lbl_obj, @lv_font_montserrat_14, 0);
if cb <> nil then
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, ud);
makeToolBtn := btn;
end;
{ ============================================================
launch — create the window and populate the initial UI
============================================================ }
procedure launch;
var
scr_w, scr_h : sint32;
wx, wy : sint32;
content : Plv_obj;
toolbar : Plv_obj;
statusbar : Plv_obj;
main_area : Plv_obj;
body_row : Plv_obj;
sidebar : Plv_obj;
sl : Plv_obj;
state : PFileBrowserState;
ctx : PProcessContext;
begin
debug.tracer.push_trace('filebrowser.launch');
{ Single instance guard }
if driver.video.windows.isWindowOpen(g_win_id) then begin
debug.tracer.pop_trace; exit;
end;
scr_w := sint32(driver.video.frontBufferWidth);
scr_h := sint32(driver.video.frontBufferHeight);
wx := (scr_w - WIN_W) div 2;
wy := (scr_h - WIN_H) div 2 - 30;
g_win_id := driver.video.windows.createWindow('Files', wx, wy, WIN_W, WIN_H,
@onClose, nil);
if g_win_id = 0 then begin
debug.tracer.pop_trace; exit;
end;
content := driver.video.windows.getWindowContent(g_win_id);
if content = nil then begin
driver.video.windows.destroyWindow(g_win_id);
g_win_id := 0;
debug.tracer.pop_trace; exit;
end;
{ Allocate and zero-initialise state }
state := PFileBrowserState(kalloc(sizeof(TFileBrowserState)));
memset(uint32(state), 0, sizeof(TFileBrowserState));
state^.win_id := g_win_id;
state^.cur_path := stringCopy('/');
state^.view_mode := vmDetail;
state^.sort_col := 0; { name }
state^.sort_asc := true;
state^.show_hidden := false;
g_state := state;
{ ---- Content area: flex column ---- }
lv_obj_set_style_layout(content, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(content, 0, 0);
lv_obj_set_style_pad_row(content, 0, 0);
lv_obj_remove_flag(content, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(content, lv_color_make(28, 31, 40), 0);
lv_obj_set_style_bg_opa(content, LV_OPA_COVER, 0);
{ ---- Toolbar ---- }
toolbar := lv_obj_create(content);
lv_obj_remove_style_all(toolbar);
lv_obj_set_size(toolbar, lv_pct(100), TOOLBAR_H);
lv_obj_set_style_bg_color(toolbar, lv_color_make(28, 31, 40), 0);
lv_obj_set_style_bg_opa(toolbar, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(toolbar, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(toolbar, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_border_width(toolbar, 1, 0);
lv_obj_set_style_pad_all(toolbar, 4, 0);
lv_obj_set_style_pad_column(toolbar, 3, 0);
lv_obj_set_style_layout(toolbar, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(toolbar, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(toolbar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(toolbar, LV_OBJ_FLAG_SCROLLABLE);
state^.toolbar := toolbar;
{ Navigation buttons }
state^.back_btn := makeToolBtn(toolbar, SYM_LEFT, 28, state, @fb_back_cb);
state^.fwd_btn := makeToolBtn(toolbar, SYM_RIGHT, 28, state, @fb_fwd_cb);
state^.up_btn := makeToolBtn(toolbar, SYM_UP, 28, state, @fb_up_cb);
{ ---- Breadcrumb bar (fills remaining toolbar space) ---- }
state^.breadcrumb := lv_obj_create(toolbar);
lv_obj_remove_style_all(state^.breadcrumb);
lv_obj_set_height(state^.breadcrumb, 26);
lv_obj_set_flex_grow(state^.breadcrumb, 1);
lv_obj_set_style_bg_color(state^.breadcrumb, lv_color_make(38, 42, 56), 0);
lv_obj_set_style_bg_opa(state^.breadcrumb, LV_OPA_COVER, 0);
lv_obj_set_style_radius(state^.breadcrumb, 4, 0);
lv_obj_set_style_pad_left(state^.breadcrumb, 4, 0);
lv_obj_set_style_pad_right(state^.breadcrumb, 4, 0);
lv_obj_set_style_pad_column(state^.breadcrumb, 2, 0);
lv_obj_set_style_layout(state^.breadcrumb, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(state^.breadcrumb, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(state^.breadcrumb, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(state^.breadcrumb, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(state^.breadcrumb, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(state^.breadcrumb, @fb_crumb_area_cb,
LV_EVENT_CLICKED, state);
{ ---- Path text area (hidden by default, shown on breadcrumb click) ---- }
state^.path_ta := lv_textarea_create(toolbar);
lv_obj_set_height(state^.path_ta, 26);
lv_obj_set_flex_grow(state^.path_ta, 1);
lv_textarea_set_one_line(state^.path_ta, true);
lv_textarea_set_text(state^.path_ta, '/');
lv_obj_set_style_bg_color(state^.path_ta, lv_color_make(38, 42, 56), 0);
lv_obj_set_style_bg_opa(state^.path_ta, LV_OPA_COVER, 0);
lv_obj_set_style_text_color(state^.path_ta, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(state^.path_ta, @lv_font_montserrat_14, 0);
lv_obj_set_style_border_color(state^.path_ta, lv_color_make(80, 120, 200), 0);
lv_obj_set_style_border_width(state^.path_ta, 1, 0);
lv_obj_set_style_radius(state^.path_ta, 4, 0);
lv_obj_set_style_pad_all(state^.path_ta, 4, 0);
lv_obj_add_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_event_cb(state^.path_ta, @fb_path_ta_cb, LV_EVENT_KEY, state);
state^.path_editing := false;
makeToolBtn(toolbar, SYM_REFRESH, 28, state, @fb_refresh_cb);
{ File operation buttons }
makeToolBtn(toolbar, SYM_DIRECTORY, 32, state, @fb_newfolder_cb);
makeToolBtn(toolbar, SYM_FILE, 32, state, @fb_newfile_cb);
makeToolBtn(toolbar, SYM_EDIT, 32, state, @fb_rename_cb);
makeToolBtn(toolbar, SYM_TRASH, 32, state, @fb_delete_cb);
{ Filter input }
state^.filter_ta := lv_textarea_create(toolbar);
lv_textarea_set_one_line(state^.filter_ta, true);
lv_textarea_set_placeholder_text(state^.filter_ta, 'Filter...');
lv_obj_set_size(state^.filter_ta, 120, 26);
lv_obj_set_style_bg_color(state^.filter_ta, lv_color_make(38, 42, 56), 0);
lv_obj_set_style_bg_opa(state^.filter_ta, LV_OPA_COVER, 0);
lv_obj_set_style_text_color(state^.filter_ta, lv_color_make(210, 215, 230), 0);
lv_obj_set_style_text_font(state^.filter_ta, @lv_font_montserrat_14, 0);
lv_obj_set_style_border_color(state^.filter_ta, lv_color_make(60, 65, 80), 0);
lv_obj_set_style_border_width(state^.filter_ta, 1, 0);
lv_obj_set_style_radius(state^.filter_ta, 4, 0);
lv_obj_set_style_pad_all(state^.filter_ta, 4, 0);
lv_obj_add_event_cb(state^.filter_ta, @fb_filter_changed_cb,
LV_EVENT_VALUE_CHANGED, state);
makeToolBtn(toolbar, SYM_CLOSE, 24, state, @fb_filter_clear_cb);
makeToolBtn(toolbar, SYM_IMAGE, 24, state, @fb_preview_toggle_cb);
makeToolBtn(toolbar, SYM_LIST, 24, state, @fb_viewmode_toggle_cb);
makeToolBtn(toolbar, SYM_EYE_OPEN, 28, state, @fb_hidden_toggle_cb);
{ Dim disabled nav buttons initially }
lv_obj_remove_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.back_btn, 80, 0);
lv_obj_remove_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(state^.fwd_btn, 80, 0);
{ ---- Tab bar ---- }
state^.tab_bar := lv_obj_create(content);
lv_obj_remove_style_all(state^.tab_bar);
lv_obj_set_size(state^.tab_bar, lv_pct(100), 26);
lv_obj_set_style_bg_color(state^.tab_bar, lv_color_make(28, 31, 40), 0);
lv_obj_set_style_bg_opa(state^.tab_bar, LV_OPA_COVER, 0);
lv_obj_set_style_layout(state^.tab_bar, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(state^.tab_bar, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(state^.tab_bar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_left(state^.tab_bar, 4, 0);
lv_obj_set_style_pad_column(state^.tab_bar, 2, 0);
lv_obj_remove_flag(state^.tab_bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_border_color(state^.tab_bar, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(state^.tab_bar, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_set_style_border_width(state^.tab_bar, 1, 0);
state^.tab_count := 1;
state^.active_tab := 0;
{ ---- Body row — sidebar + main content ---- }
body_row := lv_obj_create(content);
lv_obj_remove_style_all(body_row);
lv_obj_set_size(body_row, lv_pct(100), 0);
lv_obj_set_flex_grow(body_row, 1);
lv_obj_set_style_layout(body_row, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(body_row, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(body_row, 0, 0);
lv_obj_remove_flag(body_row, LV_OBJ_FLAG_SCROLLABLE);
state^.body_row := body_row;
{ ---- Bookmark sidebar (150px) ---- }
sidebar := lv_obj_create(body_row);
lv_obj_remove_style_all(sidebar);
lv_obj_set_size(sidebar, 150, lv_pct(100));
lv_obj_set_style_bg_color(sidebar, lv_color_make(25, 27, 36), 0);
lv_obj_set_style_bg_opa(sidebar, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(sidebar, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(sidebar, LV_BORDER_SIDE_RIGHT, 0);
lv_obj_set_style_border_width(sidebar, 1, 0);
lv_obj_set_style_pad_all(sidebar, 6, 0);
lv_obj_set_style_pad_row(sidebar, 2, 0);
lv_obj_set_style_layout(sidebar, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(sidebar, LV_FLEX_FLOW_COLUMN);
lv_obj_add_flag(sidebar, LV_OBJ_FLAG_SCROLLABLE);
state^.sidebar := sidebar;
{ ---- Main content area — hosts directory listing rows ---- }
main_area := lv_obj_create(body_row);
lv_obj_remove_style_all(main_area);
lv_obj_set_size(main_area, 0, lv_pct(100));
lv_obj_set_flex_grow(main_area, 1);
lv_obj_set_style_bg_color(main_area, lv_color_make(20, 22, 32), 0);
lv_obj_set_style_bg_opa(main_area, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(main_area, 4, 0);
lv_obj_set_style_layout(main_area, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(main_area, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(main_area, 2, 0);
lv_obj_add_flag(main_area, LV_OBJ_FLAG_SCROLLABLE);
state^.content_area := main_area;
{ Type-ahead keyboard handler on content area }
lv_obj_add_event_cb(main_area, @fb_typeahead_key_cb, LV_EVENT_KEY, state);
{ ---- Preview panel (right side, hidden by default) ---- }
state^.preview_panel := lv_obj_create(body_row);
lv_obj_remove_style_all(state^.preview_panel);
lv_obj_set_size(state^.preview_panel, 200, lv_pct(100));
lv_obj_set_style_bg_color(state^.preview_panel, lv_color_make(25, 27, 36), 0);
lv_obj_set_style_bg_opa(state^.preview_panel, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(state^.preview_panel, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(state^.preview_panel, LV_BORDER_SIDE_LEFT, 0);
lv_obj_set_style_border_width(state^.preview_panel, 1, 0);
lv_obj_set_style_pad_all(state^.preview_panel, 8, 0);
lv_obj_set_style_pad_row(state^.preview_panel, 4, 0);
lv_obj_set_style_layout(state^.preview_panel, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(state^.preview_panel, LV_FLEX_FLOW_COLUMN);
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
state^.preview_visible := false;
{ ---- Status bar ---- }
statusbar := lv_obj_create(content);
lv_obj_remove_style_all(statusbar);
lv_obj_set_size(statusbar, lv_pct(100), STATUSBAR_H);
lv_obj_set_style_bg_color(statusbar, lv_color_make(28, 31, 40), 0);
lv_obj_set_style_bg_opa(statusbar, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(statusbar, lv_color_make(48, 52, 68), 0);
lv_obj_set_style_border_side(statusbar, LV_BORDER_SIDE_TOP, 0);
lv_obj_set_style_border_width(statusbar, 1, 0);
lv_obj_set_style_layout(statusbar, LV_LAYOUT_FLEX, 0);
lv_obj_set_flex_flow(statusbar, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(statusbar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_left(statusbar, 8, 0);
lv_obj_remove_flag(statusbar, LV_OBJ_FLAG_SCROLLABLE);
state^.status_bar := statusbar;
sl := lv_label_create(statusbar);
lv_label_set_text(sl, '/');
lv_obj_set_style_text_color(sl, lv_color_make(140, 150, 170), 0);
lv_obj_set_style_text_font(sl, @lv_font_montserrat_14, 0);
state^.status_label := sl;
{ Register resize callback }
driver.video.windows.setWindowResizeCallback(g_win_id, @onResize);
{ Create process so filebrowser appears in PS }
ctx := proc.mgr.create('Files', @fb_entry, void(state), 1);
if ctx <> nil then begin
state^.pid := ctx^.ProcessID;
driver.video.windows.setWindowOwner(g_win_id, ctx^.ProcessID);
end;
{ Create 50ms poll timer so LVGL can pick up async refresh data }
state^.refresh_poll := lv_timer_create(@fb_refresh_poll_cb, 50, state);
{ Load initial directory }
navigate_to(state, '/');
rebuild_tab_bar(state);
{ Create 500ms poll timer for directory watch notifications }
state^.watch_timer := lv_timer_create(@fb_watch_poll_timer, 500, state);
{ Default bookmarks }
add_bookmark_entry(state, '/');
add_bookmark_entry(state, '/sys');
add_bookmark_entry(state, '/boot');
debug.tracer.pop_trace;
end;
{ ============================================================
init — register with the desktop program launcher
============================================================ }
procedure init();
begin
debug.tracer.push_trace('filebrowser.init');
g_win_id := 0;
g_state := nil;
driver.video.desktop.registerProgram('Files', @launch);
debug.tracer.pop_trace;
end;
end.