🔧 master: work in progress [might break]
This commit is contained in:
parent
9dd86155c6
commit
21152b128c
21 changed files with 363 additions and 66 deletions
3
.env
3
.env
|
|
@ -1,7 +1,8 @@
|
|||
# aliases
|
||||
build(){ $(pwd)/util/xrforge.sh build; }
|
||||
rebuild(){ $(pwd)/util/xrforge.sh rebuild; }
|
||||
|
||||
# server hook
|
||||
# forgejo server hook
|
||||
#test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && {
|
||||
# cp -r example/assets/* .
|
||||
#}
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,2 +1,5 @@
|
|||
experiences
|
||||
experienceA
|
||||
experienceB
|
||||
result
|
||||
src/v
|
||||
janusweb*
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -1,10 +1,3 @@
|
|||
|
||||
```
|
||||
$ source .env # not needed if your system has autoenv
|
||||
$ util/xrforge.sh build # fast: repeatable build
|
||||
$ nix-build # slow: reproducable nix build (for experts)
|
||||
```
|
||||
|
||||
NOTES:
|
||||
|
||||
```
|
||||
|
|
@ -44,6 +37,21 @@ curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=name' -H
|
|||
curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=recent' -H 'accept: application/vnd.manyfold.v0+json'
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
```bash
|
||||
$ source .env # not needed if your system has autoenv
|
||||
$ build # fast: repeatable build
|
||||
$ rebuild # faster: lua-repeatable build
|
||||
$ nix-build # slow: reproducable nix build (for experts)
|
||||
```
|
||||
|
||||
Then run with debug flag to see output of `trace()` calls
|
||||
```bash
|
||||
$ DEBUG=1 result/bin/xrforge.com
|
||||
```
|
||||
|
||||
|
||||
## Permacomputing Spirit
|
||||
|
||||
* developed on [Dillo](https://dillo-browser.github.io/) as lowest common denominator / compatibility
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
-H
|
||||
"Access-Control-Allow-Origin: *"
|
||||
Access-Control-Allow-Origin: *
|
||||
-H
|
||||
"Access-Control-Allow-Method: GET, POST, OPTIONS"
|
||||
Access-Control-Allow-Method: GET, POST, OPTIONS
|
||||
-H
|
||||
"Allow: GET, POST, OPTIONS"
|
||||
Allow: GET, POST, OPTIONS
|
||||
-D
|
||||
.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package.path = package.path .. ";.lua/?.lua"
|
|||
package.path = package.path .. ";src/.lua/?.lua"
|
||||
package.path = package.path .. ";middleware/?.lua"
|
||||
package.path = package.path .. ";cmd/?.lua"
|
||||
package.path = package.path .. ";plugin/?.lua"
|
||||
|
||||
print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on
|
||||
|
||||
|
|
@ -12,27 +13,30 @@ json = require "json"
|
|||
HidePath('/usr/share/zoneinfo/')
|
||||
HidePath('/usr/share/ssl/')
|
||||
|
||||
dir = require "opendirectory"
|
||||
|
||||
-- create
|
||||
app = require("soakbean") {
|
||||
bin = "./xrforge.com",
|
||||
bindir = arg[-1]:gsub("xrforge.com",""),
|
||||
cache = {},
|
||||
cache = { items = {} },
|
||||
plugin = {},
|
||||
opts = {},
|
||||
cmd={
|
||||
help = {file="cmd/help.lua", info="display all possible flags"},
|
||||
server = {file="cmd/server.lua", info="starts the webserver"}
|
||||
server = {file="cmd/server.lua", info="starts the webserver"},
|
||||
createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"}
|
||||
}
|
||||
}
|
||||
|
||||
-- api skeleton (implemented via soakbean's app.on(...))
|
||||
app.set = function(k,v) end
|
||||
app.init = function() end
|
||||
app.scan = require('scan').scan
|
||||
|
||||
-- load config
|
||||
app.opts = require('config')(app)
|
||||
|
||||
-- load plugins
|
||||
app.plugin.janusxr = require('plugin/janusxr')
|
||||
|
||||
app.runcmd(app) -- run clicmds
|
||||
|
||||
|
|
@ -62,16 +66,17 @@ app --
|
|||
.use( app.response() ) -- try serve app response (if any)
|
||||
.use( function(req,next) Route() end) -- fallback default redbean fileserver
|
||||
|
||||
function OnHttpRequest() app.run() end
|
||||
|
||||
app.on('init', function()
|
||||
if assert(unix.fork()) == 0 then
|
||||
os.execute("ls ; sleep 5")
|
||||
--os.execute("ls ; sleep 5")
|
||||
app.scan()
|
||||
print("ok")
|
||||
--local ok, headers, body = Fetch('http://127.0.0.1:8080/test')
|
||||
end
|
||||
end)
|
||||
|
||||
function OnHttpRequest() app.run() end
|
||||
|
||||
OnServerStart = function()
|
||||
app.init() -- this triggers init event
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,23 @@ return function(app)
|
|||
if k == 'app.opts' then
|
||||
app.opts = v
|
||||
app.opts.updated = os.time()
|
||||
local newconfig = "app.on('init', function()\n\tprint('config loaded')\nend)\n\nreturn " .. util.dump(app.opts)
|
||||
local newconfig = [[
|
||||
|
||||
-- TIP: run 'xrforge.com createplugin' to start writing plugins
|
||||
|
||||
app.on('init', function()
|
||||
print('config loaded')
|
||||
|
||||
local scan = app.scan
|
||||
app.scan = function()
|
||||
os.execute("echo scan before-hook")
|
||||
scan()
|
||||
os.execute("echo scan after-hook")
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
]] .. "\n\nreturn " .. util.dump(app.opts)
|
||||
newconfig = newconfig:gsub("^}$"," init = function() print('xrforge.lua config loaded') end\n}")
|
||||
util.writefile( app.bindir .. configfile, newconfig )
|
||||
print("stored xrforge.lua")
|
||||
|
|
|
|||
|
|
@ -46,9 +46,10 @@ dir.foreach = function(path,cb)
|
|||
if name == "." then goto continue end
|
||||
attr,err = unix.stat(path..'/'..name, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err then print("error: ".. name .. ": " ..err) end
|
||||
cb(path,stat,state,name,kind,ino,off,attr)
|
||||
cb(path,stat,state,name,kind,ino,off,attr, path .. "/" ..name)
|
||||
::continue::
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbean)
|
||||
|
|
@ -97,7 +98,7 @@ dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbea
|
|||
Write('<tbody>\r\n')
|
||||
|
||||
local stat, state = dir.scan(path)
|
||||
dir.foreach(path, function(path,stat,state,name,kind,ino,off,attr)
|
||||
dir.foreach(path, function(path,stat,state,name,kind,ino,off,attr,fullpath)
|
||||
Write('<tr>\r\n')
|
||||
Write('<td>')
|
||||
local link = EscapeHtml(name)
|
||||
|
|
|
|||
1
src/.lua/plugin
Symbolic link
1
src/.lua/plugin
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../plugin
|
||||
50
src/.lua/scan.lua
Normal file
50
src/.lua/scan.lua
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
local dir = require "opendirectory"
|
||||
|
||||
local scan
|
||||
scan = {
|
||||
|
||||
shouldHideFile = function(name)
|
||||
return name == ".."
|
||||
or name == "util" -- repository
|
||||
or name == ".git" -- files
|
||||
or name == "src" --
|
||||
or name == "result" --
|
||||
end,
|
||||
|
||||
scan = function()
|
||||
-- scan dirs
|
||||
local dref = dir.foreach( app.opts.disk_dir, function(path,stat,state,name,kind,ino,off,attr,fullpath)
|
||||
if kind == unix.DT_DIR and not scan.shouldHideFile(name) then
|
||||
local experience = {
|
||||
path = fullpath,
|
||||
name = name,
|
||||
tag = {},
|
||||
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
|
||||
}
|
||||
app.pub("scan_experience", nil, experience)
|
||||
print("ℹ️ ".. fullpath)
|
||||
|
||||
-- scan files
|
||||
local fref = dir.foreach(fullpath, function(path,stat,state,name,kind,ino,off,attr, fullpath)
|
||||
if kind ~= unix.DT_DIR and not scan.shouldHideFile(name) then
|
||||
experience.file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
|
||||
app.pub("scan_experience_file", nil, experience)
|
||||
local size = attr:size()
|
||||
trace("ℹ️ ".. fullpath)
|
||||
end
|
||||
end)
|
||||
fref:close()
|
||||
end
|
||||
end)
|
||||
dref:close()
|
||||
|
||||
foreach( app.cache.items, function(k,v)
|
||||
-- remove file attributes (which are closed already)
|
||||
v.file.stat = nil
|
||||
v.file.attr = nil
|
||||
v.file.state = nil
|
||||
end)
|
||||
end
|
||||
}
|
||||
|
||||
return scan
|
||||
|
|
@ -197,9 +197,9 @@ sb = {
|
|||
for k,v in pairs(argv) do
|
||||
app.opts[ v:gsub("=.*","") ] = v:gsub(".*=","")
|
||||
end
|
||||
for k,v in pairs(app.opts) do
|
||||
if app.cmd[k] then
|
||||
local file = app.cmd[k].file
|
||||
for k,v in pairs(argv) do
|
||||
if app.cmd[v] then
|
||||
local file = app.cmd[v].file
|
||||
return require( file:sub(0,-5) )(app,argv)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -69,6 +69,16 @@ util.template = function(content,data)
|
|||
return app.tpl( content, mdata )
|
||||
end
|
||||
|
||||
util.contains = function(tab,k_or_v)
|
||||
local result = false
|
||||
foreach( tab, function(k,v)
|
||||
if k == k_or_v or v == k_or_v then
|
||||
result = true
|
||||
end
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
util.memoize = function(fn, ttl_seconds)
|
||||
local cached_results = nil
|
||||
local last_executed = 0
|
||||
|
|
|
|||
48
src/cmd/createplugin.lua
Normal file
48
src/cmd/createplugin.lua
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
return function(app,argv)
|
||||
print( [[
|
||||
|
||||
--
|
||||
-- 1. put the code below in a file ('xrforge.myplugin.lua' e.g.)
|
||||
-- 2. make sure to require it at the top of xrforge.lua (require('xrforge.myplugin') e.g.)
|
||||
-- 3. profit!
|
||||
--
|
||||
|
||||
app.on('scan_experience', function(exp)
|
||||
print_r(exp)
|
||||
end)
|
||||
|
||||
app.on('scan_experience_file', function(exp)
|
||||
print_r(exp)
|
||||
end)
|
||||
|
||||
app.on('set', function(k,v)
|
||||
print("content was updated")
|
||||
end)
|
||||
|
||||
-- plugin UI
|
||||
local plugin = {
|
||||
name = "MyPlugin",
|
||||
info = "description / usage of your plugin goes here (html supported)",
|
||||
opts = {
|
||||
{
|
||||
key='app.opts.plugin_myplugin_foo',
|
||||
title='foo title',
|
||||
info='Enable or disable foo',
|
||||
default="yes",
|
||||
values={yes="yes",no="no",maybe="maaaaybe"}
|
||||
},
|
||||
{
|
||||
key='app.opts.plugin_myplugin_bar',
|
||||
title='foo title',
|
||||
info='Name your bar',
|
||||
default="bar",
|
||||
values=""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plugin
|
||||
|
||||
]] )
|
||||
os.exit()
|
||||
end
|
||||
|
|
@ -235,6 +235,15 @@ img.icon{ display:none; }
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
b.tag{
|
||||
display:inline-block;
|
||||
background: #77f;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* progressive enhancement quirks */
|
||||
|
||||
.spectrum{ display:none }
|
||||
|
|
|
|||
|
|
@ -39,11 +39,12 @@ body{
|
|||
}
|
||||
|
||||
.grid .tile .text{
|
||||
padding: 10px 20px 10px 20px !important;
|
||||
padding: 10px 20px 10px 15px !important;
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.grid .tile img.icon{
|
||||
transform: translate(0,5px);
|
||||
transform: translate(0,-3px);
|
||||
}
|
||||
|
||||
.clear{
|
||||
|
|
@ -69,11 +70,11 @@ img.icon{
|
|||
iframe#viewer{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: calc( 100vh - 59px);
|
||||
height: calc( 99.7vh - 59px);
|
||||
margin-top: 0px;
|
||||
background: #FFF;
|
||||
left: 0;
|
||||
top: 70px;
|
||||
top: 62px;
|
||||
}
|
||||
|
||||
/* telescopic text */
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ ${notification}
|
|||
Keywords
|
||||
</td>
|
||||
<td >
|
||||
<input type="text" value="${opts.hide_keywords}"></input>
|
||||
<input type="text" name="opts.hide_keywords" value="${opts.hide_keywords}"></input>
|
||||
</td>
|
||||
<td class="col3">
|
||||
Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed
|
||||
|
|
@ -145,13 +145,17 @@ ${notification}
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="margin-left:165px">
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
${plugins}
|
||||
|
||||
<div style="margin-left:187px">
|
||||
<div class="btn">
|
||||
<input type="submit" class="btn secondary" value="Save" style=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
${footer}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,57 @@
|
|||
local util = require("util")
|
||||
|
||||
local data = util.merge({notification=''},{ opts = app.opts })
|
||||
data.plugins = ""
|
||||
|
||||
function pluginInput(opt)
|
||||
local html = ""
|
||||
html = html .. [[
|
||||
<tr>
|
||||
<td>
|
||||
]]
|
||||
html = html .. opt.title
|
||||
html = html .. [[
|
||||
</td>
|
||||
<td>
|
||||
]]
|
||||
if type(opt.values) == 'table' then
|
||||
html = html .. util.template('<select name="${key}" value="${value}">\n', {
|
||||
key = opt.key,
|
||||
value = app.opts[key]
|
||||
})
|
||||
foreach( opt.values, function(k,v)
|
||||
html = html .. util.template('<option value="${v}">${k}</option>\n',{
|
||||
k=k,
|
||||
v=v
|
||||
})
|
||||
end)
|
||||
html = html .. "</select>\n"
|
||||
else
|
||||
html = html .. util.template('<input type="text" name="${key}" value="${value}"></input>',{
|
||||
key = opt.key,
|
||||
value = app.opts[key]
|
||||
})
|
||||
end
|
||||
html = html .. util.template([[
|
||||
</td>
|
||||
<td class="col3">${info}</td>
|
||||
</td>
|
||||
</tr>
|
||||
]],opt)
|
||||
return html
|
||||
end
|
||||
|
||||
function pluginUI(name,plugin)
|
||||
local plugindata = util.flatten(plugin,"plugin")
|
||||
plugindata.opts = ""
|
||||
foreach(plugin.opts, function(k,opt)
|
||||
plugindata.opts = plugindata.opts .. pluginInput(opt)
|
||||
end)
|
||||
print_r(plugindata)
|
||||
data.plugins = data.plugins .. util.template( LoadAsset('/page/admin/plugin.html'), plugindata)
|
||||
end
|
||||
|
||||
function saveData()
|
||||
if GetMethod() == 'POST' then
|
||||
local params = GetParams()
|
||||
local saveconfig = false
|
||||
|
|
@ -18,7 +68,10 @@ if GetMethod() == 'POST' then
|
|||
data.notification = '<div class="notification">no changes detected</div>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
foreach( app.plugin, pluginUI )
|
||||
saveData()
|
||||
local html = util.template( LoadAsset("page/admin/index.html"),data)
|
||||
|
||||
|
||||
|
|
|
|||
21
src/page/admin/plugin.html
Normal file
21
src/page/admin/plugin.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<div class="tab">
|
||||
<div class="label col2">${plugin.name} <b class="tag">plugin</b></div>
|
||||
</div>
|
||||
<div class="margin1">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<th width="165"/>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>${plugin.info}<br/><br/></td>
|
||||
</tr>
|
||||
${opts}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
|
@ -6,21 +6,6 @@ ${header}
|
|||
</div>
|
||||
<hr style="margin-top:0">
|
||||
|
||||
<div class="btn">
|
||||
<a href="#foo">button</a>
|
||||
</div>
|
||||
|
||||
<div class="btn">
|
||||
<a href="#foo" class="primary">button primary <img src="img/arrow-down.svg" class="icon"/></a>
|
||||
</div>
|
||||
|
||||
<div class="btn">
|
||||
<a href="https://foo.com" target="_blank" class="secondary">button secondary <img src="img/arrow-right.svg" class="icon"/></a>
|
||||
</div>
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
<div class="grid">
|
||||
<div class="tile">
|
||||
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
${header}
|
||||
|
||||
<iframe border="0" frameborder="0" src="https://web.janusxr.org" id="viewer"/>
|
||||
<iframe border="0" frameborder="0" src="/v/index.html#janus.url=/" id="viewer"/>
|
||||
|
||||
${footer}
|
||||
|
|
|
|||
67
src/plugin/janusxr.lua
Normal file
67
src/plugin/janusxr.lua
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
local util = require('util')
|
||||
|
||||
local pattern_fireboxroom = "[Ff][Ii][Rr][Ee][Bb][Oo][Xx][Rr][Oo][Oo][Mm][ ]?.*>"
|
||||
local pattern_title = "<[Tt][Ii][Tt][Ll][Ee]>(.*)<"
|
||||
|
||||
app.on('scan_experience', function(exp)
|
||||
--print_r(exp)
|
||||
end)
|
||||
|
||||
app.on('scan_experience_file', function(exp)
|
||||
local size = exp.file.attr:size()
|
||||
if exp.file.name:match(".glb") and not util.contains(exp.tag,"xrfragment") then
|
||||
print("✅ └─ plugin/janusxr: generating janusxr.xml")
|
||||
elseif size < 1000 then -- skip large binary files
|
||||
local data = util.readfile( exp.file.fullpath)
|
||||
if data:match(pattern_fireboxroom) then
|
||||
table.insert(exp.tag, "janusxr")
|
||||
local title = data:match(pattern_title)
|
||||
if title then exp.title = title:gsub("<.*","") end
|
||||
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
|
||||
table.insert( app.cache.items, exp)
|
||||
print("✅ └─ plugin/janusxr: " .. exp.file.name)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
app.on('set', function(k,v)
|
||||
print("content was updated")
|
||||
end)
|
||||
|
||||
-- this registers the plugin UI
|
||||
local plugin = {
|
||||
name = "JanusXR",
|
||||
info = '<a href="https://coderofsalvation.github.io/janus-guide" target="_blank">janusxr</a> allows composing (interconnected) 3D experiences (via portals) via its JML markup language. It also supports AFRAME (geometry)markup.',
|
||||
opts = {
|
||||
{
|
||||
key='app.opts.plugin_janusxr_wrap_image',
|
||||
title='wrap images',
|
||||
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed images into an XR experience',
|
||||
default="yes",
|
||||
values={yes="yes",no="no"}
|
||||
},
|
||||
{
|
||||
key='app.opts.plugin_janusxr_wrap_video',
|
||||
title='wrap videos',
|
||||
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed videos into an XR experience',
|
||||
default="yes",
|
||||
values={yes="yes",no="no"}
|
||||
},
|
||||
{
|
||||
key='app.opts.plugin_janusxr_wrap_aframe',
|
||||
title='wrap AFRAME',
|
||||
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed AFRAME markup into an XR experience',
|
||||
default="yes",
|
||||
values={yes="yes",no="no"}
|
||||
},
|
||||
{
|
||||
key='app.opts.plugin_janusxr_wrap_template',
|
||||
title='template',
|
||||
info='What kind of space should images/videos be rendered into?',
|
||||
default="random",
|
||||
values={random="random room",plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return plugin
|
||||
|
|
@ -19,6 +19,15 @@ download(){
|
|||
verify
|
||||
}
|
||||
|
||||
janusweb(){
|
||||
version=1.7.4
|
||||
test -f janusweb-${version}.tar.gz || {
|
||||
wget "https://github.com/jbaicoianu/janusweb/releases/download/v${version}/janusweb-${version}.tar.gz"
|
||||
tar -xvf janusweb-${version}.tar.gz
|
||||
mv janusweb-${version} src/v
|
||||
}
|
||||
}
|
||||
|
||||
"$@"
|
||||
}
|
||||
|
||||
|
|
@ -28,4 +37,9 @@ build(){
|
|||
zip -r ../result/bin/xrforge.com * .lua .init.lua .args
|
||||
}
|
||||
|
||||
rebuild(){
|
||||
cd src
|
||||
zip -r ../result/bin/xrforge.com .lua page plugin cmd css .init.lua .args
|
||||
}
|
||||
|
||||
"$@"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue