DeclareModule AppManager Declare Open() EndDeclareModule Module AppManager EnableExplicit ;- Layout #Win_W = 520 #Win_H = 380 #Toolbar_H = 40 #Statusbar_H = 26 #List_Row_H = 48 ;- Colors Global Color_Bg = RGB( 22, 22, 34) Global Color_Row_Odd = RGB( 28, 28, 42) Global Color_Row_Even = RGB( 22, 22, 34) Global Color_Sel = RGB( 52, 52, 72) Global Color_Accent = RGB(100, 120, 255) Global Color_Text = RGB(200, 200, 215) Global Color_Dim = RGB(110, 110, 135) Global Color_Sep = RGB( 48, 48, 65) Global Font_UI = LoadFont(#PB_Any, "sans-serif", 13) Global Font_Small = LoadFont(#PB_Any, "sans-serif", 11) ;- App list Structure InstalledApp ID.s Name.s Manifest.s Permissions.s EndStructure Global NewList _Apps.InstalledApp() ;- State Global Window = 0 Global _Canvas Global _StatusLabel Global _InstallBtn Global _LaunchBtn Global _UninstallBtn Global _Hover = -1 Global _Selected = -1 Global _PendingUninstall.s = "" ;- Install dialog Global _InputWin = 0 Global _URLField Global _OKBtn Global _CancelBtn ;- Private declarations Declare _Refresh() Declare _DrawList() Declare _UpdateButtons() Declare _Status(Msg.s) Declare _OpenInstallDialog() Declare _CloseInstallDialog() Declare _ListCallback(Success, DataString.s) Declare _InstallCallback(Success, DataString.s) Declare _UninstallCallback(Success, DataString.s) Declare _DoUninstall(Result) Declare Handler_Close() Declare Handler_Canvas() Declare Handler_Install() Declare Handler_Launch() Declare Handler_Uninstall() Declare Handler_InputOK() Declare Handler_InputCancel() Declare Handler_Resize() ;- Public Procedure Open() If IsWindow(Window) SetActiveWindow(Window) : ProcedureReturn EndIf Window = OpenWindow(#PB_Any, 160, 100, #Win_W, #Win_H, "App Manager", #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu) SetWindowColor(Window, Color_Bg) _InstallBtn = ButtonGadget(#PB_Any, 4, 6, 100, #Toolbar_H - 12, "+ Install") _LaunchBtn = ButtonGadget(#PB_Any, 112, 6, 80, #Toolbar_H - 12, "Launch") _UninstallBtn = ButtonGadget(#PB_Any, 200, 6, 90, #Toolbar_H - 12, "Uninstall") _Canvas = CanvasGadget(#PB_Any, 0, #Toolbar_H, #Win_W, #Win_H - #Toolbar_H - #Statusbar_H) _StatusLabel = TextGadget(#PB_Any, 4, #Win_H - #Statusbar_H + 4, #Win_W - 8, #Statusbar_H - 8, "") SetGadgetColor(_StatusLabel, #PB_Gadget_FrontColor, Color_Dim) SetGadgetColor(_StatusLabel, #PB_Gadget_BackColor, Color_Bg) BindGadgetEvent(_InstallBtn, @Handler_Install()) BindGadgetEvent(_LaunchBtn, @Handler_Launch()) BindGadgetEvent(_UninstallBtn, @Handler_Uninstall()) BindGadgetEvent(_Canvas, @Handler_Canvas()) BindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Window) BindEvent(#PB_Event_CloseWindow, @Handler_Close(), Window) Desktop::Register("App Manager", Window, "📦") _UpdateButtons() _Refresh() EndProcedure ;- Private Procedure _Refresh() _Status("Loading...") ClearList(_Apps()) _Selected = -1 _DrawList() HTTPRequest(#PB_HTTP_Get, "/api/apps/list", "", @_ListCallback()) EndProcedure Procedure _DrawList() If Not IsGadget(_Canvas) : ProcedureReturn : EndIf If Not StartDrawing(CanvasOutput(_Canvas)) : ProcedureReturn : EndIf Protected W = OutputWidth() Protected H = OutputHeight() Box(0, 0, W, H, Color_Bg) If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf Protected TH = TextHeight("A") ForEach _Apps() Protected i = ListIndex(_Apps()) Protected Y = i * #List_Row_H If Y + #List_Row_H > H : Break : EndIf Protected RowBg If i = _Selected RowBg = Color_Sel ElseIf i = _Hover RowBg = RGB(40, 40, 58) ElseIf i % 2 = 0 RowBg = Color_Row_Even Else RowBg = Color_Row_Odd EndIf Box(0, Y, W, #List_Row_H, RowBg) If i = _Selected : Box(0, Y, 3, #List_Row_H, Color_Accent) : EndIf DrawingMode(#PB_2DDrawing_Transparent) DrawText(14, Y + (#List_Row_H / 2) - TH - 1, _Apps()\Name, Color_Text) If IsFont(Font_Small) : DrawingFont(FontID(Font_Small)) : EndIf DrawText(14, Y + (#List_Row_H / 2) + 2, _Apps()\ID, Color_Dim) If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf Box(0, Y + #List_Row_H - 1, W, 1, Color_Sep) Next If ListSize(_Apps()) = 0 DrawingMode(#PB_2DDrawing_Transparent) If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf DrawText((W - TextWidth("No apps installed")) / 2, (H - TH) / 2, "No apps installed", Color_Dim) EndIf StopDrawing() EndProcedure Procedure _UpdateButtons() DisableGadget(_LaunchBtn, Bool(_Selected < 0)) DisableGadget(_UninstallBtn, Bool(_Selected < 0)) EndProcedure Procedure _Status(Msg.s) If IsGadget(_StatusLabel) : SetGadgetText(_StatusLabel, Msg) : EndIf EndProcedure Procedure _OpenInstallDialog() If IsWindow(_InputWin) : ProcedureReturn : EndIf Protected X = WindowX(Window) + (#Win_W / 2) - 170 Protected Y = WindowY(Window) + (#Win_H / 2) - 50 _InputWin = OpenWindow(#PB_Any, X, Y, 340, 100, "Install App from URL", #PB_Window_TitleBar | #PB_Window_SystemMenu) TextGadget(#PB_Any, 10, 14, 320, 18, "Package URL (.zip):") _URLField = StringGadget(#PB_Any, 10, 34, 320, 26, "") _OKBtn = ButtonGadget(#PB_Any, 10, 68, 150, 24, "Install") _CancelBtn = ButtonGadget(#PB_Any, 170, 68, 160, 24, "Cancel") SetActiveGadget(_URLField) BindGadgetEvent(_OKBtn, @Handler_InputOK()) BindGadgetEvent(_CancelBtn, @Handler_InputCancel()) BindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), _InputWin) EndProcedure Procedure _CloseInstallDialog() If Not IsWindow(_InputWin) : ProcedureReturn : EndIf UnbindGadgetEvent(_OKBtn, @Handler_InputOK()) UnbindGadgetEvent(_CancelBtn, @Handler_InputCancel()) UnbindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), _InputWin) CloseWindow(_InputWin) _InputWin = 0 EndProcedure ;- HTTP callbacks Procedure _ListCallback(Success, DataString.s) If Not IsWindow(Window) : ProcedureReturn : EndIf ClearList(_Apps()) _Selected = -1 If Success And ParseJSON(0, DataString) Protected Root = JSONValue(0) Protected Total = JSONArraySize(Root) Protected i For i = 0 To Total - 1 Protected Item = GetJSONElement(Root, i) Protected ManiNode = GetJSONMember(Item, "manifest") AddElement(_Apps()) _Apps()\ID = GetJSONString(GetJSONMember(Item, "app_id")) Protected N.s = GetJSONString(GetJSONMember(ManiNode, "name")) If N <> "" _Apps()\Name = N Else _Apps()\Name = _Apps()\ID EndIf ; Re-serialise manifest to a JSON string Protected Icon.s = GetJSONString(GetJSONMember(ManiNode, "icon")) _Apps()\Manifest = "{" + ~"\"id\":\"" + GetJSONString(GetJSONMember(ManiNode, "id")) + ~"\"," + ~"\"name\":\"" + GetJSONString(GetJSONMember(ManiNode, "name")) + ~"\"," + ~"\"version\":\"" + GetJSONString(GetJSONMember(ManiNode, "version")) + ~"\"," + ~"\"icon\":\"" + ReplaceString(Icon, ~"\"", ~"\\\"") + ~"\"," + ~"\"entry\":\"" + GetJSONString(GetJSONMember(ManiNode, "entry")) + ~"\"}" ; Re-serialise permissions array to a JSON string Protected PermNode = GetJSONMember(Item, "permissions") Protected PermCount = JSONArraySize(PermNode) Protected j, Perms.s = "[" For j = 0 To PermCount - 1 If j > 0 : Perms + "," : EndIf Perms + ~"\"" + GetJSONString(GetJSONElement(PermNode, j)) + ~"\"" Next _Apps()\Permissions = Perms + "]" ; Sync with the desktop start menu Desktop::InstallThirdPartyApp(_Apps()\ID, _Apps()\Manifest, _Apps()\Permissions, Icon) Next FreeJSON(0) Protected Count = ListSize(_Apps()) If Count = 1 _Status("1 app installed") Else _Status(Str(Count) + " apps installed") EndIf Else _Status("Failed to load app list.") EndIf _DrawList() _UpdateButtons() EndProcedure Procedure _InstallCallback(Success, DataString.s) If Not IsWindow(Window) : ProcedureReturn : EndIf DisableGadget(_InstallBtn, #False) If Success And ParseJSON(0, DataString) Protected Root = JSONValue(0) If GetJSONBoolean(GetJSONMember(Root, "success")) Protected AppID.s = GetJSONString(GetJSONMember(Root, "app_id")) FreeJSON(0) Notify::Toast("Installed: " + AppID, Notify::#Success) _Refresh() ProcedureReturn EndIf Protected Err.s = GetJSONString(GetJSONMember(Root, "error")) FreeJSON(0) Notify::Toast(Err, Notify::#Error) Else Notify::Toast("Install failed.", Notify::#Error) EndIf _Status("Ready.") EndProcedure Procedure _UninstallCallback(Success, DataString.s) If Not IsWindow(Window) : ProcedureReturn : EndIf If Success And ParseJSON(0, DataString) If GetJSONBoolean(GetJSONMember(JSONValue(0), "success")) FreeJSON(0) Notify::Toast("App uninstalled.", Notify::#Success) Desktop::UninstallThirdPartyApp(_PendingUninstall) _PendingUninstall = "" _Refresh() ProcedureReturn EndIf FreeJSON(0) EndIf Notify::Toast("Uninstall failed.", Notify::#Error) _PendingUninstall = "" _Status("Ready.") EndProcedure ;- Event handlers Procedure Handler_Canvas() Protected EType = EventType() Protected MY = GetGadgetAttribute(_Canvas, #PB_Canvas_MouseY) Protected Row = MY / #List_Row_H Select EType Case #PB_EventType_MouseMove If Row >= ListSize(_Apps()) : Row = -1 : EndIf If Row <> _Hover _Hover = Row : _DrawList() EndIf Case #PB_EventType_MouseLeave If _Hover <> -1 : _Hover = -1 : _DrawList() : EndIf Case #PB_EventType_LeftButtonUp If Row >= 0 And Row < ListSize(_Apps()) _Selected = Row : _DrawList() : _UpdateButtons() EndIf Case #PB_EventType_LeftDoubleClick If Row >= 0 And Row < ListSize(_Apps()) _Selected = Row Handler_Launch() EndIf EndSelect EndProcedure Procedure Handler_Install() _OpenInstallDialog() EndProcedure Procedure Handler_Launch() If _Selected < 0 Or Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf AppRuntime::Launch(_Apps()\ID, _Apps()\Manifest, _Apps()\Permissions) EndProcedure Procedure Handler_Uninstall() If _Selected < 0 Or Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf Notify::Confirm("Uninstall " + _Apps()\Name, "Remove this app? Private storage will also be deleted.", @_DoUninstall()) EndProcedure Procedure _DoUninstall(Result) If Not Result Or _Selected < 0 : ProcedureReturn : EndIf If Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf _PendingUninstall = _Apps()\ID _Status("Uninstalling " + _Apps()\ID + "...") HTTPRequest(#PB_HTTP_Post, "/api/apps/uninstall", "app_id=" + URLEncoder(_Apps()\ID, #PB_UTF8) + "&keep_data=0", @_UninstallCallback()) EndProcedure Procedure Handler_InputOK() Protected URL.s = Trim(GetGadgetText(_URLField)) If URL = "" : ProcedureReturn : EndIf _CloseInstallDialog() _Status("Installing...") DisableGadget(_InstallBtn, #True) HTTPRequest(#PB_HTTP_Post, "/api/apps/install", "url=" + URLEncoder(URL, #PB_UTF8), @_InstallCallback()) EndProcedure Procedure Handler_InputCancel() _CloseInstallDialog() EndProcedure Procedure Handler_Resize() Protected W = WindowWidth(Window) Protected H = WindowHeight(Window) ResizeGadget(_Canvas, 0, #Toolbar_H, W, H - #Toolbar_H - #Statusbar_H) ResizeGadget(_StatusLabel, 4, H - #Statusbar_H + 4, W - 8, #Statusbar_H - 8) _DrawList() EndProcedure Procedure Handler_Close() _CloseInstallDialog() UnbindGadgetEvent(_InstallBtn, @Handler_Install()) UnbindGadgetEvent(_LaunchBtn, @Handler_Launch()) UnbindGadgetEvent(_UninstallBtn, @Handler_Uninstall()) UnbindGadgetEvent(_Canvas, @Handler_Canvas()) UnbindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Window) UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(), Window) Desktop::Unregister(Window) Window = 0 EndProcedure EndModule ; IDE Options = SpiderBasic 3.10 (Windows - x86) ; CursorPosition = 4 ; FirstLine = 30 ; Folding = DAA5 ; iOSAppOrientation = 0 ; AndroidAppCode = 0 ; AndroidAppOrientation = 0 ; EnableXP ; DPIAware ; CompileSourceDirectory