improved default popup for mobile + openbrush/manyfold centralized refactor

This commit is contained in:
Leon van Kammen 2026-09-15 20:41:30 +02:00
parent 86eaf1556f
commit 81b9d238a9
13 changed files with 521 additions and 188 deletions

View file

@ -31,6 +31,7 @@ app = require("soakbean") {
app.reset = function() app.reset = function()
app.cache = { items = {}, url = {}, shared = require('sharedcache'), dirs={} } app.cache = { items = {}, url = {}, shared = require('sharedcache'), dirs={} }
end end
app.reset()
app.addPlugin = function(file) app.addPlugin = function(file)
local p = require(file) local p = require(file)
@ -50,7 +51,8 @@ end
app.addExperience = function(experience, ino) app.addExperience = function(experience, ino)
if app.cache.items[ino] then -- handle duplicate symlinks, symlinks to symlinks e.g. if app.cache.items[ino] then -- handle duplicate symlinks, symlinks to symlinks e.g.
app.cache.items[ino .. ' ' .. experience.parent.id ] = experience local postfix = '_' .. ( experience.parent and experience.parent.id or tostring(os.time()) )
app.cache.items[ino .. postfix ] = experience
else else
app.cache.items[ino] = experience app.cache.items[ino] = experience
end end
@ -63,9 +65,7 @@ end
-- api skeleton (implemented via soakbean's app.on(...)) -- api skeleton (implemented via soakbean's app.on(...))
app.set = function(k,v) end app.set = function(k,v) end
app.init = function() app.init = function() end
app.reset()
end
-- load config -- load config
app.opts = require('config')(app) app.opts = require('config')(app)
@ -97,19 +97,16 @@ if unix.getpid() == 0 then unix.exit(0) end -- child-threads shall not pass beyo
app.runcmd(app) -- run clicmds app.runcmd(app) -- run clicmds
print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on
-- init URL router -- init URL router
app.url['^/data'] = '/data.lua' -- setup custom file endpoint app.url['^/data'] = '/data.lua' -- setup custom file endpoint
app.use( app.plugin.global.middleware.homepage_2D_or_3D ) app.use( app.plugin.global.middleware.homepage_2D_or_3D )
app.url['^/admin[/]?$'] = '/page/admin/index.lua'
app.use( require('middleware/onlyGET')({exclude={'^/admin'}}) ) -- we are staticwebgen-first server-last
app.get('^/design[/]?$', util.pagerender('page/design.html')) app.get('^/design[/]?$', util.pagerender('page/design.html'))
app.get('^/about[/]?$', util.pagerender('page/about.html')) app.get('^/about[/]?$', util.pagerender('page/about.html'))
app.post('^/save', function(req,res,next) -- setup inline POST endpoint app.post('^/save', function(req,res,next) -- setup inline POST endpoint
-- also .get(), .put(), .delete(), .options() -- also .get(), .put(), .delete(), .options()
app.cache = req.body -- middleware auto-decodes json app.cache = req.body -- middleware auto-decodes json
@ -125,7 +122,17 @@ app --
return user == app.opts.admin_user and pass == app.opts.admin_pass return user == app.opts.admin_user and pass == app.opts.admin_pass
end end
})) }))
.use( require("json").middleware() ) -- try plug'n'play json API middleware
app.url['^/admin[/]?$'] = '/page/admin/index.lua'
app.use( require('middleware/onlyGET')({exclude={'^/admin'}}) ) -- we are staticwebgen-first server-last
app.get('/admin/server/reset', function(req,res,next) -- easy way to reload/rescan xrforge
app.reset()
res.status(303)
res.header('Location', req.header['Referer']:gsub("?.*","") .. '?notify=Server%20is%20being%20reloaded.' )
res.send(req,res)
end)
app.use( require("json").middleware() ) -- try plug'n'play json API middleware
.use( require('page/dir').middleware ) -- directory browser .use( require('page/dir').middleware ) -- directory browser
.use( app.router( app.url ) ) -- try url router .use( app.router( app.url ) ) -- try url router
.use( app.response() ) -- try serve app response (if any) .use( app.response() ) -- try serve app response (if any)
@ -149,3 +156,10 @@ OnServerStart = function()
app.init() -- this triggers init event app.init() -- this triggers init event
end end
OnServerStop = function()
-- run the 'reset' cli-cmd equivalent (as childprocesses might leave
-- the terminal in a confused state)
io.write("\27[H\27[2J\27[3J\27[0m")
io.flush()
end

View file

@ -1,22 +1,76 @@
-- this is a helper/orchestrator for apicalls/syncs
--
-- implemented in openbrush.lua
local apiSync = { local apiSync = {
syncHashtags = function(opts) init = function(pluginname)
local synced = 0 local plugin = app.plugin[pluginname]
local tags = opts.tags local api = {}
if #tags == 0 then table.insert(tags,'') end api.pluginname = pluginname
foreach( tags, function(k,tag) if not plugin.apiCall or not plugin.fetch or not plugin.download then
local url = opts.apiCall( #tag > 0 and tag or nil) print("apiSync.lua: plugin " .. pluginname .. " does not implement the apiSync interface")
opts.body = nil -- erase previous url result os.exit()
print('🤞 fetch ' .. url)
local t = opts.fetch(url,opts)
if t then
synced = synced + opts.download(t,tag)
end
end, true)
if synced > 0 then
app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir)
end end
return synced api.apiCall = plugin.apiCall
api.fetch = plugin.fetch
api.download = plugin.download
api.syncHashtags = function(opts)
local synced = 0
local tags = opts.tags
if #tags == 0 then table.insert(tags,'') end
foreach( tags, function(k,tag)
local url = opts.apiCall( #tag > 0 and tag or nil)
opts.body = nil -- erase previous url result
print('🤞 fetch ' .. url)
local t = opts.fetch(url,opts)
if t then
synced = synced + opts.download(t,tag)
end
end, true)
if synced > 0 then
app.cache.url = {} -- delete page cache
app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir)
end
return synced
end
api.sync = function(api)
if #app.opts['plugin_' .. pluginname .. '_sync_URL'] > 0 and util.cmdExist('wget') then
local synced = api.syncHashtags({
tags = util.split( app.opts['plugin_' .. api.pluginname .. '_sync_hashtag'] or '', ' '),
apiCall = api.apiCall,
fetch = api.fetch,
download = api.download
})
end
end
-- runs sync in foreground
app.on('reset', function()
api.sync(api)
end)
-- runs sync in foreground
app.on('init', function()
foreach( app.plugin.openbrush.opts, function(k,opt)
if opt.key == 'app.opts.plugin_openbrush_sync_folder' then
opt.values = app.cache.dirs
end
end)
if util.contains(argv, 'generate') then api.sync(api) end
end)
-- runs sync background
app.on('scheduler', function(opts)
if util.contains(argv, 'server') then
if opts.interval == app.opts['plugin_' .. pluginname .. '_sync_when'] then
api.sync(api)
end
end
end)
return api
end end
} }

View file

@ -57,6 +57,7 @@ td,div,p{
input, input,
textarea, textarea,
button,
select{ select{
box-sizing: border-box; box-sizing: border-box;
margin: 0px 0px 10px 0px; margin: 0px 0px 10px 0px;
@ -91,6 +92,7 @@ input[type=submit]{
.btn a:active, .btn a:active,
.btn a:visited, .btn a:visited,
.btn a:link, .btn a:link,
button,
input[type=submit] { input[type=submit] {
font-size:15px; font-size:15px;
display: inline-block; display: inline-block;

View file

@ -3,7 +3,9 @@ ${header}
<br/> <br/>
<hr style="margin-top:0"> <hr style="margin-top:0">
${notification} {if #notification > 0 then
print('<div class="notification">' .. notification .. '</div>')
end}
<form method="POST"> <form method="POST">
<div class="tab-frame"> <div class="tab-frame">
@ -19,6 +21,9 @@ ${notification}
<div class="btn"> <div class="btn">
<input type="submit" class="btn secondary" value="Save" style=""/> <input type="submit" class="btn secondary" value="Save" style=""/>
</div> </div>
<div class="btn" style="float:right">
<a href="/admin/server/reset" class="btn primary">reload server</a>
</div>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,6 +1,6 @@
local util = require("util") local util = require("util")
local data = {notification=''} local data = {notification= GetParam('notify') or ''}
data.plugins = "" data.plugins = ""
data.tabs = {} data.tabs = {}
@ -87,9 +87,9 @@ function saveData()
end) end)
if saveconfig then if saveconfig then
app.set('app.opts', app.opts) app.set('app.opts', app.opts)
data.notification = '<div class="notification">updated</div>' data.notification = 'updated'
else else
data.notification = '<div class="notification">no changes detected</div>' data.notification = 'no changes detected'
end end
end end
end end

View file

@ -31,7 +31,21 @@
]]) ]])
end end
end} end}
<br/><br/> <br/>
{if true then
local btns = plugin.buttons or {}
if #btns > 0 then
for i,btn in pairs(btns) do
print([[
<div class="btn">
<a href="]] .. btn.href .. [[" class="btn primary">]] .. (btn.label or '') .. [[</a>
</div>
]])
end
print('<br/>')
end
end}
<br/>
</td> </td>
</tr> </tr>
${opts} ${opts}

View file

@ -99,7 +99,6 @@ return {
-- what to do here? no generated portals? -- what to do here? no generated portals?
data.janusxr = app.tpl( util.readfile(v.path), data ) data.janusxr = app.tpl( util.readfile(v.path), data )
end end
print_r(data.item)
end end
end end
end) end)

View file

@ -43,7 +43,6 @@ return {
end, end,
scan = function(scandir) scan = function(scandir)
app.reset() -- flush cache
local me = app.plugin.diskscan local me = app.plugin.diskscan
local seen = {symlinks={}} local seen = {symlinks={}}
-- ensure rootdir as experience -- ensure rootdir as experience

View file

@ -98,51 +98,60 @@ local plugin = {
title='Viewer instructions', title='Viewer instructions',
info='Default popup for 3D viewer instructions', info='Default popup for 3D viewer instructions',
default = [[ default = [[
<h1>Tips to navigate in XR</h1> <h1>Hi there! 👋</h1>
<table> Welcome to this XR experience.<br/>
<tr> <br/>
<td width="200"> <div class="btn script">
<b>AR/VR headset</b> <button class="btn secondary" onclick="document.querySelector('#tips').style.display = ''">Tips for navigating in XR</button>
</td> </div>
<td>
walk around / use right-longpinch (or thumbstick) for local teleports<br/> <div id="tips" style="display:none">
</td> <br/>
</tr> <table>
<tr> <tr>
<td> <td width="200">
<b>PhoneVR</b> <b>AR/VR headset</b>
</td> </td>
<td> <td>
use gaze or bluetooth/mouse to navigate walk around / use right-longpinch (or thumbstick) for local teleports<br/>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>PhoneAR</b> <b>PhoneVR</b>
</td> </td>
<td> <td>
walk around and tap screen to click use gaze or bluetooth/mouse to navigate
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>Mobile</b> <b>PhoneAR</b>
</td> </td>
<td> <td>
use the WASD-keys and mousedrag to navigate walk around and tap screen to click
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>Keyboard+Mouse</b> <b>Mobile</b>
</td> </td>
<td> <td>
use the WASD-keys and mousedrag to navigate use the WASD-keys and mousedrag to navigate
</td> </td>
</tr> </tr>
</table> <tr>
<td>
<b>Keyboard+Mouse</b>
</td>
<td>
use the WASD-keys and mousedrag to navigate
</td>
</tr>
</table>
<br/> <br/>
You've got this! You've got this!
</div>
<br/> <br/>
]], ]],
control='textarea', control='textarea',

View file

@ -27,11 +27,12 @@ local grid = {
local index = 0 local index = 0
for i, v in pairs(items) do for i, v in pairs(items) do
local url = v.redirect and v.redirect or v.url local url = v.redirect and v.redirect or v.url
local overlay = v.title:match('#overlay') and true or false
-- Position current item -- Position current item
local pos = { local pos = {
x = v.title:match("#overlay") and 0 or x * gridsize, x = overlay and 0 or x * gridsize,
y = 0, y = 0,
z = v.title:match("#overlay") and 0 or z * gridsize z = overlay and 0 or z * gridsize
} }
local posStr = pos.x .. ' ' .. pos.y .. ' ' .. pos.z local posStr = pos.x .. ' ' .. pos.y .. ' ' .. pos.z
local is_nested_room = false local is_nested_room = false
@ -41,24 +42,26 @@ local grid = {
else else
jml = jml .. ' <object id="' .. url .. '" pos="' .. posStr .. '"/>\n' jml = jml .. ' <object id="' .. url .. '" pos="' .. posStr .. '"/>\n'
end end
index = index + 1 if not overlay then
-- Move to next position in the spiral index = index + 1
if index < #items then -- Move to next position in the spiral
x = x + dx if index < #items then
z = z + dz x = x + dx
stepsTaken = stepsTaken + 1 z = z + dz
if stepsTaken == steps then stepsTaken = stepsTaken + 1
stepsTaken = 0 if stepsTaken == steps then
-- Rotate clockwise: right -> up -> left -> down stepsTaken = 0
local oldDx = dx -- Rotate clockwise: right -> up -> left -> down
dx = -dz local oldDx = dx
dz = oldDx dx = -dz
turns = turns + 1 dz = oldDx
-- Increase the length after every second turn turns = turns + 1
if turns % 2 == 0 then -- Increase the length after every second turn
steps = steps + 1 if turns % 2 == 0 then
end steps = steps + 1
end end
end
end
end end
end end
return jml return jml

View file

@ -1,64 +1,312 @@
local util = require('util') local util = require('util')
local apiSync = require('apiSync')
app.on('scan_experience', function(exp) --app.on('init', function()
--print_r(exp) -- app.plugin.manyfold.api = apiSync.init( 'manyfold' )
end) --end)
app.on('scan_experience_file', function(exp) local function scanTiltFile(exp)
end) if exp.path:lower():match(".tilt$") then
app.markViewable(exp)
end
end
local function scanForOpenBrush(exp)
local tags = app.plugin.manyfold.tags
if not tags then
tags = util.split( app.opts.plugin_manyfold_sync_hashtag, ' ')
table.insert( tags,"manyfold")
app.plugin.manyfold.tags = tags
end
foreach( tags, function(k,tag)
tag = tag:gsub("#","")
if exp.path:match('/' .. tag .. '/') then -- might not be precise enough
if not exp.parent.janusxr then
exp.parent.janusxr = {}
end
exp.parent.janusxr.room = app.opts.plugin_manyfold_wrap_room
exp.parent.portal_width = tonumber( app.opts.plugin_manyfold_portal_width )
exp.parent.layout = app.opts.plugin_manyfold_layout
end
end)
scanTiltFile(exp)
end
--app.on('scan_experience', scanForOpenBrush )
--app.on('scan_experience_file', scanForOpenBrush )
--app.on("scan_experience_janusxr", function(opts) scanForOpenBrush(opts.exp) end)
app.on('set', function(k,v) app.on('set', function(k,v)
end) end)
local sync_URL = app.opts_plugin_manyfold_sync_URL or ''
-- this registers the plugin UI -- this registers the plugin UI
local plugin = { local plugin = {
name = "Manyfold", name = "ManyFold",
tab = "<img src='img/link.svg' class='icon invert'/> Manyfold", infowide = true,
info = '', info = [[
<img src="img/manyfold.png" style="width:50%; max-width:233px; display:inline-block; float:left; margin-right:20px;"/>
<a href="https://manyfold.app/" target="_blank">OpenBrush</a> lets you paint in 3D space with virtual reality.<br>
XRForge's webviewer (<a href="https://github.com/jbaicoianu/janusweb">JanusWeb</a>) supports interconnecting OpenBrush projects via 3D portals or a grid.<br/>
OpenBrush projects are stored in a (selfhosted) <a href="https://icosa.gallery" target="_blank">icosa.gallery</a> instance, which can be configured below.<br><br>
<b>NOTE:</b> for collective virtualworld building use <b>hashtags in project-titles</b>. Each hashtag will become its own folder, which allows for <a href="/about#autocompose">crowdsourcing/combining scenes</a> designed in manyfold.
]],
--buttons = {
-- { label = "resync now!", href="/reset" }
--},
opts = { opts = {
{ {
key='app.opts.plugin_manyfold_instance_1', key='app.opts.plugin_manyfold_wrap_room',
title='Instance 1 URL', title='Wrap VR space',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)', info='render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room',
default="https://xrforge.isvery.ninja", default="room_plane",
values='' values= app.plugin.janusxr.opts[4].values -- reuse rooms from janusxr plugin
}, },
{ {
key='app.opts.plugin_manyfold_instance_2', key='app.opts.plugin_manyfold_layout',
title='Instance 2 URL', title='Layout assets',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)', info='NOTE: use #overlay in asset-title to skip layout (sky/floor/e.g.)',
default="", default="grid",
values='' values={portals="As portals", grid="As grid"}
}, },
{ {
key='app.opts.plugin_manyfold_instance_3', key='app.opts.plugin_manyfold_layout_grid_size',
title='Instance 3 URL', title='Grid size',
info='<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)', info='in square meters (only used for grid layout)',
default="", default="3",
placeholder="3",
values='' values=''
}, },
{ {
key='app.opts.plugin_manyfold_sync_strategy', key='app.opts.plugin_manyfold_sync_strategy',
title='Sync strategy', title='How to sync',
info="Specify how content from manyfold instances should be fetched", info='download assets locally (prevent linkrot)',
default="recent", default = "mirror",
values={recent='only recent',all_recent='everything + recent', disabled='disabled'} values={mirror="mirror locally"}
}, },
--{ {
-- key='app.opts.plugin_manyfold_sync_strategy', key='app.opts.plugin_manyfold_sync_when',
-- title='Instance 3 URL', title='Sync interval',
-- info='Specify how content from manyfold instances should be fetched", info='when to sync?',
-- default="recent", default = "boot",
-- values={recent='only recent',all_recent='everything + recent', disabled='disabled'} values={disabled="disabled", boot="on boot", hourly="hourly", daily="daily"}
--}, },
--{ {
-- key='app.opts.plugin_manyfold_sync_interval', key='app.opts.plugin_manyfold_sync_folder',
-- title='Instance 3 URL', title='Sync to folder',
-- info='Specify how often xrforge should check for recent items #serveronly' info='manyfold (icosa.gallery) assets will be saved here',
-- default="recent", placeholder = "manyfold",
-- values={recent='only recent',all_recent='everything + recent', disabled='disabled'} default = app.opts.plugin_diskscan_dir,
--}, values = {}
} },
{
key='app.opts.plugin_manyfold_sync_folder_thumbnail',
title='Thumbnail',
info='URL of Thumbnail for folder/portal',
placeholder = "img/manyfold.png",
default = "img/manyfold.png",
values=''
},
{
key='app.opts.plugin_manyfold_portal_width',
title='Portal spacing',
info='Spacing between portals (prevent thumbnail-overlap)',
placeholder = "4.2",
default = "4.2",
values=''
},
{
key='app.opts.plugin_manyfold_sync_URL',
title='Icosa gallery URL',
info='public/selfhosted <a href="https://github.com/icosa-foundation/icosa-gallery target="_blank">icosa.gallery</a> server-instance',
placeholder = "icosa.gallery",
default = "icosa.gallery",
values=''
},
{
key='app.opts.plugin_manyfold_sync_hashtag',
title='tags',
info=( sync_URL and 'space-separated tags will generate their own folders' or ''),
default = "#1 #xrforge",
placeholder = "#1 #xrforge",
values=''
},
{
key='app.opts.plugin_manyfold_sync_category',
title='category',
info=( sync_URL and 'space-separated, leave empty or see <a href="https://api.' .. sync_URL .. '/v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a>' or ''),
placeholder = "ART",
default = "",
values=''
},
{
key='app.opts.plugin_manyfold_sync_keywords',
title='keywords',
info=( sync_URL and 'space-separated, leave empty or see <a href="https://api.' .. sync_URL .. '/v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a>' or ''),
placeholder = "xrforge",
default = "",
values=''
},
{
key='app.opts.plugin_manyfold_sync_triangle_max',
title='Polygons maximum',
info=( sync_URL and 'performance-cap, leave empty or see <a href="https://api.' .. sync_URL .. '/v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a>' or ''),
placeholder = "10000",
default = "",
values=''
},
{
key='app.opts.plugin_manyfold_sync_license',
title='License',
info='filter assets on specific license',
default = "",
values={ [""]="", ALL_CC="CreateCommons (ALL)"}
},
{
key='app.opts.plugin_manyfold_sync_author',
title='Author name',
info='space-separated: filter assets on specific Author name',
placeholder='sarah',
default = "",
values=""
},
{
key='app.opts.plugin_manyfold_sync_authorid',
title='Author id',
info='space-separated: filter assets on specific Author id',
placeholder='9e8b98fbe',
default = "",
values=""
},
{
key='app.opts.plugin_manyfold_sync_maxitems',
title='Max latest items',
info='limit the total amount of result',
placeholder='3',
default = "3",
values=""
},
{
key='app.opts.plugin_manyfold_sync_collection',
title='In collection',
info='should assets be part of a collection?',
default = "disabled",
values={enabled="yes", disabled="no"}
}
},
-- expected by src/.lua/apiSync.lua
apiCall = function(tag)
return 'https://api.' .. app.opts.plugin_manyfold_sync_URL .. '/v1/assets?xrforge=' .. EscapeFragment(app.host)
.. '&xrforge_strategy=' .. EscapeFragment(app.opts.plugin_manyfold_sync_strategy)
.. '&time=' .. tostring( os.time() )
.. (tag and #tag > 0 and '&keywords=' .. EscapeFragment(tag) or '')
.. (#app.opts.plugin_manyfold_sync_category > 0 and '&category=' .. EscapeFragment(app.opts.plugin_manyfold_sync_category) or '')
.. (#app.opts.plugin_manyfold_sync_keywords > 0 and '&keywords=' .. EscapeFragment(app.opts.plugin_manyfold_sync_keywords) or '')
.. (#app.opts.plugin_manyfold_sync_triangle_max > 0 and '&triangleCountMax=' .. EscapeFragment(app.opts.plugin_manyfold_sync_triangle_max) or '')
.. (#app.opts.plugin_manyfold_sync_license > 0 and '&license=' .. app.opts.plugin_manyfold_sync_license or '' )
.. (app.opts.plugin_manyfold_sync_collection == 'enabled' and '&inCollection=true' or '' )
.. (#app.opts.plugin_manyfold_sync_maxitems > 0 and '&pageSize=' .. EscapeFragment(app.opts.plugin_manyfold_sync_maxitems) or '' )
-- TODO: filter AFTER the request
-- .. (#app.opts.plugin_manyfold_sync_author > 0 and '&authorName=' .. EscapeFragment(app.opts.plugin_manyfold_sync_author) or '')
-- .. (#app.opts.plugin_manyfold_sync_authorid > 0 and '&authorId=' .. EscapeFragment(app.opts.plugin_manyfold_sync_authorid) or '')
end,
getFolder = function(tag)
local folder = app.opts.plugin_manyfold_sync_folder
if tag then
folder = folder .. '/' .. util.hyphenate(tag)
end
return folder
end,
-- expected by src/.lua/apiSync.lua
fetch = function(url,opts)
opts = opts or {}
if not opts.body then
local ok, headers, body = Fetch( url )
if not ok then
return print("[manyfold.lua] error: could not fetch: " .. app.plugin.manyfold.apiCall(tag))
end
opts.body = body
end
return DecodeJson(opts.body)
end,
-- expected by src/.lua/apiSync.lua
download = function(t,tag)
local me = app.plugin.manyfold
local synced = 0
foreach( t.assets, function(k,asset)
if type(asset) == 'table' then
local dirname = '_' .. asset.assetId .. '-' .. util.hyphenate( asset.displayName)
local dirnameFull = me.getFolder(tag) .. '/' .. dirname
local jsonfile = dirnameFull .. '/asset.json'
local sync = true
local assetPrevious = false
local f = false
function preferFormat(f)
local found = false
foreach( asset.formats, function(k,v)
if v.formatType == f then
found = v
end
end)
return found
end
f = preferFormat('TILT')
if not f then f = preferFormat('GLTF2') end
if not f then f = preferFormat('GLTF1') end
if not f then f = preferFormat('OBJ') end
if f then
if not util.fileExist( dirnameFull ) then
os.execute("mkdir -p " .. dirnameFull)
end
if util.fileExist(jsonfile) then
assetPrevious = DecodeJson( util.readfile(jsonfile) )
if util.parseIsoDate(asset.updateTime) == util.parseIsoDate(assetPrevious.updateTime) then
sync = false -- already finished
end
end
if sync then
print('' .. asset.displayName .. ' (syncing) #manyfold')
util.writefile( jsonfile, EncodeJson(asset) )
local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","")
local thumbnailfile = ''
if asset.thumbnail then
thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'')
print('🤞 fetch ' .. asset.thumbnail.url)
os.execute('wget "' ..asset.thumbnail.url .. '" -O "' .. thumbnailfile .. '"')
end
print('🤞 fetch ' .. f.root.url)
os.execute('wget "' .. f.root.url .. '" -O "' .. rootfile .. '"')
if #f.resources > 0 then
foreach( f.resources, function(k,resource)
local file = dirnameFull .. '/' .. resource.url:gsub(".*/","")
print('🤞 fetch ' .. resource.url)
os.execute('wget "' .. resource.url .. '" -O "' .. file .. '"')
end)
end
synced = synced + 1
else
print(' ' .. asset.displayName .. ' (up to date) #manyfold')
end
else
print('[manyfold.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId)
end
end
end, true)
return synced
end
} }
if not util.cmdExist('wget') then
plugin.info = plugin.info .. '<br><br><b>NOTE:</b> wget command not installed..plugin is disabled'
end
return plugin return plugin

View file

@ -1,34 +1,8 @@
local util = require('util') local util = require('util')
local apiSync = require('apiSync') local apiSync = require('apiSync')
local syncIcosaGallery = function()
if #app.opts.plugin_openbrush_sync_URL > 0 then
local synced = apiSync.syncHashtags({
tags = util.split( app.opts.plugin_openbrush_sync_hashtag or '', ' '),
apiCall = app.plugin.openbrush.apiCall,
fetch = app.plugin.openbrush.fetch,
download = app.plugin.openbrush.download
})
end
end
app.on('init', function() app.on('init', function()
foreach( app.plugin.openbrush.opts, function(k,opt) app.plugin.openbrush.api = apiSync.init( 'openbrush' )
if opt.key == 'app.opts.plugin_openbrush_sync_folder' then
opt.values = app.cache.dirs
end
end)
if util.contains(argv, 'generate') then
syncIcosaGallery()
end
end)
app.on('scheduler', function(opts)
if util.contains(argv, 'server') then
if util.cmdExist('wget') and opts.interval == app.opts.plugin_openbrush_sync_when then
syncIcosaGallery()
end
end
end) end)
local function scanTiltFile(exp) local function scanTiltFile(exp)
@ -52,6 +26,7 @@ local function scanForOpenBrush(exp)
end end
exp.parent.janusxr.room = app.opts.plugin_openbrush_wrap_room exp.parent.janusxr.room = app.opts.plugin_openbrush_wrap_room
exp.parent.portal_width = tonumber( app.opts.plugin_openbrush_portal_width ) exp.parent.portal_width = tonumber( app.opts.plugin_openbrush_portal_width )
exp.parent.grid_size = tonumber( app.opts.plugin_openbrush_grid_size )
exp.parent.layout = app.opts.plugin_openbrush_layout exp.parent.layout = app.opts.plugin_openbrush_layout
end end
end) end)
@ -75,39 +50,42 @@ local plugin = {
info = [[ info = [[
<img src="img/openbrush.png" style="width:50%; max-width:233px; display:inline-block; float:left; margin-right:20px;"/> <img src="img/openbrush.png" style="width:50%; max-width:233px; display:inline-block; float:left; margin-right:20px;"/>
<a href="https://openbrush.app/" target="_blank">OpenBrush</a> lets you paint in 3D space with virtual reality.<br> <a href="https://openbrush.app/" target="_blank">OpenBrush</a> lets you paint in 3D space with virtual reality.<br>
XRForge's webviewer (<a href="https://github.com/jbaicoianu/janusweb">JanusWeb</a>) supports interconnecting OpenBrush projects via 3D portals.<br/> XRForge's webviewer (<a href="https://github.com/jbaicoianu/janusweb">JanusWeb</a>) supports interconnecting OpenBrush projects via 3D portals or a grid.<br/>
OpenBrush projects are stored in a (selfhosted) <a href="https://icosa.gallery" target="_blank">icosa.gallery</a> instance, which can be configured below.<br><br> OpenBrush projects are stored in a (selfhosted) <a href="https://icosa.gallery" target="_blank">icosa.gallery</a> instance, which can be configured below.<br><br>
<b>NOTE:</b> for collective virtualworld building use <b>hashtags in project-titles</b>. Each hashtag will become its own folder, which allows for <a href="/about#autocompose">crowdsourcing/combining scenes</a> designed in openbrush. <b>NOTE:</b> for collective virtualworld building use <b>hashtags in project-titles</b>. Each hashtag will become its own folder, which allows for <a href="/about#autocompose">crowdsourcing/combining scenes</a> designed in openbrush.
]], ]],
--buttons = {
-- { label = "resync now!", href="/reset" }
--},
opts = { opts = {
{ {
key='app.opts.plugin_openbrush_wrap_room', key='app.opts.plugin_openbrush_wrap_room',
title='Wrap VR space', title='Wrap VR space',
info='render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room', info='render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room',
default="room_plane", default="",
values= app.plugin.janusxr.opts[4].values -- reuse rooms from janusxr plugin values= app.plugin.janusxr.opts[4].values -- reuse rooms from janusxr plugin
}, },
{ {
key='app.opts.plugin_openbrush_layout', key='app.opts.plugin_openbrush_layout',
title='Layout assets', title='Layout assets',
info='NOTE: use #overlay in asset-title to skip layout (sky/floor/e.g.)', info='NOTE: use #overlay in asset-title to skip layout (sky/floor/e.g.)',
default="portals", default="grid",
values={portals="As portals", grid="As grid"} values={portals="As portals", grid="As grid"}
}, },
{ {
key='app.opts.plugin_openbrush_layout_grid_size', key='app.opts.plugin_openbrush_layout_grid_size',
title='Grid size', title='Grid size',
info='in square meters (unused for non-grid layout)', info='in square meters (only used for grid layout)',
default="3", default="75",
placeholder="3", placeholder="75",
values='' values=''
}, },
{ {
key='app.opts.plugin_openbrush_sync_strategy', key='app.opts.plugin_openbrush_sync_strategy',
title='How to sync', title='How to sync',
info='mirroring = most resilient; references = instant updates', info='download assets locally (prevent linkrot)',
default = "mirror", default = "mirror",
values={mirror="mirror locally",reference="reference remote URL"} values={mirror="mirror locally"}
}, },
{ {
key='app.opts.plugin_openbrush_sync_when', key='app.opts.plugin_openbrush_sync_when',
@ -116,13 +94,6 @@ local plugin = {
default = "boot", default = "boot",
values={disabled="disabled", boot="on boot", hourly="hourly", daily="daily"} values={disabled="disabled", boot="on boot", hourly="hourly", daily="daily"}
}, },
{
key='app.opts.plugin_openbrush_sync_bruteforce',
title='Sync anyways',
info='ignore timestamps',
default = "enabled",
values={disabled="disabled", enabled="enabled"}
},
{ {
key='app.opts.plugin_openbrush_sync_folder', key='app.opts.plugin_openbrush_sync_folder',
title='Sync to folder', title='Sync to folder',
@ -142,7 +113,7 @@ local plugin = {
{ {
key='app.opts.plugin_openbrush_portal_width', key='app.opts.plugin_openbrush_portal_width',
title='Portal spacing', title='Portal spacing',
info='Adjust spacing between portals (prevent thumbnail-overlap)', info='Spacing between portals (prevent thumbnail-overlap)',
placeholder = "4.2", placeholder = "4.2",
default = "4.2", default = "4.2",
values='' values=''
@ -159,8 +130,8 @@ local plugin = {
key='app.opts.plugin_openbrush_sync_hashtag', key='app.opts.plugin_openbrush_sync_hashtag',
title='tags', title='tags',
info=( sync_URL and 'space-separated tags will generate their own folders' or ''), info=( sync_URL and 'space-separated tags will generate their own folders' or ''),
default = "#1 #xrforge", default = "#xrforge",
placeholder = "#1 #xrforge", placeholder = "#xrforge",
values='' values=''
}, },
{ {
@ -187,6 +158,13 @@ local plugin = {
default = "", default = "",
values='' values=''
}, },
{
key='app.opts.plugin_openbrush_sync_license',
title='License',
info='filter assets on specific license',
default = "",
values={ [""]="", ALL_CC="CreateCommons (ALL)"}
},
{ {
key='app.opts.plugin_openbrush_sync_author', key='app.opts.plugin_openbrush_sync_author',
title='Author name', title='Author name',
@ -220,14 +198,16 @@ local plugin = {
} }
}, },
-- expected by src/.lua/apiSync.lua
apiCall = function(tag) apiCall = function(tag)
return 'https://api.' .. app.opts.plugin_openbrush_sync_URL .. '/v1/assets?xrforge=' .. EscapeFragment(app.host) return 'https://api.' .. app.opts.plugin_openbrush_sync_URL .. '/v1/assets?xrforge=' .. EscapeFragment(app.host)
.. '&xrforge_strategy=' .. EscapeFragment(app.opts.plugin_openbrush_sync_strategy) .. '&xrforge_strategy=' .. EscapeFragment(app.opts.plugin_openbrush_sync_strategy)
.. '&time=' .. tostring( os.time() )
.. (tag and #tag > 0 and '&keywords=' .. EscapeFragment(tag) or '') .. (tag and #tag > 0 and '&keywords=' .. EscapeFragment(tag) or '')
.. (#app.opts.plugin_openbrush_sync_category > 0 and '&category=' .. EscapeFragment(app.opts.plugin_openbrush_sync_category) or '') .. (#app.opts.plugin_openbrush_sync_category > 0 and '&category=' .. EscapeFragment(app.opts.plugin_openbrush_sync_category) or '')
.. (#app.opts.plugin_openbrush_sync_keywords > 0 and '&keywords=' .. EscapeFragment(app.opts.plugin_openbrush_sync_keywords) or '') .. (#app.opts.plugin_openbrush_sync_keywords > 0 and '&keywords=' .. EscapeFragment(app.opts.plugin_openbrush_sync_keywords) or '')
.. (#app.opts.plugin_openbrush_sync_triangle_max > 0 and '&triangleCountMax=' .. EscapeFragment(app.opts.plugin_openbrush_sync_triangle_max) or '') .. (#app.opts.plugin_openbrush_sync_triangle_max > 0 and '&triangleCountMax=' .. EscapeFragment(app.opts.plugin_openbrush_sync_triangle_max) or '')
.. (app.opts.plugin_openbrush_sync_strategy == 'mirror' and '&license=ALL_CC' or '' ) .. (#app.opts.plugin_openbrush_sync_license > 0 and '&license=' .. app.opts.plugin_openbrush_sync_license or '' )
.. (app.opts.plugin_openbrush_sync_collection == 'enabled' and '&inCollection=true' or '' ) .. (app.opts.plugin_openbrush_sync_collection == 'enabled' and '&inCollection=true' or '' )
.. (#app.opts.plugin_openbrush_sync_maxitems > 0 and '&pageSize=' .. EscapeFragment(app.opts.plugin_openbrush_sync_maxitems) or '' ) .. (#app.opts.plugin_openbrush_sync_maxitems > 0 and '&pageSize=' .. EscapeFragment(app.opts.plugin_openbrush_sync_maxitems) or '' )
-- TODO: filter AFTER the request -- TODO: filter AFTER the request
@ -243,6 +223,7 @@ local plugin = {
return folder return folder
end, end,
-- expected by src/.lua/apiSync.lua
fetch = function(url,opts) fetch = function(url,opts)
opts = opts or {} opts = opts or {}
if not opts.body then if not opts.body then
@ -255,6 +236,7 @@ local plugin = {
return DecodeJson(opts.body) return DecodeJson(opts.body)
end, end,
-- expected by src/.lua/apiSync.lua
download = function(t,tag) download = function(t,tag)
local me = app.plugin.openbrush local me = app.plugin.openbrush
local synced = 0 local synced = 0
@ -292,11 +274,13 @@ local plugin = {
sync = false -- already finished sync = false -- already finished
end end
end end
if sync or app.opts.plugin_openbrush_sync_bruteforce == 'enabled' then if sync then
print('' .. asset.displayName .. ' (syncing) #openbrush')
util.writefile( jsonfile, EncodeJson(asset) ) util.writefile( jsonfile, EncodeJson(asset) )
local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","") local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","")
local thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'') local thumbnailfile = ''
if app.opts.plugin_openbrush_layout == 'portals' then if asset.thumbnail and asset.thumbnail.url then
thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'')
print('🤞 fetch ' .. asset.thumbnail.url) print('🤞 fetch ' .. asset.thumbnail.url)
os.execute('wget "' ..asset.thumbnail.url .. '" -O "' .. thumbnailfile .. '"') os.execute('wget "' ..asset.thumbnail.url .. '" -O "' .. thumbnailfile .. '"')
end end
@ -311,7 +295,7 @@ local plugin = {
end end
synced = synced + 1 synced = synced + 1
else else
print('[openbrush.lua] ' .. asset.displayName .. ' is up to date') print(' ' .. asset.displayName .. ' (up to date) #openbrush')
end end
else else
print('[openbrush.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId) print('[openbrush.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId)

View file

@ -47,6 +47,8 @@ return {
if pid ~= nil then if pid ~= nil then
unix.kill( pid, unix.SIGTERM) unix.kill( pid, unix.SIGTERM)
end end
print("gracefully stopping child-processes")
unix.wait()
unix.exit() unix.exit()
end) end)
end end