KUMOS/Client/Default Apps/AppManager.sbi
2026-05-02 15:49:06 +02:00

411 lines
12 KiB
Plaintext

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