DeclareModule FileExplorer Declare Open() EndDeclareModule Module FileExplorer EnableExplicit ; Layout constants #Toolbar_Height = 35 #Statusbar_Height = 24 #Window_W = 480 #Window_H = 400 #Max_Entries = 512 ; Gadgets Global Window Global UpButton Global PathLabel Global NewFolderButton Global NewFileButton Global ListView Global StatusLabel ; Navigation state Global CurrentPath.s = "/" Global Dim PathStack.s(64) Global StackDepth = 0 ; Entry cache (populated on each listing) Global Dim EntryIDs.s(#Max_Entries) Global Dim EntryNames.s(#Max_Entries) Global Dim EntryIsDir(#Max_Entries) Global EntryCount = 0 ; Input window state Global InputWindow = 0 Global InputField Global InputMode ; 0 = new folder, 1 = new file Global ButtonNew Global CancelBtn ; Private declarations Declare _Load(Path.s) Declare _ListCallback(Success.i, DataString.s) Declare _MkdirCallback(Success.i, DataString.s) Declare _CreateFileCallback(Success.i, DataString.s) Declare _OpenInputWindow(Mode.i) Declare _CloseInputWindow() Declare Handler_Resize() Declare Handler_Up() Declare Handler_NewFolder() Declare Handler_NewFile() Declare Handler_ListView() Declare Handler_Close() Declare Handler_InputConfirm() Declare Handler_InputCancel() ; Public procedures Procedure Open() If IsWindow(Window) SetActiveWindow(Window) ProcedureReturn EndIf Window = OpenWindow(#PB_Any, 150, 80, #Window_W, #Window_H, "File Explorer", #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu) ; Toolbar UpButton = ButtonGadget(#PB_Any, 2, 4, 30, #Toolbar_Height - 8, "↑") PathLabel = TextGadget (#PB_Any, 36, 4, #Window_W - 200, #Toolbar_Height - 8, "/") NewFolderButton= ButtonGadget(#PB_Any, #Window_W - 162, 4, 80, #Toolbar_Height - 8, "+ Folder") NewFileButton = ButtonGadget(#PB_Any, #Window_W - 78, 4, 74, #Toolbar_Height - 8, "+ File") ; File list ListView = ListViewGadget(#PB_Any, 0, #Toolbar_Height, #Window_W, #Window_H - #Toolbar_Height - #Statusbar_Height) ; Status bar StatusLabel = TextGadget(#PB_Any, 4, #Window_H - #Statusbar_Height + 4, #Window_W - 8, #Statusbar_Height - 8, "") BindGadgetEvent(UpButton, @Handler_Up()) BindGadgetEvent(NewFolderButton, @Handler_NewFolder()) BindGadgetEvent(NewFileButton, @Handler_NewFile()) BindGadgetEvent(ListView, @Handler_ListView()) BindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Window) BindEvent(#PB_Event_CloseWindow, @Handler_Close(), Window) Desktop::Register("File Explorer", Window, "📁") CurrentPath = "/" StackDepth = 0 _Load("/") EndProcedure ; Private procedures Procedure _Load(Path.s) CurrentPath = Path SetGadgetText(PathLabel, Path) SetGadgetText(StatusLabel, "Loading...") ClearGadgetItems(ListView) EntryCount = 0 FS::List(Path, @_ListCallback()) EndProcedure Procedure _ListCallback(Success.i, DataString.s) If Not Success Or Not IsWindow(Window) Notify::Toast("Failed to load directory.", Notify::#Error) ProcedureReturn EndIf If Not ParseJSON(0, DataString) Notify::Toast("Could not read directory.", Notify::#Error) ProcedureReturn EndIf Protected Root.i = JSONValue(0) Protected Count.i = JSONArraySize(Root) Protected i.i, Item.i ClearGadgetItems(ListView) EntryCount = 0 For i = 0 To Count - 1 If EntryCount >= #Max_Entries : Break : EndIf Item = GetJSONElement(Root, i) EntryIDs(EntryCount) = Str(GetJSONInteger(GetJSONMember(Item, "id"))) EntryNames(EntryCount) = GetJSONString(GetJSONMember(Item, "name")) EntryIsDir(EntryCount) = GetJSONInteger(GetJSONMember(Item, "is_dir")) Protected Prefix.s = "[F] " If EntryIsDir(EntryCount) : Prefix = "[D] " : EndIf AddGadgetItem(ListView, -1, Prefix + EntryNames(EntryCount)) EntryCount + 1 Next FreeJSON(0) Protected Status.s = Str(EntryCount) + " item" If EntryCount <> 1 : Status + "s" : EndIf SetGadgetText(StatusLabel, Status) EndProcedure Procedure _MkdirCallback(Success.i, DataString.s) If Not IsWindow(Window) : ProcedureReturn : EndIf _Load(CurrentPath) ; refresh regardless — server may have created it EndProcedure Procedure _CreateFileCallback(Success.i, DataString.s) If Not IsWindow(Window) : ProcedureReturn : EndIf If Success And ParseJSON(0, DataString) Protected Root.i = JSONValue(0) If GetJSONBoolean(GetJSONMember(Root, "success")) Protected ID.s = Str(GetJSONInteger(GetJSONMember(Root, "id"))) Protected Path.s = CurrentPath If Right(Path, 1) <> "/" : Path + "/" : EndIf Path + EntryNames(0) ; name was stored in EntryNames(0) temporarily FreeJSON(0) _Load(CurrentPath) TextEditor::Open(ID, Path) ProcedureReturn EndIf FreeJSON(0) EndIf _Load(CurrentPath) EndProcedure Procedure _OpenInputWindow(Mode.i) If IsWindow(InputWindow) : ProcedureReturn : EndIf InputMode = Mode Protected Title.s = "New Folder" If Mode = 1 : Title = "New File" : EndIf Protected X.i = WindowX(Window) + (#Window_W / 2) - 150 Protected Y.i = WindowY(Window) + (#Window_H / 2) - 45 InputWindow = OpenWindow(#PB_Any, X, Y, 300, 90, Title, #PB_Window_TitleBar | #PB_Window_SystemMenu) TextGadget (#PB_Any, 10, 12, 280, 20, "Name:") InputField = StringGadget(#PB_Any, 10, 32, 280, 24, "") ButtonNew = ButtonGadget(#PB_Any, 10, 62, 130, 24, "OK") ; EventGadget index 2 CancelBtn = ButtonGadget(#PB_Any, 150, 62, 140, 24, "Cancel") SetActiveGadget(InputField) BindGadgetEvent(ButtonNew, @Handler_InputConfirm()) BindGadgetEvent(CancelBtn, @Handler_InputCancel()) BindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), InputWindow) EndProcedure Procedure _CloseInputWindow() If IsWindow(InputWindow) CloseWindow(InputWindow) EndIf EndProcedure ; Event handlers Procedure Handler_Resize() Protected W.i = WindowWidth(Window) Protected H.i = WindowHeight(Window) ResizeGadget(PathLabel, 36, 4, W - 200, #Toolbar_Height - 8) ResizeGadget(NewFolderButton, W - 162, 4, 80, #Toolbar_Height - 8) ResizeGadget(NewFileButton, W - 78, 4, 74, #Toolbar_Height - 8) ResizeGadget(ListView, 0, #Toolbar_Height, W, H - #Toolbar_Height - #Statusbar_Height) ResizeGadget(StatusLabel, 4, H - #Statusbar_Height + 4, W - 8, #Statusbar_Height - 8) EndProcedure Procedure Handler_Up() If StackDepth > 0 StackDepth - 1 _Load(PathStack(StackDepth)) EndIf EndProcedure Procedure Handler_NewFolder() _OpenInputWindow(0) EndProcedure Procedure Handler_NewFile() _OpenInputWindow(1) EndProcedure Procedure Handler_ListView() If EventType() <> #PB_EventType_LeftDoubleClick : ProcedureReturn : EndIf Protected Index.i = GetGadgetState(ListView) If Index < 0 Or Index >= EntryCount : ProcedureReturn : EndIf If EntryIsDir(Index) ; Push current path, navigate in PathStack(StackDepth) = CurrentPath StackDepth + 1 Protected NewPath.s = CurrentPath If Right(NewPath, 1) <> "/" : NewPath + "/" : EndIf NewPath + EntryNames(Index) _Load(NewPath) Else ; Open in text editor Protected Path.s = CurrentPath If Right(Path, 1) <> "/" : Path + "/" : EndIf Path + EntryNames(Index) TextEditor::Open(EntryIDs(Index), Path) EndIf EndProcedure Procedure Handler_Close() If IsWindow(InputWindow) UnbindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), InputWindow) UnbindGadgetEvent(ButtonNew, @Handler_InputConfirm()) UnbindGadgetEvent(CancelBtn, @Handler_InputCancel()) _CloseInputWindow() EndIf UnbindGadgetEvent(UpButton, @Handler_Up()) UnbindGadgetEvent(NewFolderButton, @Handler_NewFolder()) UnbindGadgetEvent(NewFileButton, @Handler_NewFile()) UnbindGadgetEvent(ListView, @Handler_ListView()) UnbindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Window) UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(), Window) Desktop::Unregister(Window) InputWindow = 0 Window = 0 EndProcedure Procedure Handler_InputConfirm() Protected Name.s = Trim(GetGadgetText(InputField)) If Name = "" : ProcedureReturn : EndIf Protected Path.s = CurrentPath If Right(Path, 1) <> "/" : Path + "/" : EndIf Path + Name _CloseInputWindow() If InputMode = 0 FS::Mkdir(Path, @_MkdirCallback()) Else ; Stash name temporarily for the callback to build the full path EntryNames(0) = Name FS::Write("", Path, "", @_CreateFileCallback()) EndIf EndProcedure Procedure Handler_InputCancel() _CloseInputWindow() EndProcedure EndModule ; IDE Options = SpiderBasic 3.10 (Windows - x86) ; CursorPosition = 96 ; Folding = CAg ; iOSAppOrientation = 0 ; AndroidAppCode = 0 ; AndroidAppOrientation = 0 ; EnableXP ; DPIAware ; CompileSourceDirectory