409 lines
14 KiB
Plaintext
409 lines
14 KiB
Plaintext
Module AppRuntime
|
|
EnableExplicit
|
|
|
|
;- Instance structure
|
|
Structure Instance
|
|
InstanceID.i ; stable unique ID — used as the JS↔SB bridge key
|
|
Window.i
|
|
View.i
|
|
AppID.s
|
|
Perms.s
|
|
EndStructure
|
|
|
|
Global NewList Instances.Instance()
|
|
Global _NextID = 1 ; auto-incrementing ID counter
|
|
|
|
;- Pending async operations
|
|
; One pending op of each type at a time (JS is single-threaded).
|
|
|
|
Global _PL_ID = -1 : Global _PL_KMID.s = "" ; list
|
|
Global _PS_ID = -1 : Global _PS_KMID.s = "" ; stat (standalone)
|
|
Global _PSR_ID = -1 : Global _PSR_KMID.s = "" ; stat-before-read/delete
|
|
Global _PR_ID = -1 : Global _PR_KMID.s = "" ; read
|
|
Global _PW_ID = -1 : Global _PW_KMID.s = "" ; write / mkdir / delete
|
|
Global _PC_ID = -1 : Global _PC_KMID.s = "" ; confirm dialog
|
|
|
|
;- Private declarations
|
|
Declare _FindByID(ID)
|
|
Declare _FindByWindow(Win)
|
|
Declare _HasPerm(ID, Token.s)
|
|
Declare _SendResponse(ID, KMID.s, Success, DataString.s)
|
|
Declare.s _StoragePath(ID, RelPath.s)
|
|
Declare _Remove(ID)
|
|
Declare _CloseByID(ID)
|
|
Declare _Dispatch(ID, RawMsg.s)
|
|
Declare _DoList(ID, KMID.s, Path.s)
|
|
Declare _DoStat(ID, KMID.s, Path.s)
|
|
Declare _DoStatForRead(ID, KMID.s, Path.s)
|
|
Declare _DoRead(ID, KMID.s, FileID)
|
|
Declare _DoWrite(ID, KMID.s, Path.s, Content.s)
|
|
Declare _DoMkdir(ID, KMID.s, Path.s)
|
|
Declare _DoDelete(ID, KMID.s, Path.s)
|
|
Declare _ListCallback(Success, DataString.s)
|
|
Declare _StatCallback(Success, DataString.s)
|
|
Declare _StatForReadCallback(Success, DataString.s)
|
|
Declare _ReadCallback(Success, DataString.s)
|
|
Declare _WriteCallback(Success, DataString.s)
|
|
Declare _ConfirmCallback(Result)
|
|
Declare Handler_Resize()
|
|
Declare Handler_Close()
|
|
|
|
;- Public procedures
|
|
|
|
; Call once from Desktop::Open.
|
|
Procedure Init()
|
|
!window._kumos_rt = { instances: {} };
|
|
!window.addEventListener('message', function(e) {
|
|
! var inst;
|
|
! for (var id in window._kumos_rt.instances) {
|
|
! inst = window._kumos_rt.instances[id];
|
|
! if (inst && inst.frameEl && inst.frameEl.contentWindow === e.source) {
|
|
! if (typeof e.data === 'string') {
|
|
! appruntime$f__dispatch(inst.id, e.data);
|
|
! }
|
|
! return;
|
|
! }
|
|
! }
|
|
!});
|
|
EndProcedure
|
|
|
|
; Open a new sandboxed app window.
|
|
Procedure Launch(AppID.s, ManifestJSON.s, Permissions.s)
|
|
Protected Count, W, H, Win, View, ID
|
|
Protected AppName.s, N.s, Src.s
|
|
|
|
AppName = AppID
|
|
If ParseJSON(0, ManifestJSON)
|
|
N = GetJSONString(GetJSONMember(JSONValue(0), "name"))
|
|
If N <> "" : AppName = N : EndIf
|
|
FreeJSON(0)
|
|
EndIf
|
|
|
|
Count = ListSize(Instances())
|
|
W = 640
|
|
H = 480
|
|
Win = OpenWindow(#PB_Any, 80 + (Count % 8) * 24, 80 + (Count % 8) * 24, W, H, AppName, #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu)
|
|
View = WebGadget(#PB_Any, 0, 0, W, H, "about:blank")
|
|
Src = "/api/apps/" + AppID + "/index.html"
|
|
ID = _NextID
|
|
_NextID + 1
|
|
|
|
!(function(){
|
|
! var frames = document.querySelectorAll('iframe[src="about:blank"]:not([data-kumos-instance])');
|
|
! var f = frames[frames.length - 1];
|
|
! if (!f) return;
|
|
! f.setAttribute('data-kumos-instance', String(v_id));
|
|
! // No allow-same-origin -> cross-origin isolation even from same host.
|
|
! f.setAttribute('sandbox', 'allow-scripts allow-forms allow-modals allow-downloads allow-pointer-lock');
|
|
! window._kumos_rt.instances[v_id] = { id: v_id, frameEl: f, appId: v_appid };
|
|
! f.src = v_src;
|
|
!})();
|
|
|
|
AddElement(Instances())
|
|
Instances()\InstanceID = ID
|
|
Instances()\Window = Win
|
|
Instances()\View = View
|
|
Instances()\AppID = AppID
|
|
Instances()\Perms = Permissions
|
|
|
|
BindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Win)
|
|
BindEvent(#PB_Event_CloseWindow, @Handler_Close(), Win)
|
|
|
|
Desktop::Register(AppName, Win, "📦")
|
|
EndProcedure
|
|
|
|
;- Private procedures
|
|
; Leaves list cursor on the found element. Returns #True if found.
|
|
Procedure _FindByID(ID)
|
|
ForEach Instances()
|
|
If Instances()\InstanceID = ID : ProcedureReturn #True : EndIf
|
|
Next
|
|
ProcedureReturn #False
|
|
EndProcedure
|
|
|
|
Procedure _FindByWindow(Win)
|
|
ForEach Instances()
|
|
If Instances()\Window = Win : ProcedureReturn #True : EndIf
|
|
Next
|
|
ProcedureReturn #False
|
|
EndProcedure
|
|
|
|
Procedure _HasPerm(ID, Token.s)
|
|
If Not _FindByID(ID) : ProcedureReturn #False : EndIf
|
|
ProcedureReturn Bool(FindString(Instances()\Perms, ~"\"" + Token + ~"\"") > 0)
|
|
EndProcedure
|
|
|
|
Procedure _SendResponse(ID, KMID.s, Success, DataString.s)
|
|
!var inst = window._kumos_rt.instances[v_id];
|
|
!if (!inst || !inst.frameEl) return;
|
|
!var resp = { kmid: v_kmid, success: !!v_success };
|
|
!if (v_success) {
|
|
! try { resp.data = JSON.parse(v_datastring); }
|
|
! catch(e) { resp.data = v_datastring || null; }
|
|
!} else {
|
|
! resp.error = v_datastring || 'Error';
|
|
!}
|
|
!try { inst.frameEl.contentWindow.postMessage(JSON.stringify(resp), '*'); } catch(e) {}
|
|
EndProcedure
|
|
|
|
Procedure.s _StoragePath(ID, RelPath.s)
|
|
Protected Base.s
|
|
If Not _FindByID(ID) : ProcedureReturn "" : EndIf
|
|
Base = "/.apps/" + Instances()\AppID + "/"
|
|
If RelPath = "" Or RelPath = "/" : ProcedureReturn Base : EndIf
|
|
If Left(RelPath, 1) = "/" : RelPath = Mid(RelPath, 2) : EndIf
|
|
ProcedureReturn Base + RelPath
|
|
EndProcedure
|
|
|
|
Procedure _Remove(ID)
|
|
If Not _FindByID(ID) : ProcedureReturn : EndIf
|
|
!delete window._kumos_rt.instances[v_id];
|
|
UnbindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Instances()\Window)
|
|
UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(), Instances()\Window)
|
|
Desktop::Unregister(Instances()\Window)
|
|
DeleteElement(Instances())
|
|
EndProcedure
|
|
|
|
Procedure _CloseByID(ID)
|
|
_Remove(ID)
|
|
EndProcedure
|
|
|
|
;- Message dispatcher
|
|
Procedure _Dispatch(ID, RawMsg.s)
|
|
Protected Root, Args, TType
|
|
Protected KMID.s, Action.s, Path.s, Content.s, Title_.s, Msg.s, MsgType.s
|
|
|
|
If Not _FindByID(ID) : ProcedureReturn : EndIf
|
|
If Not ParseJSON(0, RawMsg) : ProcedureReturn : EndIf
|
|
|
|
Root = JSONValue(0)
|
|
KMID = GetJSONString(GetJSONMember(Root, "kmid"))
|
|
Action = GetJSONString(GetJSONMember(Root, "action"))
|
|
Args = GetJSONMember(Root, "args")
|
|
|
|
If KMID = "" : FreeJSON(0) : ProcedureReturn : EndIf
|
|
|
|
Path = GetJSONString(GetJSONMember(Args, "path"))
|
|
Content = GetJSONString(GetJSONMember(Args, "content"))
|
|
Title_ = GetJSONString(GetJSONMember(Args, "title"))
|
|
Msg = GetJSONString(GetJSONMember(Args, "message"))
|
|
MsgType = GetJSONString(GetJSONMember(Args, "type"))
|
|
FreeJSON(0)
|
|
|
|
Select Action
|
|
|
|
Case "window.ready"
|
|
_FindByID(ID)
|
|
_SendResponse(ID, KMID, #True, ~"{\"app_id\":\"" + ReplaceString(Instances()\AppID, ~"\"", ~"\\\"") + ~"\"}")
|
|
|
|
Case "window.setTitle"
|
|
_FindByID(ID)
|
|
SetWindowTitle(Instances()\Window, Title_)
|
|
_SendResponse(ID, KMID, #True, "")
|
|
|
|
Case "window.close"
|
|
_SendResponse(ID, KMID, #True, "")
|
|
!setTimeout(function(){ appruntime$f__closebyid(v_id); }, 50);
|
|
|
|
Case "notify.toast"
|
|
If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf
|
|
TType = Notify::#Info
|
|
If MsgType = "success" : TType = Notify::#Success
|
|
ElseIf MsgType = "warning" : TType = Notify::#Warning
|
|
ElseIf MsgType = "error" : TType = Notify::#Error
|
|
EndIf
|
|
Notify::Toast(Msg, TType)
|
|
_SendResponse(ID, KMID, #True, "")
|
|
|
|
Case "notify.confirm"
|
|
If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf
|
|
If _PC_ID >= 0 : _SendResponse(ID, KMID, #False, "A dialog is already open") : ProcedureReturn : EndIf
|
|
_PC_ID = ID : _PC_KMID = KMID
|
|
Notify::Confirm(Title_, Msg, @_ConfirmCallback())
|
|
|
|
Case "storage.list" : _DoList(ID, KMID, _StoragePath(ID, Path))
|
|
Case "storage.stat" : _DoStat(ID, KMID, _StoragePath(ID, Path))
|
|
Case "storage.read" : _DoStatForRead(ID, KMID, _StoragePath(ID, Path))
|
|
Case "storage.write" : _DoWrite(ID, KMID, _StoragePath(ID, Path), Content)
|
|
Case "storage.mkdir" : _DoMkdir(ID, KMID, _StoragePath(ID, Path))
|
|
Case "storage.delete" : _DoDelete(ID, KMID, _StoragePath(ID, Path))
|
|
|
|
Case "fs.list"
|
|
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
|
|
_DoList(ID, KMID, Path)
|
|
|
|
Case "fs.stat"
|
|
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
|
|
_DoStat(ID, KMID, Path)
|
|
|
|
Case "fs.read"
|
|
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
|
|
_DoStatForRead(ID, KMID, Path)
|
|
|
|
Case "fs.write"
|
|
If Not _HasPerm(ID, "fs.write") : _SendResponse(ID, KMID, #False, "Permission denied: fs.write") : ProcedureReturn : EndIf
|
|
_DoWrite(ID, KMID, Path, Content)
|
|
|
|
Default
|
|
_SendResponse(ID, KMID, #False, "Unknown action: " + Action)
|
|
|
|
EndSelect
|
|
EndProcedure
|
|
|
|
;- Async FS helpers
|
|
Procedure _DoList(ID, KMID.s, Path.s)
|
|
If _PL_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PL_ID = ID : _PL_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/list?path=" + URLEncoder(Path, #PB_UTF8), "", @_ListCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoStat(ID, KMID.s, Path.s)
|
|
If _PS_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PS_ID = ID : _PS_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoStatForRead(ID, KMID.s, Path.s)
|
|
If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PSR_ID = ID : _PSR_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoRead(ID, KMID.s, FileID)
|
|
If _PR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PR_ID = ID : _PR_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/read?id=" + FileID, "", @_ReadCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoWrite(ID, KMID.s, Path.s, Content.s)
|
|
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PW_ID = ID : _PW_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/write", "path=" + URLEncoder(Path, #PB_UTF8) + "&content=" + URLEncoder(Content, #PB_UTF8), @_WriteCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoMkdir(ID, KMID.s, Path.s)
|
|
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PW_ID = ID : _PW_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/mkdir", "path=" + URLEncoder(Path, #PB_UTF8), @_WriteCallback())
|
|
EndProcedure
|
|
|
|
Procedure _DoDelete(ID, KMID.s, Path.s)
|
|
If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PSR_ID = ID : _PSR_KMID = "DEL:" + KMID
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback())
|
|
EndProcedure
|
|
|
|
;- Callbacks
|
|
Procedure _ListCallback(Success, DataString.s)
|
|
Protected ID
|
|
Protected KMID.s
|
|
If _PL_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PL_ID : KMID = _PL_KMID
|
|
_PL_ID = -1 : _PL_KMID = ""
|
|
_SendResponse(ID, KMID, Success, DataString)
|
|
EndProcedure
|
|
|
|
Procedure _StatCallback(Success, DataString.s)
|
|
Protected ID
|
|
Protected KMID.s
|
|
If _PS_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PS_ID : KMID = _PS_KMID
|
|
_PS_ID = -1 : _PS_KMID = ""
|
|
_SendResponse(ID, KMID, Success, DataString)
|
|
EndProcedure
|
|
|
|
Procedure _StatForReadCallback(Success, DataString.s)
|
|
Protected ID, IsDelete, IsDir, FileID, Root
|
|
Protected Tag.s, KMID.s
|
|
|
|
If _PSR_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PSR_ID
|
|
Tag = _PSR_KMID
|
|
_PSR_ID = -1 : _PSR_KMID = ""
|
|
|
|
IsDelete = Bool(Left(Tag, 4) = "DEL:")
|
|
If IsDelete : KMID = Mid(Tag, 5) : Else : KMID = Tag : EndIf
|
|
|
|
If Not Success : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf
|
|
If Not ParseJSON(0, DataString) : _SendResponse(ID, KMID, #False, "Bad stat response") : ProcedureReturn : EndIf
|
|
|
|
Root = JSONValue(0)
|
|
IsDir = GetJSONInteger(GetJSONMember(Root, "is_dir"))
|
|
FileID = GetJSONInteger(GetJSONMember(Root, "id"))
|
|
FreeJSON(0)
|
|
|
|
If FileID <= 0 : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf
|
|
|
|
If IsDelete
|
|
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
|
|
_PW_ID = ID : _PW_KMID = KMID
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/delete", "id=" + FileID, @_WriteCallback())
|
|
Else
|
|
If IsDir : _SendResponse(ID, KMID, #False, "Is a directory") : ProcedureReturn : EndIf
|
|
_DoRead(ID, KMID, FileID)
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _ReadCallback(Success, DataString.s)
|
|
Protected ID
|
|
Protected KMID.s
|
|
If _PR_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PR_ID : KMID = _PR_KMID
|
|
_PR_ID = -1 : _PR_KMID = ""
|
|
_SendResponse(ID, KMID, Success, DataString)
|
|
EndProcedure
|
|
|
|
Procedure _WriteCallback(Success, DataString.s)
|
|
Protected ID, OK
|
|
Protected KMID.s
|
|
|
|
If _PW_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PW_ID : KMID = _PW_KMID
|
|
_PW_ID = -1 : _PW_KMID = ""
|
|
|
|
If Not Success : _SendResponse(ID, KMID, #False, "Request failed") : ProcedureReturn : EndIf
|
|
|
|
If ParseJSON(0, DataString)
|
|
OK = GetJSONBoolean(GetJSONMember(JSONValue(0), "success"))
|
|
FreeJSON(0)
|
|
_SendResponse(ID, KMID, OK, "")
|
|
Else
|
|
_SendResponse(ID, KMID, #False, "Bad server response")
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _ConfirmCallback(Result)
|
|
Protected ID
|
|
Protected KMID.s, BoolStr.s
|
|
|
|
If _PC_ID < 0 : ProcedureReturn : EndIf
|
|
ID = _PC_ID : KMID = _PC_KMID
|
|
_PC_ID = -1 : _PC_KMID = ""
|
|
|
|
BoolStr = "false"
|
|
If Result : BoolStr = "true" : EndIf
|
|
_SendResponse(ID, KMID, #True, BoolStr)
|
|
EndProcedure
|
|
|
|
;- Window event handlers
|
|
Procedure Handler_Resize()
|
|
If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf
|
|
ResizeGadget(Instances()\View, 0, 0, WindowWidth(Instances()\Window), WindowHeight(Instances()\Window))
|
|
EndProcedure
|
|
|
|
Procedure Handler_Close()
|
|
If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf
|
|
_Remove(Instances()\InstanceID)
|
|
EndProcedure
|
|
|
|
EndModule
|
|
|
|
; IDE Options = SpiderBasic 3.20 (Windows - x86)
|
|
; CursorPosition = 2
|
|
; Folding = BAAA9
|
|
; iOSAppOrientation = 0
|
|
; AndroidAppCode = 0
|
|
; AndroidAppOrientation = 0
|
|
; EnableXP
|
|
; DPIAware
|
|
; CompileSourceDirectory |