refactored everything to plugins (todo: fix memoize tiles)

This commit is contained in:
Leon van Kammen 2026-07-23 18:17:55 +02:00
parent 1014554be6
commit d36dfa30b2
20 changed files with 241 additions and 229 deletions

2
run.sh
View file

@ -1,2 +1,4 @@
#!/bin/sh
source ./.env
rebuild
result/bin/xrforge.com server

View file

@ -23,21 +23,40 @@ app = require("soakbean") {
cmd={
help = {file="cmd/help.lua", info="display all possible flags"},
server = {file="cmd/server.lua", info="starts the webserver"},
createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"}
createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"},
generate = {file="cmd/generate.lua", info="generate www-folder with XR experiences"}
}
}
-- 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
-- load system plugins
app.plugin.moderation = require('plugin/moderation')
app.plugin.diskscan = require('plugin/diskscan')
app.plugin.manyfold = require('plugin/manyfold')
app.plugin.janusxr = require('plugin/janusxr')
-- ensure plugin config-defaults
foreach( app.plugin, function(name,plugin)
foreach( plugin.opts, function(k,opt)
local key = opt.key:gsub("app.opts.","")
if not app.opts[ key ] then
app.opts[ key ] = opt.default
end
end)
end)
-- set order preference (optional)
app.plugin[1] = app.plugin.diskscan
app.plugin[2] = app.plugin.moderation
app.plugin[3] = app.plugin.janusxr
app.plugin[4] = app.plugin.manyfold
app.runcmd(app) -- run clicmds
-- init URL router
@ -68,8 +87,9 @@ app --
app.on('init', function()
if assert(unix.fork()) == 0 then
app.plugin.diskscan.scan()
print_r(app.cache)
--os.execute("ls ; sleep 5")
app.scan()
print("ok")
--local ok, headers, body = Fetch('http://127.0.0.1:8080/test')
end

View file

@ -5,17 +5,10 @@ return function(app)
local config = { -- default
name = "XRForge",
baseurl="/",
title="My XRForge experiences",
title="Your XR Forge",
subtitle="Portable XR experiences for Phone, VRheadset & desktop.",
admin_title = "XRForge Admin",
admin_subtitle = "Manage your XR experiences here",
disk_dir = ".",
hide_keywords = "wip beta -v[0-9] customerX",
manyfold_instance_1 = "https://xrforge.isvery.ninja",
manyfold_instance_2 = "",
manyfold_instance_3 = "",
sync_interval = 30,
sync_strategy = "recent"
}
local configfile = "xrforge.lua"

View file

@ -31,17 +31,16 @@ local dir = {}
dir.scan = function(path)
local stat = unix.stat(path)
if stat == nil then
return ServeError(404,"does not exist")
else
if not unix.S_ISDIR( stat:mode() ) then return false end -- let redbean take care of serving static file
end
state = assert(unix.opendir(path))
return stat, state
if not stat or not unix.S_ISDIR( stat:mode() ) then
return nil,nil, "not a dir"
end -- let redbean take care of serving static file
state,err = assert(unix.opendir(path))
return stat, state, err
end
dir.foreach = function(path,cb)
local stat, state = dir.scan(path)
local stat, state, err = dir.scan(path)
if not err then
for name, kind, ino, off in state do
if name == "." then goto continue end
attr,err = unix.stat(path..'/'..name, unix.AT_SYMLINK_NOFOLLOW)
@ -49,7 +48,8 @@ dir.foreach = function(path,cb)
cb(path,stat,state,name,kind,ino,off,attr, path .. "/" ..name)
::continue::
end
return state
end
return state,err
end
dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbean)
@ -97,7 +97,11 @@ dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbea
Write('<th class=hide r>ctim\r\n')
Write('<tbody>\r\n')
local stat, state = dir.scan(path)
local stat, state, err = dir.scan(path)
if stat == nil then
return ServeError(404,"does not exist")
end
dir.foreach(path, function(path,stat,state,name,kind,ino,off,attr,fullpath)
Write('<tr>\r\n')
Write('<td>')

1
src/.lua/page Symbolic link
View file

@ -0,0 +1 @@
../page

View file

@ -3,7 +3,7 @@ return function(app,argv)
--
-- 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.)
-- 2. in xrforge.lua make sure to add (at the top): app.plugin.myplugin = require('xrforge.myplugin')
-- 3. profit!
--
@ -38,7 +38,10 @@ local plugin = {
default="bar",
values=""
}
}
},
myfunc = function()
-- call me via app.plugin.myplugin.myfunc()
end
}
return plugin

4
src/cmd/generate.lua Normal file
View file

@ -0,0 +1,4 @@
return function(app,argv)
app.plugin.diskscan.scan()
os.exit()
end

View file

@ -184,14 +184,14 @@ ul{
.menu{
text-align: right;
}
.menu,
.menu *{
white-space: nowrap;
}
.menu a {
padding-right:30px;
}
.menu br {
display:none
}
.grid .tile{ border-radius:5px; }
.grid .tile .text{ padding:3px 0px; }
.grid .tile .img{

View file

@ -33,9 +33,14 @@ body{
form table tr > td:nth-child(3) {
display:none;
}
.menu br {
display:initial !important;
.menu{
position:absolute;
right:20px;
}
.menu a{
padding-right:20px !important;
}
.menu a.xl{ display:none; }
}
.grid .tile .text{

View file

@ -10,145 +10,6 @@ ${notification}
<form method="POST">
<div class="tab">
<div class="label col2">
Local content
</div>
</div>
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td>
Directory
</td>
<td >
<input type="text" value="${opts.disk_dir}"></input>
</td>
<td class="col3">
Directory to scan for experiences (use '.' for current)
</td>
</tr>
</table>
</div>
<br/>
<br/>
<div class="tab">
<div class="label col2">
Hide content
</div>
</div>
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td>
Keywords
</td>
<td >
<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
</td>
</tr>
</table>
</div>
<br/>
<br/>
<div class="tab">
<div class="label col2">
Manyfold instances
</div>
</div>
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td>
Instance URL 1
</td>
<td >
<input type="text" name="opts.manyfold_instance_1" value="${opts.manyfold_instance_1}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Instance URL 2
</td>
<td>
<input type="text" name="opts.manyfold_instance_2" value="${opts.manyfold_instance_2}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Instance URL 3
</td>
<td>
<input type="text" name="opts.manyfold_instance_3" value="${opts.manyfold_instance_3}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Sync strategy
</td>
<td>
<select value="${opts.manyfold_sync_strategy}">
<option value="recent" default>only recent</option>
<option value="all+recent">everything + recent</option>
<option value="disabled">disabled</option>
</select>
</td>
<td class="col3">
Specify how manyfold instances should be synced.
</td>
</tr>
<tr>
<td>
Sync interval
</td>
<td>
<select value="${opts.manyfold_sync_interval}">
<option value="30">every 30 mins</option>
<option value="15" selected>every 15 mins</option>
<option value="5">every 5 minutes</option>
</select>
</td>
<td class="col3">
Specify how often manyfold instances should be checked for recent items.
</td>
</tr>
</table>
</div>
<br/>
<br/>
${plugins}
<div style="margin-left:187px">

View file

@ -14,11 +14,13 @@ function pluginInput(opt)
</td>
<td>
]]
local value = app.opts[ opt.key:gsub("^app.opts.","") ]
if type(opt.values) == 'table' then
html = html .. util.template('<select name="${key}" value="${value}">\n', {
key = opt.key,
value = app.opts[key]
value = value
})
print(opt.key:gsub("^app.opts.","") )
foreach( opt.values, function(k,v)
html = html .. util.template('<option value="${v}">${k}</option>\n',{
k=k,
@ -29,7 +31,7 @@ function pluginInput(opt)
else
html = html .. util.template('<input type="text" name="${key}" value="${value}"></input>',{
key = opt.key,
value = app.opts[key]
value = value
})
end
html = html .. util.template([[
@ -41,15 +43,18 @@ function pluginInput(opt)
return html
end
function pluginUI(name,plugin)
function pluginUI(seen)
return function(name,plugin)
if seen[ plugin.name ] then return end -- already processed
seen[ plugin.name ] = true
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
end
function saveData()
if GetMethod() == 'POST' then
@ -70,7 +75,8 @@ function saveData()
end
end
foreach( app.plugin, pluginUI )
local seen = {}
foreach( app.plugin, pluginUI(seen) )
saveData()
local html = util.template( LoadAsset("page/admin/index.html"),data)

View file

@ -1,5 +1,5 @@
<div class="tab">
<div class="label col2">${plugin.name} <b class="tag">plugin</b></div>
<div class="label col2">${plugin.name}</div>
</div>
<div class="margin1">
<table width="100%">

View file

@ -7,24 +7,7 @@ ${header}
<hr style="margin-top:0">
<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>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;Item 1
</div>
</div>
<div class="tile">
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;Item 2
</div>
</div>
<div class="tile">
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;Item 3
</div>
</div>
${tiles}
</div>
${footer}

View file

@ -1,12 +1,16 @@
local util = require("util")
local tile = require('page/partial/tile')
if not app.cache['experiences.html'] then
print("processing! " .. util.count(app.cache.items) .. " items")
app.cache['experiences.html'] = util.memoize(
function()
print("processing!")
local data = util.merge({foo='1'},{ opts = app.opts })
local html = util.template( LoadAsset("page/experiences.html"),app)
local data = util.merge({tiles=''},{ opts = app.opts })
foreach(app.cache.items, function(k,v)
data.tiles = data.tiles .. tile( util.flatten(v,"item") )
end)
local html = util.template( LoadAsset("page/experiences.html"),data)
return html
end,
60*5 -- re-run every 5 mins (otherwise return cache)

View file

@ -3,7 +3,7 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="${opts.baseurl}" />
<title>XRForge - selfsovereign interoperable XR experiences</title>
<link rel="stylesheet" href="css/xrforge.css"></link>
@ -25,10 +25,8 @@
</td>
<td class="menu">
<div class="margin1L">
<a href="/experiences">Experiences</a><br/>
<a href="/about">About</a><br/>
<a href="/howto">Howto</a><br/>
</div>
<!-- important: no spaces between anchor tags for older browsers without flexbox -->
<a href="/experiences">experiences</a><a href="/about">about</a><a class="xl" href="/howto">Howto</a></div>
</td>
</tr>
</table>

12
src/page/partial/tile.lua Normal file
View file

@ -0,0 +1,12 @@
local util = require('util')
return function(opts)
return util.template([[
<div class="tile">
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>
<div class="text">
<img src="img/folder.svg" class="icon"/>&nbsp;${item.title}
</div>
</div>
]],opts)
end

View file

@ -1,8 +1,18 @@
local dir = require "opendirectory"
local scan
scan = {
-- this registers the plugin UI /funcs
return {
name = "Content (from disk)",
info = 'XRForge can search in (unnested) folders for XR/image/video files.',
opts = {
{
key='app.opts.plugin_diskscan_dir',
title='Directory',
info="Directory to scan for experiences (use '.' for current) ",
default=".",
values=''
}
},
shouldHideFile = function(name)
return name == ".."
or name == "util" -- repository
@ -12,12 +22,15 @@ scan = {
end,
scan = function()
local me = app.plugin.diskscan
local folder = app.opts.plugin_diskscan_dir
-- 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 dref, derr= dir.foreach( folder, function(path,stat,state,name,kind,ino,off,attr,fullpath)
if (kind == unix.DT_DIR or kind == unix.DT_LNK) and not me.shouldHideFile(name) then
local experience = {
path = fullpath,
name = name,
title = name,
tag = {},
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
}
@ -25,18 +38,18 @@ scan = {
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
local fref, ferr = dir.foreach(fullpath, function(path,stat,state,name,kind,ino,off,attr, fullpath)
if kind ~= unix.DT_DIR and not me.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()
if not ferr then fref:close() end
end
end)
dref:close()
if not derr then dref:close() end
foreach( app.cache.items, function(k,v)
-- remove file attributes (which are closed already)
@ -46,5 +59,3 @@ scan = {
end)
end
}
return scan

View file

@ -11,12 +11,17 @@ 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")
table.insert(exp.tag, "janusxr")
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
table.insert( app.cache.items, exp)
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
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)
@ -31,8 +36,15 @@ 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.',
info = '<a href="https://coderofsalvation.github.io/janus-guide" target="_blank">JanusXR</a> connects XR experiences via XML portals (+AFRAME (geometry)markup also supported).',
opts = {
{
key='app.opts.plugin_janusxr_janusweb_endpoint',
title='backend URL',
info='Where janusweb (editor) can save its JML/assets while livecoding',
default="/janusxr",
values=''
},
{
key='app.opts.plugin_janusxr_wrap_image',
title='wrap images',

64
src/plugin/manyfold.lua Normal file
View file

@ -0,0 +1,64 @@
local util = require('util')
app.on('scan_experience', function(exp)
--print_r(exp)
end)
app.on('scan_experience_file', function(exp)
end)
app.on('set', function(k,v)
print("content was updated")
end)
-- this registers the plugin UI
local plugin = {
name = "Manyfold",
info = '',
opts = {
{
key='app.opts.plugin_manyfold_instance_1',
title='Instance 1 URL',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)',
default="https://xrforge.isvery.ninja",
values=''
},
{
key='app.opts.plugin_manyfold_instance_2',
title='Instance 2 URL',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)',
default="",
values=''
},
{
key='app.opts.plugin_manyfold_instance_3',
title='Instance 3 URL',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)',
default="",
values=''
},
{
key='app.opts.plugin_manyfold_sync_strategy',
title='Instance 3 URL',
info="Specify how content from manyfold instances should be fetched",
default="recent",
values={recent='only recent',all_recent='everything + recent', disabled='disabled'}
},
--{
-- key='app.opts.plugin_manyfold_sync_strategy',
-- title='Instance 3 URL',
-- info='Specify how content from manyfold instances should be fetched",
-- default="recent",
-- values={recent='only recent',all_recent='everything + recent', disabled='disabled'}
--},
--{
-- key='app.opts.plugin_manyfold_sync_interval',
-- title='Instance 3 URL',
-- info='Specify how often xrforge should check for recent items #serveronly'
-- default="recent",
-- values={recent='only recent',all_recent='everything + recent', disabled='disabled'}
--},
}
}
return plugin

29
src/plugin/moderation.lua Normal file
View file

@ -0,0 +1,29 @@
local util = require('util')
app.on('scan_experience', function(exp)
--print_r(exp)
end)
app.on('scan_experience_file', function(exp)
end)
app.on('set', function(k,v)
print("content was updated")
end)
-- this registers the plugin UI
local plugin = {
name = "Moderation",
info = 'Hide certain experiences from the listing (client work e.g.)',
opts = {
{
key='app.opts.plugin_moderation_mutekeywords',
title='Mute keywords',
info='Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed',
default = "wip beta -v[0-9] customerX",
values=''
},
}
}
return plugin