mml bugfix + lovr-packager

This commit is contained in:
Leon van Kammen 2026-02-26 22:40:23 +01:00
parent f446266a4b
commit f82fcb24f9
15 changed files with 263 additions and 33 deletions

View file

@ -1,5 +1,8 @@
# XRForge
XRForge is a pre-configured [Manyfold](https://manyfold.app) container (reproducably via [nix](https://nixos.org) dockertools).
It also contains some extra's, to better fit an XR audience & enable community libraries.
> Self-sovereign **XR Experiences** for teams & organisations, powered by FOSS ♥
<img src="manyfold/usr/src/app/public/assets/xrh-full.svg" height="35" style="height:35px;display:inline-block"/>

View file

@ -9,13 +9,13 @@ test -n "$GODOT_VERSION" || export GODOT_VERSION=4.4.1-stable
test -n "$IMPORT_INSTANCES" || export IMPORT_INSTANCES=1
test -n "$SERVER_CORS" || export SERVER_CORS=http://localhost:5577
test -n "$SERVER_JANUS" || export SERVER_JANUS=http://localhost:5566
test -n "$HTTPS_ONLY" || unsafe=1 && export HTTPS_ONLY=disabled
test -n "$HTTPS_ONLY" || unsafe=1
test -n "$SECRET_KEY_BASE" || unsafe=1 && export SECRET_KEY_BASE=j1gf2cj3gfcjhf2j34298kjk2j3h4k
test -n "$SUDO_RUN_UNSAFELY" || unsafe=1 && export SUDO_RUN_UNSAFELY=enabled
test -n "$DATABASE_ADAPTER" || export DATABASE_ADAPTER=sqlite3
test -n "$MULTIUSER" || export MULTIUSER=enabled
test -n "$FEDERATE_SERVERS" || export FEDERATE_SERVERS='"*"'
test -n "$PUBLIC_HOSTNAME" || export PUBLIC_HOSTNAME=localhost
test -n "$PUBLIC_HOSTNAME" #|| export PUBLIC_HOSTNAME=localhost
test -n "$CADDY" && {
export HTTPS_ONLY=enabled
@ -179,9 +179,11 @@ set_upload_path(){
}
mount_dir(){
mkdir /usr/src/app/public/lib
find /mnt -type d -mindepth 1 -maxdepth 1 | grep -vE '(janusweb|view)' | while read dir; do
echocolor "[$APPNAME]" "mounting $dir as library"
add_lib_to_db "$dir"
ln -s "$dir" /usr/src/app/public/lib/.
done
}
@ -243,7 +245,8 @@ set_homepage(){
rename_app(){
echocolor "[$APPNAME]" "renaming manyfold to $APPNAME"
sed -i 's/title: Manyfold/title: '$APPNAME'/g' /usr/src/app/config/locales/*.yml
sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a>, <a href="https://github.com/jbaicoianu/janusweb" target="_blank">JanusWeb</a> and <a href="https://nixos.org" target="_blank">NIX</a>|g' /usr/src/app/config/locales/*.yml
sed -i 's|powered_by_html:.*|powered_by_html: \|\n <small>Opensource-powered by <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a>, <a href="https://xrhf.isvery.ninja">XR Hypermedia Federation</a>, <a href="https://github.com/jbaicoianu/janusweb" target="_blank">JanusWeb</a>, <a href="https://nixos.org" target="_blank">NIX</a> and <a href="https://nlnet.nl" target="_blank">NLnet</a></small>|g' /usr/src/app/config/locales/*.yml
sed -i 's| by_html:.*| by_html: \|\n XRForge is a <u tabindex="0">radically <span>openource platform. A sysadmin can selfhost this platform right now!<br><br><img src="assets/xrforge_term.svg" style="border-radius:7px; border-radius 7px; width 100%; max-width 450px; margin-bottom 40px;"/><br><br>This </span></u> opensource platform allows organisations to run on intranets or federate with others.|g' /usr/src/app/config/locales/*.yml
echocolor "[$APPNAME]" "renaming 'model' to 'experience'"
for dir in /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml; do
sed -i 's|Models|Experiences|g' "$dir"
@ -253,6 +256,9 @@ rename_app(){
done
sed -i 's|File(s) uploaded succesfully|File(s) uploaded succesfully. The experience is now being (re)generated, please be patient and check back later|g' /usr/src/app/config/locales/models/en.yml
sed -i 's|File(s) uploaded succesfully|File(s) uploaded succesfully. The experience is now being (re)generated, please be patient and check back later|g' /usr/src/app/config/locales/model_files/en.yml
# link instead of duplicate models/collections immersive overviews
ln -s /usr/src/app/app/views/models/_janusroom.html.erb /usr/src/app/app/views/collections/.
}
federate_servers(){
@ -387,6 +393,6 @@ usage(){
exit 0
}
test "$unsafe" = 1 && echocolor "[WARNING]" "default env-vars SECRET_KEY_BASE or SUDO_RUN_UNSAFELY are used. Please check: https://codeberg.org/coderofsalvation/xrforge/src/branch/master/manyfold" && echo
test "$unsafe" = 1 && echocolor "[WARNING]" "default env-vars SECRET_KEY_BASE or SUDO_RUN_UNSAFELY are used. Please check: https://codeberg.org/coderofsalvation/xrforge/src/branch/master/manyfold" 1>&2 && echo
test -n "$1" && "$@"
test -n "$1" || usage

View file

@ -49,13 +49,14 @@ begin
# Iterate through the images array
gltf['images'].each_with_index do |img, i|
# move file to modeldirectory (but dont overwrite if user overwrite it)
# use matching texture in original modeldirectory (if generated via extract-textures shellscript)
imgExts = ["jpg","png"]
imgExts.each do |imgExt|
new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}.#{imgExt}"
if File.exist?(new_filename)
XRForge.log("🤔 detected #{File.basename(new_filename)}",logfile)
XRForge.log("✅ importing #{File.basename(new_filename)} -> #{resource['path']}",logfile)
# here we update the glTF JSON with the matching texture
img['uri'] = new_filename # NOTE: editing uri will cause assimp to drop image['name'] when exporting :/
update = true
end
@ -64,6 +65,7 @@ begin
if update
File.write( gltf_path, JSON.pretty_generate(gltf) )
XRForge.log("✅ writing #{resource['path']}",logfile)
# convert scratch-model (with updated textures) to actual model
system("assimp export #{gltf_path} #{resource['path']} --embed-textures")
end
end

View file

@ -6,5 +6,7 @@ echo "[package_lovr_zip.sh] zipping lovr.zip"
# overwrite empty lovr template project-zip with given URL
test -n "$LOVR_TEMPLATE_ZIP" && timeout 50 wget "$LOVR_TEMPLATE_ZIP" -O ~/templates/template_lovr.zip
ln -s $(echo *.glb *.obj *.dae | awk '{print $1}') scene.glb
cp ~/templates/template_lovr.zip .xrforge/lovr.zip
zip .xrforge/lovr.zip $(find . -maxdepth 1 -type f)
zip .xrforge/lovr.zip $(find -L . -maxdepth 1 -type f)
test -f scene.glb && rm scene.glb

View file

@ -43,7 +43,7 @@ begin
# https://viewer.mml.io/main/v1/?url=https%3A%2F%2Ffoo.org%2Fbar.mml
mml = <<~MML
<m-model src="#{federate_drive_host}/#{model_file.gsub("#","%23")}" anim-loop="true" anim-enabled="true"></m-model>
<m-model src="../#{model_file.gsub("#","%23")}" anim-loop="true" anim-enabled="true"></m-model>
MML
File.write('.xrforge/scene.mml', mml)

View file

@ -108,12 +108,6 @@ class Components::ModelCard < Components::Base
div class: "col" do
#open_button
#whitespace
if ! @model.tags.where(name: "singleuser" ).any?
a alt: "start a meeting at this location", href: "/view/index.html?profile=default#janus.url="+model_url(@model), target: "_blank", class: "btn btn-secondary btn-sm" do
i class: "bi bi-telephone"
img src: "/assets/janusxr.svg", style: "width: 16px; margin-left: 7px; transform: translate(0px,-2px);"
end
end
whitespace
status_badges @model
end

View file

@ -16,7 +16,7 @@ class Components::PreviewFrame < Components::Base
def view_template
if @file
a href: "/view/index.html?profile=preview#janus.url="+model_url(@file.model), target: "_blank" do
a href: "/view/index.html?profile=default#janus.url="+model_url(@file.model), target: "_blank" do
local
end
elsif @object.remote?

View file

@ -0,0 +1,42 @@
<footer class="container-fluid mt-5 py-2 border-top" id="footer">
<div class="row">
<div class="col-lg-3 me-auto">
<span class="d-inline-flex align-items-center mb-2">
<%= image_tag "roundel.svg", width: 48, height: 48, class: "me-2", alt: translate("application.title") %>
<span class="fs-5"><%= t(".powered_by_html", name: t("application.title")) %></span>
</span>
<ul class="list-unstyled small text-muted">
<li class="mb-2"><%= t ".by_html" %></li>
<li class="mb-2"><a href="https://codeberg.org/coderofsalvation/xrforge" target="_blank">Sourcecode</a></li>
<!--
<%= t ".open_source_html" %><
-->
<% if current_user&.is_administrator? %>
<li class="mb-2"><%= t ".version" %>:
<%= link_to "#{Rails.application.config.app_version} (#{Rails.application.config.git_sha.first(8)})",
"#{Rails.application.config.upstream_repo}/tree/#{Rails.application.config.git_sha}",
target: "_blank", rel: "noreferrer" %>
</li>
<% end %>
</ul>
</div>
<div class="col-4 col-lg-2 mb-3">
<h5><%= site_name(default: t(".instance_heading")) %></h5>
<ul class="list-unstyled">
<li class="mb-2"><%= link_to t(".about"), about_path %></li>
<% if SiteSettings.support_link.presence %>
<li class="mb-2"><a href="<%= SiteSettings.support_link %>" target="_blank" rel="noreferrer"><%= t ".support" %></a></li>
<% end %>
<li class="mb-2"><%= link_to t(".api"), api_url, rel: "noopener", target: "_blank" %></li>
</ul>
</div>
<div class="col-4 col-lg-2 mb-3">
<h5><%= t "application.title" %></h5>
<ul class="list-unstyled">
<li class="mb-2"><a href="https://codeberg.org/coderofsalvation/xrforge/issues" target="_blank" rel="noreferrer"><%= t ".issues" %></a></li>
<li class="mb-2"><a href="https://chat.isvery.ninja" target="_blank" rel="noreferrer"><%= t ".community" %></a></li>
<li class="mb-2"><a href="https://leondustar.gumroad.com/l/hGYGh" target="_blank" rel="noreferrer"><%= t ".sponsor" %></a></li>
</ul>
</div>
</div>
</footer>

View file

@ -40,5 +40,21 @@
</div>
</main>
<%= render "application/footer" %>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="notification" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<img src="..." class="rounded me-2" alt="...">
<strong class="me-auto"></strong>
<small></small>
</div>
<div class="toast-body">
</div>
</div>
</div>
<!-- remember telescopic text e.g. -->
<%= "<script src='assets/xrforge.js'></script>".html_safe %>
</body>
</html>

View file

@ -3,7 +3,7 @@
<div class="row row-cols-md-2 mt-2">
<div class="col-md-9" id="item_list">
<iframe allowfullscreen allow="xr-spatial-tracking; fullscreen" src="/view/index.html?overlay=true&profile=preview" style="width:100%; height:360px; border:0; border-radius:10px;"></iframe>
<iframe allowfullscreen allow="xr-spatial-tracking; fullscreen" src="/view/index.html?overlay=true&profile=default" style="width:100%; height:50vh; border:0; border-radius:10px;"></iframe>
<%= yield :items %>
</div>

View file

@ -19,6 +19,7 @@
</assets>
<room pos="0 0 0" skybox="true" showavatar="false" use_local_asset="room_plane">
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
<text js_id="location" pos="0 2.6 2" billboard="y" col="#BBFFFF" scale="5 5 1" text="<%= request.original_url %>" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" />
<CircularLayout pos="0 0 0" radius="4.5" scale="1 1 1" >
<% @models.each_with_index do |model, i| %>
<object id="o_<%= i %>">

View file

@ -60,12 +60,12 @@ end
<div class="shimmer"></div>
<img src="/assets/roundel-1d688b1e.svg" />
<br><br>
🚀 (Re)generating the experience..<br><br>
🚀 (Re)generating the experience..<br>
☕ Please be patient or check back later.<br>
</article>
</div>
<% else %>
<iframe allowfullscreen allow="xr-spatial-tracking; fullscreen" src="<%=janusURLPreview%>?networking=false" style="width:100%; height:<%=viewer_width%>px; border:0; border-radius:10px;"></iframe>
<iframe allowfullscreen allow="xr-spatial-tracking; fullscreen" src="<%=janusURLPreview%>?networking=false" style="width:100%; height: 50vh; border:0; border-radius:10px;"></iframe>
<% end %>
<div style="height:15px"></div>
<% else %>
@ -134,7 +134,7 @@ end
<%== jml %>
-->
<% if File.exist?( filePath+".xrforge/scene.gltf" ) %>
<% if File.exist?( filePath+".xrforge/scene.gltf" ) && ENV['FEDERATE_DRIVE_HOST'] %>
<tr>
<td>⁂</td>
<td>
@ -177,7 +177,7 @@ end
</tr>
<% end %>
<% if File.exist?( filePath+".xrforge/scene.mml" ) %>
<% if File.exist?( filePath+".xrforge/scene.mml" ) && ENV['FEDERATE_DRIVE_HOST'] %>
<tr>
<td>⁂</td>
<td>
@ -210,6 +210,8 @@ end
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<% if ENV['FEDERATE_DRIVE_HOST'] %>
<tr>
<td>
<i class="bi bi-journal-check" role="img"></i>
@ -232,6 +234,7 @@ end
</div>
</td>
</tr>
<% end %>
<% if @model.collection %>
<tr>
@ -308,6 +311,7 @@ end
</div>
<%= card :secondary, "Packages" do %>
<% if ENV['FEDERATE_DRIVE_HOST'] %>
<table class="table table-borderless table-sm">
<tr>
<td>
@ -331,6 +335,32 @@ end
</td>
</tr>
</table>
<% end %>
<% if ENV['FEDERATE_DRIVE_HOST'] %>
<table class="table table-borderless table-sm">
<tr>
<td>
<i class="bi bi-controller" role="img"></i>
</td>
<td>
<%= link_to "loVR project", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/lovr.zip" %>
<label for="toggle_loVR"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_loVR" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is a LoVR app (startingpoint) which wraps your (3D file) experience.<br>
<a href="https://loVR.org" target="_blank">Godot</a> is a Free and Opensource Game engine.<br>
You can modify and run the app on these platforms:
<b>WARNING</b>: use <a href="https://en.wikipedia.org/wiki/Progressive_enhancement" target="_blank">progressive enhancement</a> so your 3D file experience will always run in other <a href="https://xrfragment.org" target="_blank">XR Fragment</a> viewers.
</small>
</div>
</div>
</td>
</tr>
</table>
<% end %>
<% end %>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>

View file

@ -12,6 +12,7 @@
<link rel="stylesheet" href="/assets/themes/slate-9cc3cc7d.css" nonce="1de89072246b25ca36376d8f1cf5e051" />
<link rel="stylesheet" href="/assets/xrforge.css" />
<script src="assets/xrforge.js"></script>
</head>
<body>
@ -62,13 +63,13 @@
<h2>Turn files into AR/VR 🥽 experiences</h2>
<br>
<center>
<a href="https://codeberg.org/coderofsalvation/xrforge" class="btn btn-secondary">
<a href="https://codeberg.org/coderofsalvation/xrforge" class="btn btn-secondary" style="text-align:left">
<img src="assets/codeberg.svg" style="width:20px"/>
Check the sourcecode<br>
<small>irc.isvery.ninja port 443 via ObsidianIRC</small>
<small>docker run codeforge.org/coderofsalvation/xrforge:latest</small>
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://isvery.ninja/chat/index.html" class="btn btn-secondary" style="text-align:left">
<a href="https://chat.isvery.ninja" class="btn btn-secondary" style="text-align:left">
<img src="assets/obsidian.png" style="width:20px"/>
chat with community <br>
<small>irc.isvery.ninja port 443 via ObsidianIRC</small>
@ -105,7 +106,7 @@
<h3>Why people want XRForge</h3>
<div>
The metaverse-hype has shown: people <b>like 3D</b> but <b>existing</b> 2D ecosystems are king.<br>
The metaverse has shown: people <b>like 3D</b> but certain <b>existing</b> 2D ecosystems are king.<br>
They're just more cost-efficient to use.<br>
Hence, XRForge promotes <b>projecting these ecosystems</b> as virtual <b>hyperlinked</b> worlds.<br>
XRForge can seed itself via local or remote
@ -213,14 +214,132 @@
<div></div>
</div>
<center>
<h3>Comparison</h3>
<table class="table table-dark table-striped" id="comparison">
<thead>
<tr>
<td></td>
<td><b>XRForge</b></td>
<td><b>Other solutions</b><br>(Meta Horizon e.g.)</td>
</tr>
</thead>
<tbody>
<tr>
<td>No data-collection or login requirement</td>
<td></td>
<td></td>
</tr>
<tr>
<td>users can host content themselves</td>
<td><small>HTTP/DAT/IPFS Open Protocols</small></td>
<td><small>we own your data</small></td>
</tr>
<tr>
<td>User operated content and viewers</td>
<td></td>
<td><small>we control both</small></td>
</tr>
<tr>
<td>portals to remote content</td>
<td><small>JanusXR or <a href="https://xrfragment.org" target="_blank">XR Fragments</a> protocol</small></td>
<td></td>
</tr>
<tr>
<td>Platform does not control network</td>
<td><small>user-operated viewers and content</small></td>
<td><small>stakeholders can block anything</small></td>
</tr>
<tr>
<td>Platform to Platform portals</td>
<td><small>JanusXR or <a href="https://xrfragment.org" target="_blank">XR Fragments</a> protocol</small></td>
<td></td>
</tr>
<tr>
<td>2D web to 3D translators</td>
<td><small>JanusWeb translators</small></td>
<td></td>
</tr>
<tr>
<td>Embed 3D scene in regular file (pdf e.g.)</td>
<td><small>supported via JanusWeb</small></td>
<td></td>
</tr>
<tr>
<td>Portals from/to Internet/Intranet</td>
<td></td>
<td></td>
</tr>
<tr>
<td>User can make content private</td>
<td><small>XRForge permissions or HTTP password</small></td>
<td></td>
</tr>
<tr>
<td>Spatial anchors / URL addressibility</td>
<td><small>JanusWeb and <a href="https://xrfragment.org" target="_blank">XR Fragments</a> protocol</small></td>
<td></td>
</tr>
<tr>
<td>Direct portals to 3D file</td>
<td><small>Janusweb and <a href="https://xrfragment.org" target="_blank">XR Fragments</a> protocol</small></td>
<td></td>
</tr>
<tr>
<td>Detect 3D scene in webpage URL</td>
<td><small>Janusweb and <a href="https://xrfragment.org" target="_blank">XR Fragments</a> protocol</small></td>
<td></td>
</tr>
<tr>
<td>Export data + Credible exit</td>
<td><small>server-export runs on opensource <a href="https://github.com/jbaicoianu/janusweb" target="_blank">janusweb</a> viewer</small></td>
<td></td>
</tr>
<tr>
<td>Lightweight (runs on Raspberry PI e.g.)</td>
<td></td>
<td></td>
</tr>
<tr>
<td>XR worlds at rest</td>
<td></td>
<td><small>compute required for nonvisited spaces</small></td>
</tr>
<tr>
<td>Integrates with Fediverse</td>
<td><small>via ActivityPub and RSS/HTML translators</small></td>
<td></td>
</tr>
<tr>
<td>Integrate with (own) dataclouds</td>
<td style="width:33%"><small>scroll down below</small>
<div style="max-height: 200px; overflow-y: scroll; background: var(--bs-body-bg); padding: 10px; border-radius: 9px;">
<small>
Via rclone: Google Drive, Amazon S3, Microsoft OneDrive, Dropbox, Backblaze B2, SFTP, Google Cloud Storage (GCS), Microsoft Azure Blob Storage, MinIO, FTP, WebDAV, Wasabi, Cloudflare R2, Local Filesystem, MEGA, pCloud, Nextcloud, DigitalOcean Spaces, Hetzner Storage Box, Box, Google Photos, iCloud Drive, SMB/CIFS, Oracle Object Storage, Proton Drive, IDrive e2, Alibaba Cloud (Aliyun) OSS, Yandex Disk, Hetzner Object Storage, OVHcloud, ownCloud, Scaleway, Koofr, Put.io, Seafile, Mail.ru, 1Fichier, Akamai NetStorage, Internet Archive, Jottacloud, Storj, Tencent COS, Huawei OBS, Qiniu Kodo, OpenStack Swift, Arvan Cloud AOS, Bizfly Cloud, Ceph, China Mobile Ecloud, Citrix ShareFile, Cloudinary, Cubbit, Digi Storage, Dreamhost, Drime, Enterprise File Fabric, Exaba, Fastmail Files, FileLu Cloud, Filen, Files.com, Gofile, HDFS, HiDrive, HTTP, ImageKit, Internxt, IONOS Cloud, Leviia, Liara, Linkbox, Magalu, Memory, OpenDrive, Oracle Cloud Storage, Outscale, Petabox, PikPak, Pixeldrain, Premiumize.me, QingStor, Quatrix, Rabata, RackCorp, Rackspace Cloud Files, Rsync.net, Scaleway, Seafile, Seagate Lyve Cloud, SeaweedFS, Selectel, Servercore, Spectra Logic, Storj, SugarSync, Uloz.to, Zoho WorkDrive</small>
</small>
</td>
<td>
</td>
</body>
</table>
</center>
<div class="spectrum">
<div></div>
<div></div>
</div>
<center>
<h3>Supporter of Open XR Hypermedia stacks</h3>
<div style="max-width:945px;">
<a href="https://coderofsalvation.github.io/janus-guide/" target="_blank">
<img src="/assets/janusxr-xrf.png"/>
<img src="/assets/janusxr-xrf.png" style="width:100%"/>
</a>
<br>
</div>
<br>
</center>
<div class="spectrum">
@ -398,14 +517,5 @@
</main>
<script>
// telescopic text:
// a JS cheat whicht allows persisting unfolds
// uncomment this if you really want this
([...document.querySelectorAll('u')]).map( (u) => {
u.addEventListener('click', e => e.target.className = 'show' )
});
</script>
</body>
</html>

View file

@ -493,3 +493,12 @@ input[type="radio"][id="browser"]:checked ~ .openlearning {
transform: scale(0.8);
}
}
.form-control::-moz-placeholder {
color: #888;
opacity: 1;
}
.form-control::placeholder {
color: #888;
opacity: 1;
}

View file

@ -0,0 +1,15 @@
window.notify = function(opts){
let { html,title, subtitle, domid, timeout } = opts
let el = document.querySelector( domid || '#notification')
if( title ) el.querySelector(".toast-header").innerHTML = `<strong class="me-auto">${title}</strong><small>${subtitle||''}</small>`
if( html ) el.querySelector(".toast-body").innerHTML = html
el.style.display = 'block'
setTimeout( () => el.style.display = 'none', timeout || 5000 )
};
// telescopic text:
// a JS cheat whicht allows persisting unfolds
// uncomment this if you really want this
([...document.querySelectorAll('u')]).map( (u) => {
u.addEventListener('click', e => e.target.className = 'show' )
});