added share button + parent world link

This commit is contained in:
Leon van Kammen 2026-08-18 15:30:10 +02:00
parent 1f4b3d52ec
commit c81f37d2c3
23 changed files with 257 additions and 50 deletions

4
.env
View file

@ -1,6 +1,6 @@
# aliases
build(){ $(pwd)/src/util/xrforge.sh build; }
rebuild(){ $(pwd)/src/util/xrforge.sh rebuild; }
build(){ $(pwd)/src/util/build.sh build; }
rebuild(){ $(pwd)/src/util/build.sh rebuild; }
# forgejo server hook
#test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && {

View file

@ -54,6 +54,7 @@ app.opts = require('config')(app)
-- load system plugins (in specific order for admin panel)
app.plugin.global = app.addPlugin('plugin/global')
app.plugin.pubnix = app.addPlugin('plugin/pubnix')
app.plugin.diskscan = app.addPlugin('plugin/diskscan')
app.plugin.convert = app.addPlugin('plugin/convert')
app.plugin.moderation = app.addPlugin('plugin/moderation')
@ -76,7 +77,7 @@ print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging fro
-- init URL router
app.url['^/data'] = '/data.lua' -- setup custom file endpoint
app.get('^/$', 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.get('^/design[/]?$', util.pagerender('page/design.html'))

View file

@ -327,7 +327,7 @@ end
function util.stripPath(str)
return str:gsub("^./",""):gsub("^/",""):gsub("/$","")
return str:gsub("^/zip/",""):gsub("^./",""):gsub("^/",""):gsub("/$","")
end
-- WARNING: this is a destructive function which renders the redbean server inactive

View file

@ -151,7 +151,7 @@ form > table > tbody > tr > td:nth-child(3) {
.primary { color: #ff33Ff;}
.secondary { color: #7070ff;}
.btn a.primary{ background: #ff33Ff; color:#000}
.btn a.secondary{ background: #224; color: #83f; }
.btn a.secondary{ background: #000; color: #83f; }
.pad1{
padding:20px;

12
src/img/share.svg Normal file
View file

@ -0,0 +1,12 @@
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 9C19.6569 9 21 7.65685 21 6C21 4.34315 19.6569 3 18 3C16.3431 3 15 4.34315 15 6C15 6.12549 15.0077 6.24919 15.0227 6.37063L8.08261 9.84066C7.54305 9.32015 6.80891 9 6 9C4.34315 9 3 10.3431 3 12C3 13.6569 4.34315 15 6 15C6.80891 15 7.54305 14.6798 8.08261 14.1593L15.0227 17.6294C15.0077 17.7508 15 17.8745 15 18C15 19.6569 16.3431 21 18 21C19.6569 21 21 19.6569 21 18C21 16.3431 19.6569 15 18 15C17.1911 15 16.457 15.3202 15.9174 15.8407L8.97733 12.3706C8.99229 12.2492 9 12.1255 9 12C9 11.8745 8.99229 11.7508 8.97733 11.6294L15.9174 8.15934C16.457 8.67985 17.1911 9 18 9Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -1,3 +1,4 @@
<!-- https://aframe.io/aframe/examples/boilerplate/hello-world/ -->
<a-scene>
<a-assets>
<a-asset-item id="tree" src="tree2.glb"></a-asset-item>

View file

@ -5,7 +5,7 @@ ${header}
<div style="height:90px;" class="jumbotron">
<h1 style="display:none">${opts.title}</h1>
<br>
<center class="col3">${opts.plugin_global_subtitle}</center>
<center class="col3">${opts.plugin_global_subtitle} </center>
</div>
<hr style="margin-top:0"/>
]])
@ -21,20 +21,23 @@ end}
{if items then
for i=1,#items do
local item = items[i]
local icon = 'list-tree'
local thumbnail = ''
if #item.thumbnail > 0 then thumbnail = 'style="background-image: url(' .. EscapeFragment( util.stripPath(item.thumbnail) ) .. ');"' end
if item.view3D or item.portal3D == false then icon = 'play' end
print([[
<div class="tile">
<a href="]] .. util.stripPath(item.url) .. [[">
<div class="img" ]] .. util.stripPath(thumbnail) .. [[></div>
<div class="text">
<img src="img/]] .. icon .. [[.svg" class="icon ]] .. icon .. [["/>&nbsp;<a href="]] .. util.stripPath(item.url) .. [[">]] .. item.title .. [[</a>
</div>
</a>
</div>
]])
local visible = item.view2D == nil or (item.view2D ~= nil and item.view2D ~= false)
if visible then
local icon = 'list-tree'
local thumbnail = ''
if #item.thumbnail > 0 then thumbnail = 'style="background-image: url(' .. EscapeFragment( util.stripPath(item.thumbnail) ) .. ');"' end
if item.view3D or item.portal3D == false then icon = 'play' end
print([[
<div class="tile">
<a href="]] .. util.stripPath(item.url) .. [[">
<div class="img" ]] .. util.stripPath(thumbnail) .. [[></div>
<div class="text">
<img src="img/]] .. icon .. [[.svg" class="icon ]] .. icon .. [["/>&nbsp;<a href="]] .. util.stripPath(item.url) .. [[">]] .. item.title .. [[</a>
</div>
</a>
</div>
]])
end
end
end}
{if itemsremote then
@ -46,7 +49,7 @@ end}
if item.view3D or item.portal3D == false then icon = 'play' end
print([[
<div class="tile">
<a href="]] .. util.stripPath(item.url) .. [[">
<a href="]] .. util.stripPath(item.url) .. [[">
<div class="img" ]] .. util.stripPath(thumbnail) .. [[></div>
<div class="text">
<img src="img/]] .. icon .. [[.svg" class="icon ]] .. icon .. [["/>&nbsp;<a href="]] .. util.stripPath(item.url) .. [[">]] .. item.title .. [[</a>

View file

@ -30,17 +30,13 @@ return {
-- "parent = " .. util.stripPath(v.parent.path) .. " " ..
-- "matchpath = " .. util.stripPath(matchpath)
-- )
if match(v.path, matchpath) then -- show parent folder if any
local parent = app.cache.items[ v.parent.id ]
if v.portal then
trace("-- portal redirect")
redirect = v.portal.url
end
if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed
local root = app.cache.items[ app.opts.plugin_diskscan_dir ]
--res.items[ root.id ] = root
end
end
--if match(v.path, matchpath) then -- add parent folder if any
--res.items[ v.parent.id ] = v.parent
-- if match(v.parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed
-- local root = app.cache.items[ app.opts.plugin_diskscan_dir ]
-- res.items[ root.id ] = root
-- end
--end
-- add items from dir
if match(v.parent.path, matchpath) then
@ -49,6 +45,15 @@ return {
res.items[ v.id ] = v
res.parent = parent
end
if not isroot then
res.items[ v.parent.id ] = {
view2D = false,
url = v.parent.url,
title = "parent world",
name = "parent world",
thumbnail = '',
}
end
end
end
end
@ -57,7 +62,9 @@ return {
local data = util.merge({
url = req.param.url, -- query hint to render janus.url in iframe (index.html)
items = {},
parent=res.parent
referrer = isroot and app.opts.plugin_federation_parent_url or util.getURL(),
parent=res.parent,
template=res.template
},{ opts = app.opts })
local i = 0
foreach( res.items, function(k,v)
@ -70,10 +77,13 @@ return {
end
-- prioritize markup experience above binary 3D files as experience containers
if not data.item or (isMarkupExperience(v) and not isMarkupExperience(data.item)) then
-- update referrer url
data.referrer = util.getURL( v.view3D and v.parent.parent.url or v.parent.url )
data.item = v
data.title = data.item.title
data.parent = v.parent
data.url = app.opts.baseurl .. util.stripPath(v.url) -- because we are running in iframe
-- .. EscapeFragment( (v.url:match("#") and "&" or "#" ) .. 'janus.referrer=' .. EscapeFragment(v.referrer) )
if v.janusxr and (v.janusxr.jml or v.janusxr.aframe) then
-- what to do here? no generated portals?
data.janusxr = app.tpl( util.readfile(v.path), data )

View file

@ -17,7 +17,7 @@
<a href="https://nlnet.nl">NLNET</a><br/>
</td>
<td class="col3">
<a href="?view=3D">View from XR Device</a><br/>
<a href="${urlpath}?view=3D">View from XR Device</a><br/>
<a href="${opts.plugin_diskscan_dirname}">Content overview</a><br/>
<a href="about">About</a><br/>
</td>

View file

@ -37,6 +37,32 @@
{if true then
print( app.opts.plugin_global_topmenu_html:gsub("\n","&nbsp;") )
end}
{if template == "page/dir.html" then
print([[<div class="btn"><a class="border secondary" href="${urlpath}?view=3D">3D</a></div>&nbsp;]])
end}
{if template == "page/index.html" then
local sharedata = {
title = (item and item.title or opts.plugin_global_title)
}
print([[
<a id="share" href="mailto:friend@your.org?subject=${util.getURL()}&body=check%20this%20on%20your%20XR%20headset">
<img src="img/share.svg" class="icon share"/>
</a>
<script>
if( navigator.share ){
const sharebtn = document.querySelector('#share')
const title = "]] .. (item and item.title or opts.plugin_global_title) .. [["
sharebtn.setAttribute('href', '') // override mailto:
sharebtn.addEventListener('click', (e) => {
navigator.share({title, url: document.location.href})
e.stopPropagation()
e.preventDefault()
return false
})
}
</script>
]])
end}
</td>
</tr>
</table>

View file

@ -3,7 +3,7 @@ ${header}
<div></div>
</div>
<iframe border="0" frameborder="0" src="janusweb/index.html#janus.url=${url}" id="viewer"></iframe>
<iframe border="0" frameborder="0" src="janusweb/index.html#janus.url=${url}&janus.referrer=${referrer}" id="viewer"></iframe>
{if item and item.parent and item.parent.info then
print([[
@ -35,8 +35,9 @@ end}
url = document.location.origin + document.location.pathname + '?url=' + url
}
}catch(e){}
const isFileWithExtension = url.replace( new RegExp('./','g'),"").match("\\.")
if( !isFileWithExtension ){
const isFileWithExtension = url.replace( new RegExp('.*/','g'),"").match("\\.") ? true : false
//url = url.replace(/#.*/,'') + '#janus.referrer=' + encodeURIComponent(document.location.href)
if( !isFileWithExtension ){
window.history.replaceState( url, iframe.contentDocument.title, url );
}
setTimeout( () => {

View file

@ -34,8 +34,8 @@ end}
</assets>
{if item then
print([[
<room pos="0 0 0" skybox="true" showavatar="false">
<object id="${url}"/>
<room pos="0 0 0" skybox="true" showavatar="false">
<object id="${url}"/>
]])
else
local room = app.opts.plugin_janusxr_portalroom
@ -47,10 +47,14 @@ end}
end}
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
{if true then
function addPortal(id,x,v,url,i,z,thumb,rotation,scale)
function addPortal(id,x,v,url,i,z,thumb,rotation,scale,item)
print('\t<object js_id="' .. id .. '_' .. i .. '" pos="' .. x .. ' 0 ' .. z .. '" rotation="' .. rotation .. '">')
print('\t <link url="' .. url .. '" ' .. thumb .. ' title_col="0.4 0.37 0.4" pos="0 0 0.001" draw_glow="' .. (thumb:match("shader") and 'false' or 'true') .. '" scale="3 3 0.1" />')
print('\t <text text="' .. v.title .. '" pos="0 3.4 0" col="1 1 1" font_scale="false" scale="' .. scale .. '" />')
--TODO: fix janusweb paragraph for markdown urls
--if item.README ~= nil then
-- print('\t <paragraph js_id="README" url="' .. item.README .. '" width="600" height="900" pos="0 1 2" transparent="true"/>')
--end
print('\t</object>')
end
end}
@ -66,7 +70,7 @@ end}
end
if v.dir then url = url .. "/" end
if v.redirect then url = v.redirect end -- dont wrap standalone glb's in janusxr
addPortal('dir',center, v, url,i, -4, thumb, '0 0 0', '0.2 0.2 0.2')
addPortal('dir',center, v, url,i, -4, thumb, '0 0 0', '0.2 0.2 0.2', v)
end
end}
{if not item and itemsremote and #itemsremote > 0 then
@ -80,7 +84,7 @@ end}
end
if v.dir then url = url .. "/" end
if v.redirect then url = v.redirect end -- dont wrap standalone glb's in janusxr
addPortal('remote',center, v, url,i, 4, thumb, '0 180 0', '0.2 0.2 0.2')
addPortal('remote',center, v, url,i, 4, thumb, '0 180 0', '0.2 0.2 0.2',v)
end
print([[
<text js_id="locationremote" pos="0 6 5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${opts.plugin_global_remote_title}" emissive="0 0 0" roughness="0.3" metalness="0" />
@ -88,6 +92,7 @@ end}
end}
<object id="cube" rotation="0 0 0" position="0 0 0" scale="2 0.1 2"/> <!-- something to stand on -->
${janusxr_jml_deco}
${opts.plugin_janusxr_jml}
</room>
</fireboxroom>

View file

@ -3,6 +3,20 @@ local dir = require "opendirectory"
app.on('init', function()
print("scanning experiences (this make take a while..)")
app.opts.plugin_diskscan_dirname = app.opts.plugin_diskscan_dir:gsub("^[.]?[/]?","")
-- swap between internal / external version of diskscan_dir
if app.opts.plugin_diskscan_dir:match("^/zip/") then
if util.fileExist(app.opts.plugin_diskscan_dir) then
print(" serving " .. app.opts.plugin_diskscan_dir .. " from local disk")
app.opts.plugin_diskscan_dir = app.opts.plugin_diskscan_dir:gsub("^/zip/","")
app.set('app.opts', app.opts)
end
else
if not util.fileExist(app.opts.plugin_diskscan_dir) then
print(" serving " .. app.opts.plugin_diskscan_dir .. " from internal (executable)")
app.opts.plugin_diskscan_dir = '/zip/' .. util.stripPath(app.opts.plugin_diskscan_dir)
app.set('app.opts', app.opts)
end
end
app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir)
end)
@ -36,6 +50,7 @@ return {
path = scandir,
url = "/" .. util.stripPath(scandir),
name = scandir:gsub(".*/",""),
thumbnail = "",
title = scandir:gsub(".*/",""):gsub("-"," "),
file = { ino = scandir },
id = scandir

View file

@ -25,6 +25,7 @@ app.on('dir', function(opts)
title = url:gsub(".*//",""),
thumbnail = thumburl,
url = '/janusweb/index.html#janus.url=' .. EscapeFragment(url)
.. '&janus.referrer=' .. EscapeFragment( util.getURL(opts.urlpath ) )
})
end
end
@ -34,7 +35,7 @@ end)
local plugin = {
name = 'Federation',
tab = "<img src='img/link.svg' class='icon invert'/> Federation",
info = 'Add links to other (janus)XR(forges), and have them show up as extra portals in portalrooms',
info = 'Add links to other (janus)XR(forges), and have them show up as extra portals in portalrooms.<br><b>NOTE:</b> please leave at the <b>Parent World URL</b> non-empty, as it promotes interconnected ecosystems of experiences ♥',
opts = { -- TODO populate this based on xrforge-federation.json
{
key='app.opts.plugin_federation_strategy',
@ -43,6 +44,14 @@ local plugin = {
default="homepage",
values={always='always',homepage='only homepage'}
},
{
key='app.opts.plugin_federation_parent_url',
title='Parent World URL',
info="3D portal (entrypoint) in homepage",
default="https://www.janusxr.org",
placeholder="https://www.janusxr.org",
values=''
},
{
key='app.opts.plugin_federation_url_1',
title='Federated URL 1',
@ -54,7 +63,7 @@ local plugin = {
key='app.opts.plugin_federation_url_2',
title='Federated URL 2',
info="URL and/or Thumbnail URL (space separated)",
default="https://www.janusxr.org https://www.janusvr.com/images/logos/logotextoverlight.png",
default="https://aframe.io/aframe/examples/boilerplate/hello-world/ /myverse/janusxr_tutorial/AFRAME.jpg",
values=''
},
{

View file

@ -113,7 +113,7 @@ local plugin = {
},
middleware = {
homepage_2D_or_3D = function(req,res,next)
if (app.opts.plugin_global_homepage == '3D') or (req.param.view == '3D') or (req.param.url) then
if (req.url == '/' or req.url == '/index.html' and app.opts.plugin_global_homepage == '3D') or (req.param.view == '3D') or (req.param.url) then
res.template = 'page/index.html'
else
res.template = 'page/dir.html'

View file

@ -8,6 +8,15 @@ app.on('scan_experience', function(exp)
--print_r(exp)
end)
app.on('dir', function(opts)
local hash = GetPath()
if #app.opts.plugin_janusxr_random_objects > 0 and
#app.opts.plugin_janusxr_random_colors > 0 then
opts.data.janusxr_jml_deco = app.plugin.janusxr.getRandomDeco(7,50,hash,0,30) ..
app.plugin.janusxr.getRandomDeco(7,50,hash .. hash,30,30)
end
end)
app.on('scan_experience_file', function(exp)
local me = app.plugin.janusxr
local size = exp.file.attr:size()
@ -56,8 +65,7 @@ app.on('scan_experience_file', function(exp)
end
end)
app.on('set', function(k,v)
end)
app.on('set', function(k,v) end)
-- this registers the plugin UI
local rooms = {random="random room",room_plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
@ -118,6 +126,22 @@ local plugin = {
default="room_plane",
values={random="random room",room_plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
},
{
key='app.opts.plugin_janusxr_random_objects',
title='Random objects',
info="Random object id's in portal rooms",
default="capsule cone cube cylinder pyramid sphere torus",
placeholder="capsule cone cube cylinder pipe pyramid sphere torus",
values="",
},
{
key='app.opts.plugin_janusxr_random_colors',
title='Random colors',
info="Random object colors in portal rooms",
default="#ffffff #111122 #FF33FF #8833FF #7070FF #404070 #222244",
placeholder="#ffffff #111122 #FF33FF #8833FF #7070FF #404070 #222244",
values="",
},
{
key='app.opts.plugin_janusxr_scraper',
title='Scraper',
@ -141,6 +165,7 @@ local plugin = {
values=''
}
},
getRandomRoom = function()
local me = app.plugin.janusxr
local room= ''
@ -153,6 +178,52 @@ local plugin = {
end
end)
return room
end,
getRandomDeco = function(num_objects, radius, seed_string, y, scale)
num_objects = num_objects or 6
y = y or 0
radius = radius or 2 -- Configurable distance from (0, 0, 0)
seed_string = seed_string or 'xf2dxf2dxf3f23' -- Given hashstring
local JML = ''
local colors = util.split( app.opts.plugin_janusxr_random_colors, ' ')
local ids = util.split( app.opts.plugin_janusxr_random_objects, ' ')
local function hash_string_to_number(str)
local hash = 5381
for i = 1, #str do
local char = string.byte(str, i)
hash = (hash * 33 + char) % 2147483647
end
return hash
end
local index = {
color= hash_string_to_number(seed_string) % #colors,
id = hash_string_to_number(seed_string) % #ids
}
for i = 1, num_objects do
-- Calculate angle in radians to space objects evenly around a circle
local angle = (i - 1) * (2 * math.pi / num_objects)
-- Calculate X and Z positions along the circle radius
local x = radius * math.cos(angle)
local z = radius * math.sin(angle)
local y = y -- Height level
local selected_id = ids[ index.id+1]
local selected_color = colors[ index.color+1 ]
local pos_str = string.format("%.2f %g %.2f", x, y, z)
local xml_tag = string.format('<object id="%s" pos="%s" col="%s" rotation="0 45 0" scale="%s %s %s" roughness="0.1" metalness="0.5"/>',
selected_id, pos_str, selected_color, scale, scale, scale)
JML = JML .. xml_tag .. "\n"
-- increase indexes
index.id = (index.id + 1 ) % #ids
index.color = (index.color + 1 ) % #colors
end
return JML
end
}

View file

@ -12,6 +12,7 @@ app.on('scan_experience_file', function(exp)
if exp.path:match(v) then
print("✅ └─ plugin/markdown: detected " .. v)
exp.parent.info = '<div id="info">' .. markdown( util.readfile(exp.path) ) .. '</div>'
exp.parent.README = util.stripPath( exp.path )
end
end)
end)

View file

@ -25,6 +25,7 @@ app.on('dir', function(opts)
title = url:gsub(".*//",""),
thumbnail = thumburl,
url = '/janusweb/index.html#janus.url=' .. EscapeFragment(url)
.. '&janus.referrer=' .. EscapeFragment( util.getURL(opts.urlpath ) )
})
end
end

50
src/plugin/pubnix.lua Normal file
View file

@ -0,0 +1,50 @@
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)
end)
-- this registers the plugin UI
local plugin = {
name = "PubNIX",
info = '<b class="tag">disabled</b> Turn server into a <a href="https://tildeverse.org/" target="_blank">pubnix</a>: automatically scan userdirectories in /home/* for XR experiences, and add them as 3D portals.',
opts = {
{
key='app.opts.plugin_pubnix_enabled',
title='Add homedirs',
info='Scan for experiences in /home/*',
default = "enabled",
values={enabled="enabled",disabled="disabled"}
},
{
key='app.opts.plugin_pubnix_folder',
title='Require folder',
info='Require specific folder (/home/john/xrforge e.g.)',
default = "./xrforge",
values=''
},
{
key='app.opts.plugin_pubnix_thumbnail',
title='User thumbnail',
info='look for /home/john/xrforge/~john.jpg sidecarfile',
default = "enabled",
values={enabled="enabled",disabled="disabled"}
},
{
key='app.opts.plugin_pubnix_exclude',
title='Exclude pattern',
info='Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> for directories',
default = "",
placeholder = "admin root",
values=''
},
}
}
return plugin

View file

@ -25,6 +25,7 @@ app.on('dir', function(opts)
title = url:gsub(".*//",""),
thumbnail = thumburl,
url = '/janusweb/index.html#janus.url=' .. EscapeFragment(url)
.. '&janus.referrer=' .. EscapeFragment( util.getURL(opts.urlpath ) )
})
end
end

View file

@ -13,7 +13,7 @@ app.on('scan_experience_file', function(exp)
local hasHref = false
if ext == 'glb' then
-- detect XR Fragment exp https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
local tree = me.extract_glb_json( util.stripPath(exp.path) )
local tree = me.extract_glb_json( exp.path ) --util.stripPath(exp.path) )
hasHref = me.detectHrefInStruct(tree)
end
-- check sidecarfiles: https://xrfragment.org/#sidecar%20files

View file

@ -36,7 +36,7 @@ download(){
wget "https://github.com/coderofsalvation/janusweb/releases/download/v${version}-rc/janusweb-${version}.tar.gz"
}
test -d janusweb-${version} || tar -xvf janusweb-${version}.tar.gz
test -d src/janusweb || cp -r janusweb-${version} src/janusweb
test -d src/janusweb || cp -Lr janusweb-${version} src/janusweb
}
"$@"